Repository navigation
Expand file tree
/
Copy pathBaseViewModel.kt
More file actions
120 lines (107 loc) · 4.53 KB
/
Copy pathBaseViewModel.kt
File metadata and controls
120 lines (107 loc) · 4.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package com.getcode.view
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
abstract class BaseViewModel<ViewState : Any, Event : Any>(
initialState: ViewState,
private val updateStateForEvent: (Event) -> (ViewState.() -> ViewState),
private val defaultDispatcher: CoroutineContext = Dispatchers.Default,
) : ViewModel() {
private val _eventFlow: MutableSharedFlow<Event> = MutableSharedFlow()
val eventFlow: SharedFlow<Event> = _eventFlow.asSharedFlow()
private val _stateFlow: MutableStateFlow<ViewState> = MutableStateFlow(initialState)
val stateFlow: StateFlow<ViewState> = _stateFlow.asStateFlow()
fun dispatchEvent(event: Event) {
setState(updateStateForEvent(event))
viewModelScope.launch(defaultDispatcher) {
_eventFlow.emit(event)
}
}
suspend fun dispatchEvent(context: CoroutineContext, event: Event) {
withContext(context) {
dispatchEvent(event)
}
}
/**
* Show a confirm button's success state, hold it long enough to be read, then move on.
*
* The hold starts when [success] is dispatched, not when the work began: by the time a screen
* has a result to act on, the checkmark has not been drawn yet, and navigating on the same
* frame swaps it away before anyone sees it. That is what separates this from
* `Result.onSuccessWithDelay`, which enforces a minimum duration for the *operation* and so
* holds for nothing once the operation itself is slow.
*
* [then] runs on [viewModelScope], so it is cancelled with the ViewModel rather than pushing a
* route the user has already left.
*/
protected fun dispatchSuccessThen(
success: Event,
hold: Duration = SuccessHoldDuration,
then: suspend () -> Unit,
): Job = viewModelScope.launch {
dispatchEvent(success)
delay(hold)
then()
}
// Events are dispatched from multiple threads — the UI thread and background flows on
// defaultDispatcher — so this must be an atomic compare-and-set, not a plain
// read-modify-write. A non-atomic assignment lets concurrent reducers derive from the same
// snapshot and clobber each other, silently dropping one event's state change.
private fun setState(update: ViewState.() -> ViewState) {
_stateFlow.update(update)
}
}
/**
* How long a confirm button holds its checkmark before the screen it sits on moves on.
*
* Matches what the username-claim, name-entry and photo-selection screens already wait; the
* longer holds elsewhere (verification's second, the deposit screen's two) are their own beat, not
* this one.
*/
val SuccessHoldDuration = 500.milliseconds
/**
* The least time a confirm button's spinner stays up once it has gone up.
*
* A floor, not a hold: work that already ran longer moves on as soon as it is done, and only work
* that answers faster than this is waited out. Pairs with [SuccessHoldDuration] at the other end —
* between them a fast call still reads as start, work, done, rather than crossfading from a label
* to a checkmark with nothing in between.
*
* Measure it from the frame the spinner was asked for. Measuring from the call's return, as
* `Result.onSuccessWithDelay` does, always waits the full duration, because by then the work is
* over — which adds latency to a slow call rather than putting a floor under a fast one.
*/
val MinimumLoadingDuration = 500.milliseconds
data class LoadingSuccessState(
val loading: Boolean = false,
val success: Boolean = false,
val error: Boolean = false,
) {
sealed interface State {
data object Idle: State
data object Loading : State
data object Success : State
data object Error : State
}
val state: State = when {
loading -> State.Loading
success -> State.Success
error -> State.Error
else -> State.Idle
}
val isIdle: Boolean = state is State.Idle
}