Coordinator Pattern Using Kotlin Coroutines in Android

Android Code Computer Skills Mobile Development

Have you heard about Kotlin coroutines before? Do you feel curious about this? Do you want to work with the Kotlin coroutines within your Android project? In present times, such rapid digital growth is commencing at a faster pace where the software development company tends to build Android SDKs for the organizations.

The developers have encountered some of the interesting situations which required the UI Flow to become dynamic and make every interaction with the server by returning a response by containing the next screen where the SDK is required to get displayed.

Android developers know how such a problem can easily result in bad code if it is not designed well. So, we are turning towards an approach which can respond dynamically to API responses. This is done by inspiring the most recognized Hannes Dorfman’s coordinator pattern.

Up to some extent, the SDKs are developed using the Model-View-Intent (MVI) framework in-house. Coroutines framework is designed using redux principles and supporting structured concurrency.

In this article, we will be focusing on our approach to solve some of the dynamic navigation using Kotlin coroutines.

What is a Coroutine-based solution?

Coordinators are becoming responsible for containing some of the business logic in order to navigate between the views. A coordinator uses a coroutine actor which processes asynchronous intentions sequentially which are scoped to a flow’s lifecycle.

Components of Coroutines

The coroutines consist of various components. You can see it in the figure below. Let us discuss them in brief.

Components of Coroutines

It comprises of basic three functional units be it application, home screen and sign in activity context. The entire coroutine is based on the functionality of these endpoints.

At the very beginning comes the Root Coordinator. It is scoped to the application’s or SDK’s coroutine context. It is responsible for invoking a specific flow coordinator which is based on the business rule via Root Navigator. With this, Flow Coordinator is scoped to the activity view model and it is shared with all the other child view models.

Generally, the flow coordinators hold the responsibility for navigating between the views within the flow. Also, Flow Navigator is a kind of stateless component which only owns the logic for adding views or fragments to the flow’s activity by getting invoked through the flow coordinator.

What is a Coordinator?

The coordinator pattern is the most common pattern for mobile app development which avoids the so-called Massive ViewControllers with way too many responsibilities. It is a type of in-app navigation flow logic and reuses ViewControllers because they are not coupled into in-app navigation.

The coordinators are meant to put inside the in-app navigation logic because the business logic cannot be a good idea. To play ping pong between ViewModel and View might work but does not seem to be an elegant solution. Doing navigation in the ViewModel is seeming to be a considerable alternative but it is really the sole responsibility of the ViewModel to take care of the navigation.

The below-given code simply represents the coordinator which is used to SignIn activity flow by that is started by Root coordinator. We will be discussing the coordinators to follow some simple design principles.

Constructor

A scope is tied to the view model of the main activity which is shared across child fragment view models. The Root Coordinator provides onFlowComplete lambda that should be invoked when a flow is finished. In this way, Root Coordinator better controls the entry and exit of the flow.

class SignInFlowCoordinator(
  private val flowNavigator: SignInFlowNavigator,
  private val scope: CoroutineScope,
  private val onFlowComplete: suspend () -> Unit
) : CoroutineScope by scope

Intentions

Intentions are yet another component which we use to coordinate what our coordinator consumes in order to compute the results. Some user action or some kind of async operations similar to an API response can create Intensions.

sealed class Intention {
object ShowWelcomeScreen : Intention()
object ShowErrorScreen : Intention()
data class OnChallenge(val challenge: Challenge) : Intention()
data class onFinish(val result: SignInResult) : Intention()
}

Actor

Coordinators contain an actor who will initialize lazily and with an unlimited channel capacity. The capacity we selected is based on our use case because we did not want to drop any intentions.

Actor

An actor may process intentions and can also compute new states via the copy extension function to make sure that the concurrent state mutation is not happening. In an ideal state, each intention will map to the navigation action which is provided by SignInFlowNavigator. The coordinator also exposes a function which adds an intention to the actor’s queue.

fun send(intention: Intention) {
actor.offer(intention)
}

Navigator 

Let us also look at the navigator which we use for Sign In activity flow. Sign In Coordinator invokes Sign In activity flow. Navigators are stateless and hence have a similar scope to that of SignInFlowCoordinator because they do not have their own business logic.

Navigator

Alike coordinator, it will process its intentions via an actor where you can notice that as we pass in an activityFetcher that will expose CompletableDeferred<AppCompatActivity> which gets suspended by the navigator as the activity is not available or is not in a valid lifecycle state.

Summing up!

The approach of coroutine which we have discussed in the post can just be one possible implementation of the Coordinator concept. This approach helps by making view models and activity or fragments lighter by removing the code duplication and abstracting the navigation logic. 

A coordinator just knows which screen to move next and hence it seems to be very common to make use of it in order to create ViewControllers or service locator and back stack management. We also have Dagger for dependency injection or make use of Activity or Fragment back stack to instantiate Activities on the Android operating system. Hence you can hard code all in your app navigation flows by using readable Kotlin DSL’s. Keep Learning!