Hello, I am Liang the Android developer, who is mainly in charge of Gunosy Android App development. This article is about how we build the MVI design pattern based on Coroutines Flow and migrate from RxJava to Coroutines for development.
Issues
Currently, We have some issues in our Gunosy Android Project:
The instability of RxJava 1
Highest occurring crash:
MissingBackpressureException.The main feature:
Article TabsandContentswere developed by RxJava.
Poor maintainability
In the regular MVVM view model:
BasePresenter, since the business logic gets more complicated, it could be tough to trace the logic of a specific flow.The coupling and nesting pattern with multiple interfaces implementation.
MVI with Coroutines Flow

What is MVI?
Model View Intent(MVI) is a reactive architecture pattern where the model is updated based on some interactions from user or the system and the view is updated based on states emitted from the model. — Hitesh Das tech.olx.com
Advantage
Unidirectional data flow
- The flow runs in one direction, in order to send the immutable result.
Events observer pattern
- Define all the events with
data classinstead ofinterface, to achieve loose coupling.
- Define all the events with
Lifecycle awareness
- The whole flow is launched within the
LifecycleCoroutineScope, which means it will be shut down automatically when destroyed.
- The whole flow is launched within the
Thread management
- Thread switching could be applied everywhere in the suspend function by
flowOnorwithContext.
- Thread switching could be applied everywhere in the suspend function by
Back-Pressure handling
- The suspend function suspends the emitter and resumes it later when it is ready to accept more elements.
Maintainability
- Collecting and processing business logic directly in the linear flow, makes it easy to edit or stack up the extension functions.
Testability
- With modularization, simply add or remove the modules we want to test and verify the results.
Supportability
- There are many JetPack libraries that provide support for Coroutines, such as Compose, WorkManager, and Room.
Disadvantage
Lots of boilerplate
- Each event needs its own module for passing through the flow.
Non-performance
- It needs to create many objects to identify in the flow, which becomes less efficient than using the functions directly.
Readability
- Because Coroutines is written in the imperative style, If there is a flow containing super complicated logic, it could be confusing to understand.
- We are trying to apply Domain-drive design such as Clean Architecture, making logic operations work separately and reusable.
Used Libraries
Main
org.jetbrains.kotlin:kotlin-reflectorg.jetbrains.kotlinx:kotlinx-coroutines-coreorg.jetbrains.kotlinx:kotlinx-coroutines-androidandroidx.appcompat:appcompatandroidx.lifecycle:lifecycle-runtime-ktx
Testing
junit:junitorg.mockito.kotlin:mockito-kotlinorg.jetbrains.kotlinx:kotlinx-coroutines-testandroidx.lifecycle:lifecycle-runtime-testing
Components
FlowContract
- Defines all the event models with
data class.
- Defines all the event models with
FlowPresenter
- Receives the request events, processes the business logic, and stores states.
FlowView
ActivityorFragmentthat already implementsLifecycleCoroutineScope.With a custom view, we can implement the interface:
CoroutineScopeto define a scope for launching in FlowPresenter.
Workflow
| Step | Component | Action | Thread | Suspendable |
|---|---|---|---|---|
| 1 | View | Click | Main | non-suspend |
| 2 | View | Send Intent | Main | non-suspend |
| 3 | Presenter | Do tasks to execute Action | Background | suspend |
| 4 | Presenter | Update State | Background | suspend |
| 5 | View | Observe State | Main | suspend |
| 6 | View | Update View | Main | non-suspend |
MVI Components
FlowContract
The
Intentis the events from the view side.The
Stateis the definition of states.The
Actionis the parent class of actions, which pass through the flow.The
Eventis the task to execute in FlowPresenter.The
Viewis the task to execute on the view side.The
Stateis the task to update the states.
class FlowContract { sealed class Intent { object Click : Intent() } data class State( val value: Int ) sealed class Action { sealed class Event : Action() { object ExecuteTask : Event() } sealed class View : Action() { object UpdateView : View() } sealed class State : Action() { data class SetValue(val value: Int) : State() } } }
FlowPresenter
The
CoroutineScopeis the main scope launching on Coroutines flow, we simply useLifecycleCoroutineScopehere.The
CoroutineContextis the base context on Coroutines flow for processing the business logic, also emitting and collecting the elements. We can switch to a different context at any moment.
abstract class FlowPresenter constructor( coroutineScope: CoroutineScope, coroutineContext: CoroutineContext = Dispatchers.Default )
- The based flow is built on
MutableSharedFlow.
val stateFlow = MutableSharedFlow<Intent>(extraBufferCapacity = 64) .increaseAction { states.value } .scan(initializeState) { state, action -> action.reduceState(state) } .flowOn(coroutineContext) .stateIn(coroutineScope, SharingStarted.Eagerly, initializeState)
Design pattern
The
statesisStateFlowwhich can extract the value we want, and derive the flow to observe the value from it.increaseActionis used to receive the specificIntentwithflatMapbuffer, process the business logic, and transform it into a new flow for passing the Action.reduceEventis used to receive theAction.Eventto execute the final task in FlowPresenter, such as logging or navigations.reduceStateis used to receive theAction.Stateto update the target state.
val value = states .distinctUntilChangedBy { it.value } .map { it.value } override fun Flow<Intent>.increaseAction(state: () -> State) = merge( filterIsInstance<Intent.Click>() .flatMapConcat { flow { emit(Action.Event.ExecuteTask) emit(Action.View.UpdateView) emit(Action.State.SetValue) } } ) override suspend fun Action.Event.reduceEvent() { when (this) { is Event.ExecuteTask -> {} } } override fun Action.State.reduceState(state: State) = when (this) { is State.SetValue -> state.copy(value = value) }
FlowView
Set up FlowPresenter after initializing the view.
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupView() setupPresenter() }
Set up FlowPresenter
The
intents()is the flow that collects view events and emits them to FlowPreseter, it also launches with lifecycle-aware.To observe the
Action.ViewandState, we launch a new coroutine fromLifecycleCoroutineScopewithrepeatOnLifecycle, so that we can decide which lifecycle state to start collecting the elements.The
valueis the state for observing by the flow.The
viewsis the action for receiving to execute the task on the view side.
private fun setupPresenter() { presenter.apply { intents() .onEach { sendIntent(it) } .launchIn(lifecycleScope) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.CREATED) { value.collect { } views.collect { when (it) { is View.UpdateView -> {} } } } } } }
Bind with UI
The
callbackFlowis the flow that wraps the call-back events and produces the request intents.With
trySendto send the view events immediately to the flow.With
awaitClose, whenLifecycleCoroutineScopeis in the destroyed state, we release the call-back to stop detecting the events.
private fun intents(): Flow<Contract.Intent> = callbackFlow { button.setOnClickListener { trySend(Contract.Intent.Click) } awaitClose { button.setOnClickListener(null) } }
Unit Test
Using
TestScopeinstead ofrunBlockingforCoroutineScope.Using
Mockitofor module mocking.testIntentInvokeEventis theAction.Eventinvoking verification.testIntentInvokeViewis theAction.Viewinvoking verification.testIntentUpdateStateis theAction.Statefor State updating verification.
@Test fun testIntentInvokeEvent() = runTest(coroutineRule.testDispatcher) { testPresenter.sendIntent(Intent.InvokeEvent(1)) Mockito.verify(testEventMock).invoke(1) } @Test fun testIntentInvokeView() = runTest(coroutineRule.testDispatcher) { val results = mutableListOf<Action.View>() val job = launch { testPresenter.views.toList(results) } testPresenter.sendIntent(Intent.InvokeView(1)) Assert.assertEquals(listOf(Action.View.TestView(1)), results) job.cancel() } @Test fun testIntentUpdateState() = runTest(coroutineRule.testDispatcher) { val results = mutableListOf<Int>() val job = launch { testPresenter.value.toList(results) } testPresenter.sendIntent(Intent.UpdateState(1)) Assert.assertEquals(listOf(0, 1), results) job.cancel() }
RxJava to Coroutines
In our Gunosy Android Project, there is a numerous amount of code currently written in RxJava 1, to work smoothly with Coroutines, we use suspendCancellableCoroutine to wrap RxJava into suspend function.
suspend fun <T> Observable<T>.toSuspend(): T = suspendCancellableCoroutine { continuation -> subscribe(object : Subscriber<T>() { override fun onNext(t: T) { continuation.resume(t) } override fun onError(e: Throwable?) { continuation.resumeWithException(e) } } }
Conclusion
Although it's not easy to migrate from RxJava to Coroutines, our code quality and development efficiency has improved. For advanced implementation in the future, we will consider how to modularize and combine multiple scenes with design patterns and architectures.
The goal of software architecture is to minimize the human resources required to build and maintain the required system. — Robert C. Martin.