Model-View-Presenter on Android with Kotlin Coroutines

Kotlin just released a major version that brings coroutines out of experimental status. We’ve been using coroutines for around six months now and have learned quite a bit about how to use coroutines and how not to. The coroutines library is a powerful tool and seems to be on the rise in popularity. There is a lot of great info out there about using coroutines in MVVM projects and in projects that make heavy use of Android Jetpack’s Architecture Components. Instead, we’d like to share how we are using coroutines in a model-view-presenter architected application.

Disclaimer: I’ll assume you’re familiar with coroutines at a high level. If you need an intro, there are plenty of resources online that would serve you better than I would. I’ll link some in the Extras section.

Our Architecture at a Glance

MVP is a tried and true architecutral pattern for Android. Recently we’ve been successfully leveraging new technologies, like coroutines and architecture components, to make MVP nicer than ever. Our current implementation is a stepping stone toward a better future that embraces a reactive paradigm with coroutines as the bedrock.

Current Architecture Overview

Implementation

There are certainly ways we can improve our architecture. We know we have a way to go yet, but we’ve found success on the road to a clean architecture.

Structured Concurrency

A driving principle of coroutines development is that coroutines are like lightweight threads. The early experimental versions of coroutines were a bit of a wild west that encouraged creating new coroutines all the time. A typical presenter might look something like this previous to coroutines version 0.26.

class PlaylistPresenter : PlaylistContract.Presenter {

    private var job: Job? = null

    override fun long fetchSongs() {
        // The default context was 'CommonPool' which 
        // delegated the work to a pool of non-ui threads.
        // A new job is created for this coroutine.
        job = launch {
            val songs = fetchSongsFromNetwork()
            withContext(UI) {
                view?.updateSongList(songs)
            }
        }
    }

    override fun cleanup() {
        // imagine tracking many job objects for multiple
        // coroutines executing simultaneously. _shudder_
        job?.cancel()
    }
}

Unlike threads, which are often created in a global scope, coroutines can be tightly scoped to the entities that own them. Instead of firing coroutines into the ether and hoping everything goes well, the team working on coroutines introduced a better way with kotlinx.coroutines 0.26.

The CoroutineScope interface facilitates constraining a coroutine’s lifetime by an object with lifetime semantics.

class Presenter : PlaylistContract.Presenter, CoroutineScope {

    private val job = Job()
    override val coroutineContext: CoroutineContext = job + Dispatchers.IO
	
    override fun long fetchSongs() {
        // This launch uses the coroutineContext defined
        // by the coroutine presenter.
        launch {
            val songs = fetchSongsFromNetwork()
            withContext(Dispatchers.Main) {
                view?.updateSongList(songs)
            }
        }
    }

    override fun cleanup() {
        // By default, every coroutine initiated in this context
        // will use the job and dispatcher specified by the 
        // coroutineContext.
        // The coroutines are scoped to their execution environment.
        job.cancel()
    }
}

Now the presenter is a CoroutineScope, and coroutines started in this scope can more appropriately be cleaned up or cancelled when the presenter is no longer necessary.

A View-Presenter Contract for Coroutines

Fortunately, generalizing the scope impementation to all presenters in a project is fairly straightforward.

interface BaseContract {
    interface View
    interface Presenter<T: View> {
        @CallSuper
        fun takeView(view: T) {
            this.view = view
        }

        @CallSuper
        fun releaseView() {
            this.view = null
        }

        var view: T? = null
    }
}

interface CoroutineContract {
    interface View : BaseContract.View, CoroutineScope {
        private val job = Job()
        override val coroutineContext: CoroutineContext = job + Dispatchers.Main
    }
    abstract class Presenter<T: View> : BaseContract.Presenter<T>, CoroutineScope {
        private val job = Job()
        override val coroutineContext: CoroutineContext = job + Dispatchers.IO

        override fun releaseView() {
            job.cancel()
            super.releaseView()
        }
    }
}

View methods will be called from the presenter. They should be modified with suspend and, importantly, they are responsible for changing the coroutine execution context back to the main thread via withContext(this.coroutineContext) where appropriate.

Presenter methods return are also suspend methods. Since presenters are coroutine scopes, you can easily use a coroutine builder on the presenter like launch to call the suspend methods on the presenter.

Example MVP Interaction

Testing

Isolating and testing presenters on the JVM is easy if a few rules are followed:

Rules for Presenters
  1. Avoid direct references to the Android framework. If necessary, they reference framework components through the view interface. This is antithetical to the passive view philosophy, but the trade-off is worth sealing the Android framework in a box for testing purposes.
  2. Presenter methods that are called by the view are suspending functions. Those functions should always be called within the presenter’s coroutine scope.
  3. takeView is the cue for the presenter that ui is ready for information. releaseView is the cue for the presenter that the ui is being torn down. Hard dependencies on the MVP view’s lifecycle are discouraged and should use the contract interfaces to communicate if necessary.

Android Test is out now and provides hope for a future where testing on the JVM can involve the Android framework. Fingers crossed.

The Future

We aspire to a more reactive variant of the MVP pattern. The good news is that we have several paths to making our reactive aspirations a reality.

Future Architecture

A possible future using Room for a local data store that serves as an authority on data presented to the user. Maybe Room will support coroutine channels by the time we take this on. Maybe we’ll use LiveData. Who knows?

Dagger 2, Retrofit, Lifecycle, & Coroutines make the foundation of our stack, but we’re always evaluating technologies that can help us write better code faster.

Extras

We like Kotlin a lot. As it currently stands, the app is exclusively written in the language. If you’re an Android developer and find this interesting, we’re hiring!

We have some exciting things planned for the Faithlife App.


Thanks to Logan Ash and Jacob Peterson for reviewing early versions of this post.

Update 01/19/2019

An earlier version of this post suggested that presenter methods should return an instance of Job so that tests can synchronize on the called coroutine in order to prevent race conditions. We ran into some problems with the way exceptions of child coroutines within a runBlocking coroutine builder are handled. runBlocking immediately cancels child coroutines when they thrown an exception. After some consideration, we determined that the presenter should be responsible for exactly how the methods are called, just with the fact that they are suspending.

The structured concurrency updates and the CoroutineScope interface helped us achieve this separation. We can start presenter coroutines in the presenter’s scope from the view and in tests we wrap the entire test in a runBlocking coroutine builder so that suspend methods are executed synchronously.

We’re happier with the new system in it’s flexibility to treat suspending functions differently depending on the context in which they are called.

Posted by Justin Brooks on November 05, 2018