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.GetCategories
|
||||||
import dev.achmad.ledgerr.domain.category.interactor.UpsertCategory
|
import dev.achmad.ledgerr.domain.category.interactor.UpsertCategory
|
||||||
import dev.achmad.ledgerr.domain.category.model.Category
|
import dev.achmad.ledgerr.domain.category.model.Category
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class CategoryScreenModel(
|
class CategoryScreenModel(
|
||||||
private val getCategories: GetCategories = inject(),
|
private val getCategories: GetCategories = inject(),
|
||||||
@@ -19,6 +22,7 @@ class CategoryScreenModel(
|
|||||||
) : ScreenModel {
|
) : ScreenModel {
|
||||||
|
|
||||||
val categories: StateFlow<List<Category>> = getCategories.subscribeAll()
|
val categories: StateFlow<List<Category>> = getCategories.subscribeAll()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = screenModelScope,
|
scope = screenModelScope,
|
||||||
started = SharingStarted.Eagerly,
|
started = SharingStarted.Eagerly,
|
||||||
@@ -27,13 +31,17 @@ class CategoryScreenModel(
|
|||||||
|
|
||||||
fun upsert(category: Category) {
|
fun upsert(category: Category) {
|
||||||
screenModelScope.launch {
|
screenModelScope.launch {
|
||||||
upsertCategory.await(category)
|
withContext(Dispatchers.IO) {
|
||||||
|
upsertCategory.await(category)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(id: Long) {
|
fun delete(id: Long) {
|
||||||
screenModelScope.launch {
|
screenModelScope.launch {
|
||||||
runCatching { deleteCategory.await(id) }
|
withContext(Dispatchers.IO) {
|
||||||
|
runCatching { deleteCategory.await(id) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-12
@@ -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.category.model.Category
|
||||||
import dev.achmad.ledgerr.domain.expense.interactor.InsertExpenses
|
import dev.achmad.ledgerr.domain.expense.interactor.InsertExpenses
|
||||||
import dev.achmad.ledgerr.domain.expense.model.Expense
|
import dev.achmad.ledgerr.domain.expense.model.Expense
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharedFlow
|
import kotlinx.coroutines.flow.SharedFlow
|
||||||
@@ -18,9 +19,11 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
sealed interface ImportState {
|
sealed interface ImportState {
|
||||||
data class BankPicker(val importers: List<BankStatementImporter>) : ImportState
|
data class BankPicker(val importers: List<BankStatementImporter>) : ImportState
|
||||||
@@ -44,6 +47,7 @@ class ImportBankStatementScreenModel(
|
|||||||
val snackbar: SharedFlow<String> = _snackbar.asSharedFlow()
|
val snackbar: SharedFlow<String> = _snackbar.asSharedFlow()
|
||||||
|
|
||||||
val categories: StateFlow<List<Category>> = getCategories.subscribeAll()
|
val categories: StateFlow<List<Category>> = getCategories.subscribeAll()
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = screenModelScope,
|
scope = screenModelScope,
|
||||||
started = SharingStarted.Eagerly,
|
started = SharingStarted.Eagerly,
|
||||||
@@ -55,7 +59,9 @@ class ImportBankStatementScreenModel(
|
|||||||
fun processPdf(uri: Uri, importer: BankStatementImporter) {
|
fun processPdf(uri: Uri, importer: BankStatementImporter) {
|
||||||
screenModelScope.launch {
|
screenModelScope.launch {
|
||||||
_state.value = ImportState.Processing(importer.bankName)
|
_state.value = ImportState.Processing(importer.bankName)
|
||||||
val result = importer.await(uri)
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
importer.await(uri)
|
||||||
|
}
|
||||||
result
|
result
|
||||||
.onSuccess { rows ->
|
.onSuccess { rows ->
|
||||||
if (rows.isEmpty()) {
|
if (rows.isEmpty()) {
|
||||||
@@ -105,23 +111,25 @@ class ImportBankStatementScreenModel(
|
|||||||
fun confirm(navigator: Navigator) {
|
fun confirm(navigator: Navigator) {
|
||||||
screenModelScope.launch {
|
screenModelScope.launch {
|
||||||
val confirmation = _state.value as? ImportState.Confirmation ?: return@launch
|
val confirmation = _state.value as? ImportState.Confirmation ?: return@launch
|
||||||
if (defaultCategoryId == 0L) {
|
|
||||||
defaultCategoryId = getCategories.awaitDefault().id
|
|
||||||
}
|
|
||||||
val selected = confirmation.rows.filter { it.isSelected }
|
val selected = confirmation.rows.filter { it.isSelected }
|
||||||
if (selected.isEmpty()) {
|
if (selected.isEmpty()) {
|
||||||
_snackbar.tryEmit("No items selected")
|
_snackbar.tryEmit("No items selected")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val expenses = selected.map { pending ->
|
withContext(Dispatchers.IO) {
|
||||||
Expense(
|
if (defaultCategoryId == 0L) {
|
||||||
amount = pending.amount,
|
defaultCategoryId = getCategories.awaitDefault().id
|
||||||
categoryId = pending.suggestedCategoryId ?: defaultCategoryId,
|
}
|
||||||
date = pending.date,
|
val expenses = selected.map { pending ->
|
||||||
note = pending.description,
|
Expense(
|
||||||
)
|
amount = pending.amount,
|
||||||
|
categoryId = pending.suggestedCategoryId ?: defaultCategoryId,
|
||||||
|
date = pending.date,
|
||||||
|
note = pending.description,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
insertExpenses.awaitAll(expenses)
|
||||||
}
|
}
|
||||||
insertExpenses.awaitAll(expenses)
|
|
||||||
navigator.pop()
|
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.data.interactor.ClearAllData
|
||||||
import dev.achmad.ledgerr.domain.expense.model.DateRange
|
import dev.achmad.ledgerr.domain.expense.model.DateRange
|
||||||
import dev.achmad.ledgerr.domain.export.interactor.ExportExpensesToCsv
|
import dev.achmad.ledgerr.domain.export.interactor.ExportExpensesToCsv
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class SettingsScreenModel(
|
class SettingsScreenModel(
|
||||||
private val exportExpensesToCsv: ExportExpensesToCsv = inject(),
|
private val exportExpensesToCsv: ExportExpensesToCsv = inject(),
|
||||||
@@ -18,15 +20,20 @@ class SettingsScreenModel(
|
|||||||
|
|
||||||
fun exportToCsv(range: DateRange, uri: Uri, onResult: (Result<Unit>) -> Unit) {
|
fun exportToCsv(range: DateRange, uri: Uri, onResult: (Result<Unit>) -> Unit) {
|
||||||
screenModelScope.launch {
|
screenModelScope.launch {
|
||||||
onResult(exportExpensesToCsv.await(range, uri))
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
exportExpensesToCsv.await(range, uri)
|
||||||
|
}
|
||||||
|
onResult(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearData(onResult: (Result<Unit>) -> Unit) {
|
fun clearData(onResult: (Result<Unit>) -> Unit) {
|
||||||
screenModelScope.launch {
|
screenModelScope.launch {
|
||||||
val result = runCatching {
|
val result = withContext(Dispatchers.IO) {
|
||||||
clearAllData.await()
|
runCatching {
|
||||||
seedDefaultCategories.await()
|
clearAllData.await()
|
||||||
|
seedDefaultCategories.await()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onResult(result)
|
onResult(result)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user