Coordinators on Android: how to build flows quickly with reusable screens

New technical post! Android engineer @Tas shares a new architectural pattern we use in our codebase called ‘the Coordinator Pattern’ :point_down:

5 Likes

@Tas, thanks for the article, the Coordinator pattern looks very useful.

It seems to me, that FlowNavigator could be replaced by NavController in the apps using Navigation Component library. Have you tried that approach or did you at least consider it? I’m interested with your thoughts about it.

Hi, welcome to the community!

The thing I do not pick up from the otherwise very detailed article was to what degree the existing code for the Android application has been refactored to pick up the new patterns

You are absolutely right, the NavController and the FlowNavigator are quite similar - and could easily be swapped. That’s one of the main benefits this patterns provided us with - a clear separation of concerns, where we can easily make changes like this.
Now as for why we chose to create our own interface, this mostly has to do with simplicity. Our navigation needs are pretty straightforward, and the FlowNavigator interface reflects that (it only has 5 methods). At this point in time, we don’t need anything more complicated, and more ways where navigation can go wrong. Having said that, maybe in the future we might change our minds…

2 Likes

I conveniently omitted that :wink: The truth is, just like any other fundamental architectural change, it’s pretty hard to move your codebase to the new paradigm. We’ve started this work about a year ago, and I think we’ve converted roughly 60% of our codebase. The initial setup was hard, but the good thing is that it’s very easy to break down, and it gets a lot easier over time. The process we followed goes something like this:

  • Identify a flow that we want to convert to use Coordinators. Ideally, it needs to be a flow that has a lot of screens that can be reused later.
  • Write the Coordinator skeleton (without any screens backing it up, just the navigation logic) in the first PR.
  • Raise one PR with the Fragment and ViewModel for each of the screens in that flow.
  • On the final PR, write the CoordinatorHost (Activity) code that hooks everything together, and update all the entry points to this flow, to point to the new Activity.

When we select another flow to convert to use Coordinators, we might already have a Fragment and a ViewModel that we’ve already converted, so we can just reuse it. Of course, when we reuse one of these into a new flow, we might need to make minor adjustments (like make the title configurable, or a button optional etc).

2 Likes

Thanks for that detailed response, and for satisfying my curiosity

It looks promising. Do you have rest of the source code published somewhere? It would be great to see how BaseViewModel, FeatureNavigator and other classes are implemented and how everything is glued together

Although we considered open sourcing parts of this, when we actually sat down and tried to extract the truly ‘reusable’ elements, we realised that there’s was very little. I’ve already pasted the CoordinatorHost and the Coordinator interfaces in the blog post. The FlowNavigator is extremely similar to the Cicerone implementation I mention.

The FeatureNavigator is really app specific, there’s nothing to open source. The interface looks something like this:

interface Navigator {
    fun goToAddMoney(accountId: String)
    fun goToTransactionDetails(transactionId: String)
    // more goTo functions
}

The implementation simply takes the parameter from the method, and creates an intent with he parameter and the correct Activity class.

As for the BaseViewModel, it doesn’t have that much code in it. Here’s most of it:

abstract class BaseViewModel<S : Any, E : Any>(initialState: S) : ViewModel() {

    private val _coordinatorEvents = MutableLiveData<SingleEvent<Any>>()
    private val _viewEvents = MutableLiveData<SingleEvent<E>>()
    private val _state: MutableLiveData<S> = MutableLiveData(initialState)

    /**
     * State changes. To be consumed by the View.
     */
    val state: LiveData<S> = _state

    /**
     * View events. To be consumed by the View (only consumed once).
     */
    val viewEvents: LiveData<SingleEvent<E>> = _viewEvents

    /**
     * Coordinator events. To be consumed by the Coordinator (only consumed once).
     */
    val coordinatorEvents: LiveData<SingleEvent<Any>> = _coordinatorEvents

    /**
     * Modify the state of this [ViewModel].
     */
    @MainThread protected fun setState(reducer: S.() -> S) {
        val currentState = _state.requireValue()
        val newState = currentState.reducer()
        if (newState != currentState) {
            _state.value = newState
        }
    }

