Skip to content

Commit caf4423

Browse files
Rajan MauryaRajan Maurya
authored andcommitted
feat(feature/activate): migrate ActivateViewModel to SubmitHandler<Unit>
Replaces manual DataState collection with template.core.base.store's SubmitHandler — a one-shot, idempotent submission executor with built-in Submitting / Submitted / Failed lifecycle and automatic cancel-prior-job semantics for sequential calls. VM: - Single SubmitHandler<Unit> instead of three viewModelScope.launch blocks - submit.state mapped to existing ActivateUiState via .map.stateIn so Screen-side state shape (Initial / Loading / ActivatedSuccessfully / Error) is unchanged — no Screen changes required - Per-action success/failure StringResource tracked in two vars set before each submit; safe because they're written synchronously with submit.submit { ... } and only read when state transitions to a terminal value - ActivateClientUseCase / ActivateCenterUseCase (Flow<DataState<T>>) bridged via .first { it !is DataState.Loading } + throw on Error - ActivateGroupUseCase (suspend, returns DataState<Unit> directly) just checks for Error and throws build.gradle.kts: + implementation(projects.coreBase.store) This is the migration template for all subsequent mutation-feature migrations (auth, savings, recurringDeposit, note, checker-inbox-task, collectionSheet, report, document, loan, client). Each follows the same shape: introduce a SubmitHandler<R> in the VM, map its state to the feature's existing UiState, bridge use-case return shapes through SubmitHandler.submit { ... }, leave the Screen untouched.
1 parent dd14cda commit caf4423

4 files changed

Lines changed: 76 additions & 48 deletions

File tree

