...

From ViewModel To Compose Presenter – DZone

From ViewModel To Compose Presenter - DZone

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 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 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. 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!

Discover more from WIREDGORILLA

Subscribe now to keep reading and get access to the full archive.

Continue reading