    /**
     * Sends an event to [viewEvents] to only be consumed once.
     */
    @MainThread protected fun sendViewEvent(event: E) {
        _viewEvents.value = SingleEvent(event)
    }

    /**
     * Sends an event to [coordinatorEvents] to only be consumed once.
     */
    @MainThread protected fun sendCoordinatorEvent(event: Any) {
        _coordinatorEvents.value = SingleEvent(event)
    }

}
3 Likes

This article is really interesting!! Thank you, I have some questions though. The FlowNavigator in the diagram is specific to a group of flows (so do you create a UpdateAddressFlowNavigator which implements FlowNavigator) or it is a single class shared between coordinators (which wouldn’t explain why it gets created in the coordinators).
The second question regards the viewmodels; viewmodels here extends BaseViewModel which extends the android component viewmodel; you create them into the Coordinators but they get called in Fragments as well, do you then injected them into Fragments or we are talking about two different viewmodels?

Ok, so the FlowNavigator<T> is a generic interface, with a few simple commands (replace(screen: T), navigateTo(screen: T), close() etc). It is implemented by the FragmentFlowNavigator<T> (because we use fragments for screens). On its constructor, this takes a parameter like this fragmentFactory: (T) -> Fragment which takes a Screen object and turns it into a factory. This is what’s specific to the flow. So in your CoordinatorHost (Activity, or top-level Fragment) you will have something like this:

fun flowNavigator() = FragmentFlowNavigator<UpdateAddressScreen>(
        supportFragmentManager,
        lifecycle
) { screen ->
        when (screen) {
            // ... provide a fragment for the Screen
        }
    }

This is then provided to Dagger, to be injected in the Coordinator when it is constructed.

About the ViewModels, it’s exactly as you mentioned - they are created in the Coordinator, but the Fragments get a reference to them. How does this work? I mention this in ‘The Fragment’ section of the main document - we have a Kotlin property delegate that does this (how it does it is also described there).

1 Like

Few years after the release of this article, do you still think that with Compose and NavHost it is relevant?

This is a very interesting question, and quite relevant to what we’re currently doing in the Monzo codebase. There are two parts to the answer.

The first one has to do with the use case we’re facing right now. We are currently migrating all our view code to Compose. As you can imagine, our codebase is quite large (600 screens and counting). So we started this 2 years ago, and expect it to take 2 more. Thanks to the coordinator pattern, and the way we’ve separated screens into fragments, this process is actually quite easy. Everything related to the coordination of screens stays the same, and we migrate individual screens one by one to use Compose. Each migrated screen is actually a ‘shell’ Fragment which contains a ComposeView at the top, and then we dive straight into Compose code.

Long term though, I expect us to fully migrate, and only have Compose screens. This is btw also relevant for new projects starting with Compose. In that case I would argue Coordinators are still relevant. Keep in mind that what’s described above is nothing more than an abstraction over fragment transactions. But a very useful abstraction that allows you to focus on the actual navigation logic, and not all the different parameters/flags/lifecycle issues that fragments come with. NavHost is definitely an improvement over that, but it still has to cater for a lot more use cases than your particular application, and is also tied to the android framework (so you can’t unit test your navigation logic). So what will we do practically? Probably keep the Coordinator abstraction, but change the implementation from underneath to not use fragments, but only switch between Composables.

2 Likes

I’m exploring this pattern and I’ve made something workable by cobbling together things from this article and others based off of this article. I hit some bumps and so I have a couple of questions:

  • How does the Coordinator provide the BaseViewModel to the fragments? Fragments know what ViewModel class they want but the api in Coordinator onCreateViewModel(screen: S): BaseViewModel can provide a ViewModel based on the sealed class Screen. Do fragments hold the parcelable screen as part of their arguments bundle and that gets consumed by the hostedViewModel? Or is there some other mechanism to tie a Fragment to a Screen?

  • Does the subscription to the ViewModel’s coordinatorEvents happen in the Coordinator when a ViewModel is constructed or in the hostedViewModel code, or somewhere else entirely?