allan.conda
11/30/2020, 5:46 PMallan.conda
11/30/2020, 5:46 PMdata class SomeState() {
private val someProperty: Boolean = false,
private val anotherProperty: Boolean = false
}
class MyViewModel(
private val someSuspendedUseCase: SomeSuspendedUseCase
) {
private val _state = MutableStateFlow(SomeState())
init {
viewModelScope.launch {
_state.value = _state.value.copy(
someProperty = someSuspendedUseCase.fetchSomething() // takes 2 seconds and returns true
)
}
}
// called sometime while coroutine above is running
fun someMethod() {
viewModelScope.launch {
_state.value = _state.value.copy(
anotherProperty = true
)
}
}
}
Then the resulting state:
// Actual
State(someProperty = true, anotherProperty = false)
// What I want
State(someProperty = true, anotherProperty = true)
I want to make sure the state being reduced is the latest state. In this case there is
a race condition.
I could make sure to await the use case result before copying and updating the state, but I’m still not completely sure this is safe if the state is being updated from different Coroutines.allan.conda
11/30/2020, 5:46 PMwasyl
11/30/2020, 5:50 PMMutex
and use withLock { }
to disallow concurrent access , or use actor
api to send desired updates (e.g. in form of lambdas) which the actor will execute sequentiallywasyl
11/30/2020, 5:51 PM_state.value = _state.value.copy(
someProperty = someSuspendedUseCase.fetchSomething()
)
you can write
val newValue = someSuspendedUseCase.fetchSomething()
_state.value = _state.value.copy(someProperty = newValue)
where it’s a bit easier to then isolate the non-atomic part, like
val newValue = ...
stateMutex.withLock {
_state.value = _state.value.copy(...)
}
allan.conda
11/30/2020, 6:00 PMallan.conda
11/30/2020, 6:00 PMactor
suggestion would work better hereallan.conda
11/30/2020, 6:01 PMallan.conda
11/30/2020, 6:02 PMwasyl
11/30/2020, 6:02 PMWould putting newValue inside the mutex lock work?yes
Or perhaps yourYes, I think it’s a bit better solution. Requires a bit more boilerplate and I assume involves some overhead (there are some channels involved) but makes it clearer that you only intend to modify the state synchronouslysuggestion would work better hereactor
allan.conda
11/30/2020, 6:03 PMcopy
call inside the mutex lock and everything else is outsidewasyl
11/30/2020, 6:03 PMtriggering an async operation as soon as possible instead of being blocked by another actionYou can do that, enqueue a started
Async
and in the actor, when it’s that action’s turn, await()
the resultallan.conda
11/30/2020, 6:04 PMwasyl
11/30/2020, 6:04 PMDeferred
, I don’t recall the name. The one started with async { }
blockallan.conda
11/30/2020, 6:04 PMallan.conda
11/30/2020, 6:05 PMwasyl
11/30/2020, 6:06 PMOr perhaps I should just put all aI do that sometimes, it’s just easier. But it’s also easier to forget to wrap state change in a mutex.call inside the mutex lock and everything else is outsidecopy
allan.conda
11/30/2020, 6:15 PMwasyl
11/30/2020, 6:24 PMallan.conda
11/30/2020, 6:58 PMI think I do, but I don’t have any automated way of verifying thatI see. Just a wild idea, have you considered making an extension function, like
suspend fun <T> MutableStateFlow<T>.reduce(mutex: Mutex? = null, reducer: (T) -> T) {
value = mutex?.withLock {
reducer(value)
}?: reducer(value)
}
// usage
_state.reduce(mutex) {
it.copy(...)
}
I try hard not to have a state, and whenever I do, to modify it only from the same class (and expose Flows).I only modify it from a single class (ViewModel) and expose a public StateFlow, although it’s pretty much the bread-and-butter for our MVVM so we use State for all Databinding cases
wasyl
11/30/2020, 7:04 PMI only modify it from a single class (ViewModel)Our view models only pass data from use cases, and as such only have a state that’s never mutated based on its previous value. That is, we only ever do
liveData.value = useCaseResult
, or rather useCase.collect { livedata.value = it }
. So we don’t even have this complexity, because collection only happens on the main thread. In the use cases, most of the synchronisation is also automatic because we mostly combine flows. Only when something happens that modifies domain internal state, we need this synchronisation (and then often libraries do this for us).
So in the end we mostly have couple of in-memory storages which should keep and update the state like this. Initially it wasn’t clear how many different patterns we’ll see, but now I’m just considering creating a single base class for that.
Your reducer makes sense though 🙂allan.conda
11/30/2020, 7:26 PMwasyl
11/30/2020, 9:58 PM