DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • Get Ready for Android 15: Must-Have Testing Strategies for Effective Updates
  • How To Create a Homescreen Widget in Android
  • Transitioning From Groovy to Kotlin for Gradle Android Projects
  • Testcontainers With Kotlin and Spring Data R2DBC

Trending

  • Understanding MCP Architecture: LLM + API vs Model Context Protocol
  • A Comprehensive Guide to Prompt Engineering
  • Ingesting Fixed-Width Mainframe Files Into Delta Lake: The Details Nobody Writes Down
  • You Secured the Code. Did You Secure the Model?
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Deployment
  4. From ViewModel To Compose Presenter: The New Form of State Management

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!

By 
Antoni Sanchez user avatar
Antoni Sanchez
·
Oct. 12, 23 · Tutorial
Likes (1)
Comment
Save
Tweet
Share
2.4K Views

Join the DZone community and get the full member experience.

Join For Free

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!

Android (robot) Event Testing Kotlin (programming language)

Published at DZone with permission of Antoni Sanchez. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Get Ready for Android 15: Must-Have Testing Strategies for Effective Updates
  • How To Create a Homescreen Widget in Android
  • Transitioning From Groovy to Kotlin for Gradle Android Projects
  • Testcontainers With Kotlin and Spring Data R2DBC

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook