Refactor: Extract boilerplate: safeLaunchLoad, clearToast, ToastEffect (CODE-21/22/23) (#312)

- Extracted `safeLaunchLoad` inline extension function to `ViewModelExt.kt` to encapsulate API call success/failure handling logic and replaced usages across 18 viewmodels.
- Extracted `ToastHost` interface to standardize `clearToast()` behavior across all ViewModels displaying toasts.
- Extracted `ToastEffect` composable to `SharedComponents.kt` and applied it to 16 screens, reducing repetitive side effect code blocks.

Fixes #286
This commit is contained in:
M57 authored and GitHub committed 2026-06-22 23:54:42 +03:00
1 parent 6abfabdac0
commit 98c02879b4
36 files changed
+427 -585

No files matched your search

@@ -1,6 +1,5 @@
package com.m57.hermescontrol.ui.logs
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
@@ -20,7 +19,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
@@ -34,6 +32,7 @@ import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState
import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.ToastEffect
@Composable
fun LogsScreen(
@@ -42,7 +41,6 @@ fun LogsScreen(
viewModel: LogsViewModel = viewModel { LogsViewModel() },
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val spacing = LocalSpacing.current
val listState = rememberLazyListState()
@@ -59,12 +57,7 @@ fun LogsScreen(
viewModel.loadLogs()
}
LaunchedEffect(state.toastMessage) {
state.toastMessage?.let { msg ->
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
viewModel.clearToast()
}
}
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
// Auto scroll to bottom when new logs arrive
LaunchedEffect(filteredLogs.size) {
@@ -1,17 +1,14 @@
package com.m57.hermescontrol.ui.logs
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.remote.safeApiCall
import kotlinx.coroutines.Dispatchers
import com.m57.hermescontrol.ui.common.ToastHost
import com.m57.hermescontrol.ui.common.safeLaunchLoad
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
data class LogsUiState(
val isLoading: Boolean = false,
@@ -20,37 +17,31 @@ data class LogsUiState(
val toastMessage: String? = null,
)
class LogsViewModel : ViewModel() {
class LogsViewModel : ViewModel(), ToastHost {
private val _uiState = MutableStateFlow(LogsUiState())
val uiState: StateFlow<LogsUiState> = _uiState.asStateFlow()
fun loadLogs() {
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
viewModelScope.launch {
val result =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getLogs() }
safeLaunchLoad(
apiCall = { safeApiCall { ApiClient.hermesApi.getLogs() } },
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
onSuccess = { data ->
val body = data
val logsList = body.lines ?: body.logs ?: emptyList()
_uiState.update { it.copy(isLoading = false, logs = logsList) }
},
onError = { errorMsg ->
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Failed to load logs: $errorMsg",
)
}
when (result) {
is NetworkResult.Success -> {
val body = result.data
val logsList = body.lines ?: body.logs ?: emptyList()
_uiState.update { it.copy(isLoading = false, logs = logsList) }
}
is NetworkResult.Failure -> {
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Failed to load logs: ${result.error.message}",
)
}
}
}
}
},
)
}
fun clearToast() {
override fun clearToast() {
_uiState.update { it.copy(toastMessage = null) }
}
}