From ViewModel to Compose Presenter: The New Form of State Management

Share This Post

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)
CTA Software

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

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Subscribe To Our Newsletter

Get updates from our latest tech findings

Have a challenging project?

We Can Work On It Together

apiumhub software development projects barcelona
Secured By miniOrange