fix(#7): emit snackbar as resource id, resolve with stringResource in UI

Drop the Context injection from the ScreenModel. Emit a sealed
ImportSnackbarMessage carrying either a @StringRes id or a dynamic
text (for the error.message case where the bank importer surfaces a
useful 'BRI import not yet implemented' string). The screen resolves
resource ids via stringResource() in Composable scope and shows the
snackbar; the original error.message info is preserved via the
Dynamic variant instead of being dropped.
This commit is contained in:
Achmad Setyabudi Susilo
2026-06-28 18:46:17 +07:00
parent 5893ffc955
commit 22863bfcd6
2 changed files with 26 additions and 8 deletions
@@ -86,9 +86,22 @@ object ImportBankStatementScreen : Screen {
pendingImporter = null
}
val importFailedText = stringResource(R.string.import_failed)
val importNoTransactionsText = stringResource(R.string.import_no_transactions)
val importNoItemsSelectedText = stringResource(R.string.import_no_items_selected)
LaunchedEffect(Unit) {
screenModel.snackbar.collect { message ->
snackbarHostState.showSnackbar(message)
val text = when (message) {
is ImportSnackbarMessage.Resource -> when (message.id) {
R.string.import_failed -> importFailedText
R.string.import_no_transactions -> importNoTransactionsText
R.string.import_no_items_selected -> importNoItemsSelectedText
else -> null
}
is ImportSnackbarMessage.Dynamic -> message.text
}
text?.let { snackbarHostState.showSnackbar(it) }
}
}
@@ -1,7 +1,7 @@
package dev.achmad.ledgerr.ui.screens.import_bank_statement
import android.content.Context
import android.net.Uri
import androidx.annotation.StringRes
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.Navigator
@@ -36,18 +36,22 @@ sealed interface ImportState {
) : ImportState
}
sealed interface ImportSnackbarMessage {
data class Resource(@StringRes val id: Int) : ImportSnackbarMessage
data class Dynamic(val text: String) : ImportSnackbarMessage
}
class ImportBankStatementScreenModel(
private val importers: List<BankStatementImporter> = inject(),
private val getCategories: GetCategories = inject(),
private val insertExpenses: InsertExpenses = inject(),
private val context: Context = inject(),
) : ScreenModel {
private val _state = MutableStateFlow<ImportState>(ImportState.BankPicker(importers))
val state: StateFlow<ImportState> = _state.asStateFlow()
private val _snackbar = MutableSharedFlow<String>(extraBufferCapacity = 1)
val snackbar: SharedFlow<String> = _snackbar.asSharedFlow()
private val _snackbar = MutableSharedFlow<ImportSnackbarMessage>(extraBufferCapacity = 1)
val snackbar: SharedFlow<ImportSnackbarMessage> = _snackbar.asSharedFlow()
val categories: StateFlow<List<Category>> = getCategories.subscribeAll()
.flowOn(Dispatchers.IO)
@@ -69,7 +73,7 @@ class ImportBankStatementScreenModel(
.onSuccess { rows ->
if (rows.isEmpty()) {
_state.value = ImportState.BankPicker(importers)
_snackbar.tryEmit(context.getString(R.string.import_no_transactions))
_snackbar.tryEmit(ImportSnackbarMessage.Resource(R.string.import_no_transactions))
} else {
_state.value = ImportState.Confirmation(importer.bankName, rows)
}
@@ -77,7 +81,8 @@ class ImportBankStatementScreenModel(
.onFailure { error ->
_state.value = ImportState.BankPicker(importers)
val message = error.message?.takeIf { it.isNotBlank() }
?: context.getString(R.string.import_failed)
?.let { ImportSnackbarMessage.Dynamic(it) }
?: ImportSnackbarMessage.Resource(R.string.import_failed)
_snackbar.tryEmit(message)
}
}
@@ -115,7 +120,7 @@ class ImportBankStatementScreenModel(
val confirmation = _state.value as? ImportState.Confirmation ?: return@launch
val selected = confirmation.rows.filter { it.isSelected }
if (selected.isEmpty()) {
_snackbar.tryEmit(context.getString(R.string.import_no_items_selected))
_snackbar.tryEmit(ImportSnackbarMessage.Resource(R.string.import_no_items_selected))
return@launch
}
withContext(Dispatchers.IO) {