Seth Madison
02/12/2023, 10:46 PMsealed interface State {
data class PersonLoading(val id: String) : State
object PersonProfile(val name: String) : State
}
sealed interface Intent {
data class LoadPerson(val id: String) : Intent
}
sealed interface Msg {
data class PersonLoading(val id: String) : Msg
object PersonLoaded(val name: String) : Msg
}
fun executeIntent(intent: Intent) {
when (intent) {
is Intent.LoadPerson -> {
dispatch(Msg.PersonLoading(intent.id))
val name = someAsyncFunction(intent.id)
// HOW DO I CHECK WHETHER THE PERSON I'M LOADING HAS CHANGED HERE???
dispatch(Msg.PersonLoaded(name))
}
}
}
I’ve seen two approaches to this problem:
(1) After you do anything async you retrieve the current state using getState
and check to make sure that the data hasn’t changed (eg add requestId
to state, or if your operation has a stable id like in the example above, use that).
(2) Create child scopes (or disposables) that are stored in state, and get cancelled on state transitions.
So (1) might look like:
fun executeIntent(intent: Intent, getState: () -> State) {
when (intent) {
is Intent.LoadPerson -> {
dispatch(Msg.PersonLoading(intent.id))
val person = someAsyncFunction(intent.id)
val state = getState()
if (state is PersonLoading && state.id == person.id) {
dispatch(Msg.PersonLoaded(name))
}
}
}
}
And (2) might look like:
sealed interface State {
val scope: CoroutineScope
data class PersonLoading(override val scope: CoroutineScope, val id: String) : State
data class PersonProfile(override val scope: CoroutineScope, val name: String) : State
}
sealed interface Intent {
data class LoadPerson(val id: String) : Intent
}
sealed interface Msg {
data class PersonLoading(val scope: CoroutineScope, val id: String) : Msg
data class PersonLoaded(val scope: CoroutineScope, val name: String) : Msg
}
fun executeIntent(intent: Intent) {
when (intent) {
is Intent.LoadPerson -> {
dispatch(Msg.PersonLoading(scope.createChildScope(), intent.id))
val name = someAsyncFunction(intent.id)
// HOW DO I CHECK WHETHER THE PERSON I'M LOADING HAS CHANGED HERE???
dispatch(Msg.PersonLoaded(scope.createChildScope(), name))
}
}
}
fun CoroutineScope.createChildScope(): CoroutineScope {
val ctx = coroutineContext
return CoroutineScope(ctx + Job(ctx.job))
}
private object ReducerImpl : Reducer<State, Msg> {
override fun State.reduce(msg: Msg): State {
scope.cancel()
when (msg) {
// etc
}
}
}
(1) Feels cumbersome, but more correct.
(2) Feels cleaner, but not well supported by the framework.
Is there preferred approach? Am I thinking about this wrong?
I feel like it would be very nice if the framework provided a way to manage (2) cleanly without sprinkling scopes all of your state, and managing their cancellation in the reducer. Like, what if you were given a stateScope
that was guaranteed to be a child scope, and that you could cancel in the reducer conditionally? Maybe there’s a better way?Arkadii Ivanov
02/13/2023, 8:35 AMSeth Madison
02/13/2023, 8:45 AMdoSomeAsyncWork
if (somethingChanged){
bail
}
Arkadii Ivanov
02/13/2023, 8:48 AMSeth Madison
02/13/2023, 8:55 AMArkadii Ivanov
02/13/2023, 8:56 AMSeth Madison
02/13/2023, 8:57 AMArkadii Ivanov
02/13/2023, 8:58 AM