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.webhooks
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -30,7 +29,6 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
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.text.font.FontWeight
@@ -42,6 +40,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
import com.m57.hermescontrol.ui.common.listContentPadding
import com.m57.hermescontrol.ui.common.listItemSpacing
@@ -53,7 +52,6 @@ fun WebhooksScreen(
viewModel: WebhooksViewModel = viewModel { WebhooksViewModel() },
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
var query by remember { mutableStateOf("") }
@@ -69,12 +67,7 @@ fun WebhooksScreen(
viewModel.loadWebhooks()
}
LaunchedEffect(state.toastMessage) {
state.toastMessage?.let { msg ->
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
viewModel.clearToast()
}
}
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
HermesScaffold(
title = { Text(stringResource(R.string.screen_webhooks)) },
@@ -7,6 +7,8 @@ import com.m57.hermescontrol.data.model.WebhooksToggleRequest
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.ui.common.ToastHost
import com.m57.hermescontrol.ui.common.safeLaunchLoad
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -25,40 +27,34 @@ data class WebhooksUiState(
val toastMessage: String? = null,
)
class WebhooksViewModel : ViewModel() {
class WebhooksViewModel : ViewModel(), ToastHost {
private val _uiState = MutableStateFlow(WebhooksUiState())
val uiState: StateFlow<WebhooksUiState> = _uiState.asStateFlow()
fun loadWebhooks() {
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
viewModelScope.launch {
val result =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getWebhooks() }
safeLaunchLoad(
apiCall = { safeApiCall { ApiClient.hermesApi.getWebhooks() } },
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
onSuccess = { data ->
val body = data
_uiState.update {
it.copy(
isLoading = false,
enabled = body.enabled,
baseUrl = body.base_url,
subscriptions = body.subscriptions.orEmpty(),
)
}
when (result) {
is NetworkResult.Success -> {
val body = result.data
_uiState.update {
it.copy(
isLoading = false,
enabled = body.enabled,
baseUrl = body.base_url,
subscriptions = body.subscriptions.orEmpty(),
)
}
},
onError = { errorMsg ->
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Failed to load webhooks: $errorMsg",
)
}
is NetworkResult.Failure -> {
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Failed to load webhooks: ${result.error.message}",
)
}
}
}
}
},
)
}
fun toggleWebhooks(targetEnabled: Boolean) {
@@ -92,7 +88,7 @@ class WebhooksViewModel : ViewModel() {
}
}
fun clearToast() {
override fun clearToast() {
_uiState.update { it.copy(toastMessage = null) }
}
}