From ViewModel To Compose Presenter: The New Form of State Management
Thanks to Compose and Molecule, we can build the state object using imperative code and expose it reactively. Read more!
Join the DZone community and get the full member experience.
Join For FreeCommonly, 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 is immediately available, and some changes 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 theMonotonicFrameClock
of thecoroutineContext
for recomposition. If one is not found, it will throw an exception. It is useful with theAndroidUiDispatcher.Main
, which has a built-inMonotonicFrameClock
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 aMonotonicFrameClock
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. 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 handling 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!
Published at DZone with permission of Antoni Sanchez. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments