Hi there. Need an advice. Inside a `ViewModel`, I ...
# coroutines
u
Hi there. Need an advice. Inside a
ViewModel
, I had a basic MVI-inspired state machine implemented in the following way:
Copy code
/**
 * State machine for this view model consisting of [Interactor], [State], [Event] and [Reducer]
 * It reduces [Event] to the immutable [State] by applying [Reducer] fuction.
 * This [State] then will be rendered.
 */
class Interactor(
        private val scope: CoroutineScope,
        private val reducer: Reducer = Reducer(),
        private val channel: Channel<Event> = Channel(),
        private val events: Flow<Event> = channel.consumeAsFlow()
    ) {
        fun onEvent(event: Event) = scope.launch { channel.send(event) }
        fun state(): Flow<State> = events.scan(State.init(), reducer.function)
    }

interface StateReducer<STATE, EVENT> {
    val function: suspend (STATE, EVENT) -> STATE
    suspend fun reduce(state: STATE, event: EVENT): STATE
}
But then I was unable to get current state, I could only observe it as a
Flow<State>
, so I re-implemented it in the following manner:
Copy code
class Interactor(
        private val scope: CoroutineScope,
        private val reducer: Reducer = Reducer(),
        private val channel: Channel<Event> = Channel(),
        private val events: Flow<Event> = channel.consumeAsFlow()
    ) {
        
       val state = ConflatedBroadcastChannel<State>()

        init {
            scope.launch {
                events.scan(State.init(), reducer.function).collect { state.send(it) }
            }
        }

        fun onEvent(event: Event) = scope.launch { channel.send(event) }
        fun state(): Flow<State> = state.asFlow()
    }
I guess it could be improved. Are there any inherent problems to this implementation? Thanks a lot, in advance đź’Ą.
e
I got the chance to work on something very similar for a talk, based off of an RxJava equivalent I’ve used in production in a few projects.
I think yours is looking pretty good.
My personal realization was that there is a “sister pattern” that I’ve had good success with, re: how to build
STATE
types
enforcing state transitions this way is how I feel can get away with a suspend function on input, instead of a 2nd scope launch.
or it might be better put that I move the burden of doing potentially async work outside of the Store, which is my equivalent to your
Interactor
class.
Btw, thanks for sharing, it’s nice to be able to compare with other folks approaches.
đź‘Ť 1
a
One concern that I have with the code, is having
StateReducer
taking a
suspend
lambda, as that would introduce the possibility of performing side effects inside it (e.g Network Requests), which IMO invalidates the purpose of reducers, that are known to be pure functions that takes an Input and gives an Output.
e
(Also, it’s worth keeping an eye out on https://github.com/Kotlin/kotlinx.coroutines/pull/1354 )
the above is a good point
I’ve avoided that type of problem by reflecting “in flight jobs” in my models, in most of my work.
i.e. “EDITING -> SAVING -> CLOSED”, where I transition my state machines to saving when the save request is in flight, for example
my reducers are intended to be non-blocking
(Thinking out loud here, since I’m always on the lookout for things I’ve overlooked, or ways to improve)
a
@Etienne BTW I've watched your talk, and loved it, it was very useful to me, thank you! I even adapted your approach in my MVI implementation of the ModelStore. https://github.com/R4md4c/GameDealz/blob/master/common/src/main/java/de/r4md4c/gamedealz/common/mvi/FlowModelStore.kt
e
oh awesome 🙂 happy it was helpful
I’ll take a look
u
I was just thinking about init block in the second version, any ideas how to improve it? @Etienne, @Ahmed Ibrahim
e
Sorry for the slow turnaround here, I’m pretty swamped lately
I don’t really understand the use of
channel
in your 2nd sample
I think I get the idea, but I think something is off, or there’s something I’m missing
u
@Etienne, thanks for coming back :) I need a way to retrieve a current state without subscribing to reducer. In the first example, there seems to be no way to do that.