Table of Contents
Commonly, we manage the state logic in an Android ViewModel by applying MVI or MVVM, and we may combine a number of asynchronous data elements to create the state of the view. Some of this data does not change, some are immediately available, and some change over time.
However, it is important to keep in mind that the task of combining reactive flows becomes more complex as the data sources and logic involved increase, making the code more difficult to understand.
Fortunately, thanks to Compose and Molecule, we can build the state object using imperative code and expose it reactively.
What is Molecule?
Molecule is a Kotlin compiler plugin that uses Jetpack Compose to continuously recompose itself and build a StateFlow or Flow. Like Jetpack Compose, Molecule relies on a frame clock, such as the MonotonicFrameClock, to synchronize its recomposition process with the rendering of frames. There are two types of RecompositionClock in Molecule:
RecompositionClock.ContextClock
behaves similarly to Jetpack Compose. It uses the MonotonicFrameClock of the coroutineContext for recomposition. If one is not found, it will throw an exception. It is useful with the AndroidUiDispatcher.Main, which has a built-in MonotonicFrameClock synchronized to the device’s frame rate.RecompositionClock.Immediate
generates a frame whenever the stream is ready to output an item. It can be used when a MonotonicFrameClock is not available, such as in unit tests or to run molecules outside the main thread.
Two functions can also be used to create a flow with molecule: moleculeFlow
or CoroutineScope.launchMolecule
. MoleculeFlow
is used to create a flow with backpressure capability and launchMolecule is used to create a StateFlow.
How do we migrate the ViewModel?
Let’s imagine that we have a screen where we need to show a list of users, in which we have a ViewModel with a flow to receive the events of the user and the flow of the list of users that we obtain from the repository, which we combine to create the stateFlow of the state of the view.
class UserListViewModel(
private val repository: Repository,
....
) : ViewModel() {
//...
private val events = _events
.onStart { emit(RequestUsers()) } // 1
.onEach { runSomeEffects(it, repository) } // 2
.shareIn(viewModelScope, SharingStarted.Eagerly, 1)
val state = repository.getUsersFlow() // 3 -> 6
.runningFold(UserListState.DEFAULT, UserListState::applyResult) // 4 -> 7
.combine(events) { state, event -> event.transformState(state) } // 5 -> 8
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(500), UserListState.DEFAULT)
//...
}
Using Compose we will migrate the above code as follows:
@Composable
fun UserListPresenter(events: Flow<UserListEvent>, repository: Repository): UserListState {
var state by remember { mutableStateOf(UserListState.DEFAULT) } // <-- Set Default as inital state
val userList by repository.getUserListFlow().collectAsState(emptyList()) // <- collect users
state = state.updateState(userList) // <-- updateState with userList
LaunchedEffect(Unit) { repository.requestUsers() } // <- Load users on first composition
LaunchedEffect(events) {
events.collect { event ->
runSomeEffects(event, repository) // <- Run some sideEffects
state = event.updateState(state) // <-- updateState with events
}
}
return state
}
As we can appreciate, it is easy to understand that everything is executed in a linear way, although like everything, it has its adaptation curve. The most positive part is that with everything we learn composing our view with Compose UI, we can apply it to the presenter and vice versa.
Now that we have our presenter ready, how do we use it?
First, we need to add the dependency and apply the apply plugin
: ‘app.cash.molecule
‘ in the modules where we are going to use it.
dependencies {
classpath "app.cash.molecule:molecule-gradle-plugin:$version"
}
We can instantiate the Presenter from some Compose screen, although this way it would not survive configuration changes.
@Composable
fun SomeScreen() {
...
val state by scope.launchMolecule(RecompositionClock.ContextClock) {
UserListPresenter(...)
}.collectAsState()
}
But the goal is to migrate our ViewModels, so let’s see how to do it. First, we will create an extension on ViewModel that will help us create the stateFlow using launchMolecule in a lazy way.
/**
* Creates a lazy StateFlow using [launchMolecule] and [RecompositionClock.ContextClock]
*/
inline fun <T> ViewModel.moleculeStateFlow(
clockContext: CoroutineContext = AndroidUiDispatcher.Main,
clock: RecompositionClock = RecompositionClock.ContextClock,
safetyMode: LazyThreadSafetyMode = LazyThreadSafetyMode.NONE,
crossinline presenter: @Composable () -> T
): Lazy<StateFlow<T>> = lazy(safetyMode) {
val scope = CoroutineScope(viewModelScope.coroutineContext + clockContext)
scope.launchMolecule(clock) { presenter() }
}
Next, we will pass as a parameter the AndroidUICoroutineContext
because the context of the ViewModel does not have MonotonicFrameClock by default and the ContexClock
as RecompositionClock. And our viewmodel would look like this:
class UserListViewModel(
private val repository: Repository,
context: CoroutineContext = AndroidUiDispatcher.Main,
clock: RecompositionClock = RecompositionClock.ContextClock
....
) : ViewModel() {
private val _events = MutableSharedFlow<UserListEvent>()
val state: StateFlow<UserListState> by moleculeStateFlow(context, clock) {
UserListPresenter(_events, repository)
}
fun emit(event: UserListEvent) = _events.tryEmit(event)
Last but not least, how do we test it?
To do unit tests we must enable returnDefaultValues
and add the Turbine dependency, a small test library for Flows.
android {
...
testOptions {
unitTests.returnDefaultValues = true
}
...
}
dependencies {
testImplementation "app.cash.turbine:turbine:$version"
}
In our test, we can choose to test our ViewModel as we have so far
class UnitTest {
...
private val viewModel = UserListPresenter(
repositoryMock,
UnconfinedTestDispatcher(),
RecompositionClock.Immediate
)
@Test
fun `some test`() = runTest {
viewModel.state.test {
val state = awaitItem()
assertEquals(State.INITIAL, state)
}
}
}
or we can also test our presenter function by creating a moleculeFlow
passing a RecompositionClock
and executing the Turbine test
function. As it is something that we will repeat in each test, we will create the following extension and use it in the test.
/**
* creates a moleculeFlow with [RecompositionClock.Immediate] recomposition clock
* and the turbine validate function
*/
suspend fun <T> (@Composable () -> T).test(
timeout: Duration? = null,
name: String? = null,
validate: suspend ReceiveTurbine<T>.() -> Unit
) = moleculeFlow(RecompositionClock.Immediate, this).test(timeout, name, validate)
class UnitTest {
...
private val presenter: @Composable () -> State
get() = { UserListPresenter(events, repositoryMock) }
@Test
fun `some test`() = runTest {
presenter.test {
val state = awaitItem()
assertEquals(State.INITIAL, state)
}
}
}
Conclusion
Compose Presenter gives us an alternative to handle the state in a more understandable and efficient way. It allows us to escape from the overload of stream operators, writing imperative code. We can use it in Android projects as well as in KMM. It is worth noting that to apply the Presenter pattern in our project it is not necessary to use the Molecule library, but it is convenient for this use case. Happy Coding!
Author
-
Android Passionate Developer that enjoy solving issues. Focused on developing quality and scalable software.
View all posts
More to Explore