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