core/data/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ kotlin {
4040
api(projects.core.network)
4141
api(projects.core.database)
4242
api(projects.coreBase.common)
43-
api(projects.coreBase.store)
43+
api(projects.core.store)
4444

4545

4646
implementation(libs.mifos.authenticator.passcode)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mifos-x-field-officer-app/blob/master/LICENSE.md
9+
*/
10+
package com.mifos.core.data.store
11+
12+
import kotlinx.coroutines.CoroutineScope
13+
14+
/**
15+
* App-facing re-exports of the offline-first submission primitives.
16+
*
17+
* Feature modules import these from `com.mifos.core.data.store` and never reach into
18+
* `template.core.base.store.*` directly. The dependency direction is enforced:
19+
* core-base/store → core/store → core/data → feature/*
20+
*/
21+
22+
typealias SubmitHandler<R> = template.core.base.store.submit.SubmitHandler<R>
23+
typealias SubmitState<R> = template.core.base.store.submit.SubmitState<R>
24+
25+
/** Creates a [SubmitHandler] bound to this [CoroutineScope] (typically `viewModelScope`). */
26+
fun <R> CoroutineScope.submitHandler(): SubmitHandler<R> =
27+
template.core.base.store.submit.submitHandler()

feature/activate/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ kotlin {
2222
implementation(compose.material3)
2323
implementation(compose.components.resources)
2424
implementation(compose.ui)
25+
implementation(projects.core.data)
2526
implementation(projects.core.domain)
2627
implementation(compose.components.uiToolingPreview)
2728
}

feature/activate/src/commonMain/kotlin/com/mifos/feature/activate/ActivateViewModel.kt

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,18 @@ import androidx.lifecycle.ViewModel
2121
import androidx.lifecycle.viewModelScope
2222
import androidx.navigation.toRoute
2323
import com.mifos.core.common.utils.DataState
24+
import com.mifos.core.data.store.SubmitState
25+
import com.mifos.core.data.store.submitHandler
2426
import com.mifos.core.domain.useCases.ActivateCenterUseCase
2527
import com.mifos.core.domain.useCases.ActivateClientUseCase
2628
import com.mifos.core.domain.useCases.ActivateGroupUseCase
2729
import com.mifos.core.model.objects.clients.ActivatePayload
28-
import kotlinx.coroutines.flow.MutableStateFlow
29-
import kotlinx.coroutines.flow.asStateFlow
30-
import kotlinx.coroutines.launch
30+
import kotlinx.coroutines.flow.SharingStarted
31+
import kotlinx.coroutines.flow.StateFlow
32+
import kotlinx.coroutines.flow.first
33+
import kotlinx.coroutines.flow.map
34+
import kotlinx.coroutines.flow.stateIn
35+
import org.jetbrains.compose.resources.StringResource
3136

3237
class ActivateViewModel(
3338
private val activateClientUseCase: ActivateClientUseCase,
@@ -39,56 +44,51 @@ class ActivateViewModel(
3944
val id = savedStateHandle.toRoute<ActivateRoute>().id
4045
val activateType = savedStateHandle.toRoute<ActivateRoute>().type
4146

42-
private val _activateUiState = MutableStateFlow<ActivateUiState>(ActivateUiState.Initial)
43-
val activateUiState = _activateUiState.asStateFlow()
47+
private val submit = viewModelScope.submitHandler<Unit>()
48+
private var successMessage: StringResource = Res.string.feature_activate_client
49+
private var failureMessage: StringResource = Res.string.feature_activate_failed_to_activate_client
4450

45-
fun activateClient(clientId: Int, clientPayload: ActivatePayload) =
46-
viewModelScope.launch {
47-
activateClientUseCase(clientId, clientPayload).collect { result ->
48-
when (result) {
49-
is DataState.Error ->
50-
_activateUiState.value =
51-
ActivateUiState.Error(Res.string.feature_activate_failed_to_activate_client)
52-
53-
is DataState.Loading -> _activateUiState.value = ActivateUiState.Loading
54-
55-
is DataState.Success ->
56-
_activateUiState.value =
57-
ActivateUiState.ActivatedSuccessfully(Res.string.feature_activate_client)
58-
}
51+
val activateUiState: StateFlow<ActivateUiState> = submit.state
52+
.map { state ->
53+
when (state) {
54+
is SubmitState.Idle -> ActivateUiState.Initial
55+
is SubmitState.Submitting -> ActivateUiState.Loading
56+
is SubmitState.Submitted<*> -> ActivateUiState.ActivatedSuccessfully(successMessage)
57+
is SubmitState.Failed -> ActivateUiState.Error(failureMessage)
5958
}
6059
}
60+
.stateIn(
61+
scope = viewModelScope,
62+
started = SharingStarted.Eagerly,
63+
initialValue = ActivateUiState.Initial,
64+
)
6165

62-
fun activateCenter(centerId: Int, centerPayload: ActivatePayload) =
63-
viewModelScope.launch {
64-
activateCenterUseCase(centerId, centerPayload).collect { result ->
65-
when (result) {
66-
is DataState.Error ->
67-
_activateUiState.value =
68-
ActivateUiState.Error(Res.string.feature_activate_failed_to_activate_center)
69-
70-
is DataState.Loading -> _activateUiState.value = ActivateUiState.Loading
71-
72-
is DataState.Success ->
73-
_activateUiState.value =
74-
ActivateUiState.ActivatedSuccessfully(Res.string.feature_activate_center)
75-
}
76-
}
66+
fun activateClient(clientId: Int, clientPayload: ActivatePayload) {
67+
successMessage = Res.string.feature_activate_client
68+
failureMessage = Res.string.feature_activate_failed_to_activate_client
69+
submit.submit {
70+
val terminal = activateClientUseCase(clientId, clientPayload)
71+
.first { it !is DataState.Loading }
72+
if (terminal is DataState.Error) throw terminal.exception
7773
}
74+
}
7875

79-
fun activateGroup(groupId: Int, groupPayload: ActivatePayload) =
80-
viewModelScope.launch {
81-
val result = activateGroupUseCase(groupId, groupPayload)
82-
when (result) {
83-
is DataState.Error ->
84-
_activateUiState.value =
85-
ActivateUiState.Error(Res.string.feature_activate_failed_to_activate_group)
86-
87-
DataState.Loading -> Unit // unreachable
76+
fun activateCenter(centerId: Int, centerPayload: ActivatePayload) {
77+
successMessage = Res.string.feature_activate_center
78+
failureMessage = Res.string.feature_activate_failed_to_activate_center
79+
submit.submit {
80+
val terminal = activateCenterUseCase(centerId, centerPayload)
81+
.first { it !is DataState.Loading }
82+
if (terminal is DataState.Error) throw terminal.exception
83+
}
84+
}
8885

89-
is DataState.Success ->
90-
_activateUiState.value =
91-
ActivateUiState.ActivatedSuccessfully(Res.string.feature_activate_group)
92-
}
86+
fun activateGroup(groupId: Int, groupPayload: ActivatePayload) {
87+
successMessage = Res.string.feature_activate_group
88+
failureMessage = Res.string.feature_activate_failed_to_activate_group
89+
submit.submit {
90+
val terminal = activateGroupUseCase(groupId, groupPayload)
91+
if (terminal is DataState.Error) throw terminal.exception
9392
}
93+
}
9494
}

0 commit comments

Comments
 (0)