fix(#7): move DB/IO off main thread in ScreenModels
screenModelScope is backed by PlatformMainDispatcher (Main.immediate), so direct interactor calls run DB queries and file I/O on the UI thread. Switch reactive flows with .flowOn(Dispatchers.IO) and wrap suspend calls in withContext(Dispatchers.IO).
This commit is contained in:
@@ -7,10 +7,13 @@ import dev.achmad.ledgerr.domain.category.interactor.DeleteCategory
|
||||
import dev.achmad.ledgerr.domain.category.interactor.GetCategories
|
||||
import dev.achmad.ledgerr.domain.category.interactor.UpsertCategory
|
||||
import dev.achmad.ledgerr.domain.category.model.Category
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class CategoryScreenModel(
|
||||
private val getCategories: GetCategories = inject(),
|
||||
@@ -19,6 +22,7 @@ class CategoryScreenModel(
|
||||
) : ScreenModel {
|
||||
|
||||
val categories: StateFlow<List<Category>> = getCategories.subscribeAll()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope = screenModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
@@ -27,13 +31,17 @@ class CategoryScreenModel(
|
||||
|
||||
fun upsert(category: Category) {
|
||||
screenModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
upsertCategory.await(category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(id: Long) {
|
||||
screenModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
runCatching { deleteCategory.await(id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
-4
@@ -11,6 +11,7 @@ import dev.achmad.ledgerr.domain.category.interactor.GetCategories
|
||||
import dev.achmad.ledgerr.domain.category.model.Category
|
||||
import dev.achmad.ledgerr.domain.expense.interactor.InsertExpenses
|
||||
import dev.achmad.ledgerr.domain.expense.model.Expense
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
@@ -18,9 +19,11 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
sealed interface ImportState {
|
||||
data class BankPicker(val importers: List<BankStatementImporter>) : ImportState
|
||||
@@ -44,6 +47,7 @@ class ImportBankStatementScreenModel(
|
||||
val snackbar: SharedFlow<String> = _snackbar.asSharedFlow()
|
||||
|
||||
val categories: StateFlow<List<Category>> = getCategories.subscribeAll()
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope = screenModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
@@ -55,7 +59,9 @@ class ImportBankStatementScreenModel(
|
||||
fun processPdf(uri: Uri, importer: BankStatementImporter) {
|
||||
screenModelScope.launch {
|
||||
_state.value = ImportState.Processing(importer.bankName)
|
||||
val result = importer.await(uri)
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
importer.await(uri)
|
||||
}
|
||||
result
|
||||
.onSuccess { rows ->
|
||||
if (rows.isEmpty()) {
|
||||
@@ -105,14 +111,15 @@ class ImportBankStatementScreenModel(
|
||||
fun confirm(navigator: Navigator) {
|
||||
screenModelScope.launch {
|
||||
val confirmation = _state.value as? ImportState.Confirmation ?: return@launch
|
||||
if (defaultCategoryId == 0L) {
|
||||
defaultCategoryId = getCategories.awaitDefault().id
|
||||
}
|
||||
val selected = confirmation.rows.filter { it.isSelected }
|
||||
if (selected.isEmpty()) {
|
||||
_snackbar.tryEmit("No items selected")
|
||||
return@launch
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
if (defaultCategoryId == 0L) {
|
||||
defaultCategoryId = getCategories.awaitDefault().id
|
||||
}
|
||||
val expenses = selected.map { pending ->
|
||||
Expense(
|
||||
amount = pending.amount,
|
||||
@@ -122,6 +129,7 @@ class ImportBankStatementScreenModel(
|
||||
)
|
||||
}
|
||||
insertExpenses.awaitAll(expenses)
|
||||
}
|
||||
navigator.pop()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import dev.achmad.ledgerr.domain.category.interactor.SeedDefaultCategories
|
||||
import dev.achmad.ledgerr.domain.data.interactor.ClearAllData
|
||||
import dev.achmad.ledgerr.domain.expense.model.DateRange
|
||||
import dev.achmad.ledgerr.domain.export.interactor.ExportExpensesToCsv
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class SettingsScreenModel(
|
||||
private val exportExpensesToCsv: ExportExpensesToCsv = inject(),
|
||||
@@ -18,16 +20,21 @@ class SettingsScreenModel(
|
||||
|
||||
fun exportToCsv(range: DateRange, uri: Uri, onResult: (Result<Unit>) -> Unit) {
|
||||
screenModelScope.launch {
|
||||
onResult(exportExpensesToCsv.await(range, uri))
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
exportExpensesToCsv.await(range, uri)
|
||||
}
|
||||
onResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearData(onResult: (Result<Unit>) -> Unit) {
|
||||
screenModelScope.launch {
|
||||
val result = runCatching {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
runCatching {
|
||||
clearAllData.await()
|
||||
seedDefaultCategories.await()
|
||||
}
|
||||
}
|
||||
onResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user