Decoupling Android Navigation Logic with VIVA Pattern

Orit Malki
Melio’s R&D blog

--

At Melio, we are building a mobile SDK. Among its core functionalities, our SDK provides different screen flows that can be assembled as needed, no matter who is using them. They can be used as building blocks for constructing Melio’s own mobile app, or any other app that needs to include Melio functionalities.

As an Android Tech Lead, one of my first tasks was architecting Melio’s Android SDK and mobile app, in accordance with our Head of Mobile development team.

One of the main challenges we faced when building our mobile SDK, was designing a navigation pattern that would support the modularity we were striving for — making our screen components agnostic to the navigation logic between them.
This required decoupling UI components from navigation business logic. This is not a trivial task in Android, as navigating from one screen to another is mechanically done by UI components themselves (Activity/Fragment).

Who are we building for?

It was important to us to lay the groundwork for other partners and developers to use what we’re building.
Let’s take for example, an e-commerce app that will incorporate Melio’s checkout ability. This app plans to use Melio’s mobile SDK to enable businesses to pay in their preferred payment method, at which point a quick authentication may be desired that our mobile SDK should seamlessly provide.
The authentication flow will then require and include, UI, logic, and of course — internal navigation between the different screens.

Pay checkout leading to authentication flow

The SDK should also be able to expose a single screen out of a flow.
For example, using only a single screen out of an onboarding flow to update a company’s billing address. In that case, this screen-unit should be agnostic to the navigation logic to/from it.

In order to achieve this, we needed to be able to dictate navigation logic from outside our screen-units.
But how could we approach this challenge?

Examining existing patterns

After reviewing different architectural design patterns to use for our screen-units, VIPER seemed like an interesting fit, as it has a good separation of navigation logic and mechanics.

In VIPER, each screen unit consists of a View, Interactor, Presenter, Entity and Router. The router’s job is the mechanics of navigation, whereas the presenter’s job is to hold the logic for a navigation action.

Although it seemed enticing, in terms of separation of concerns, this pattern presented a dependency issue for us — each VIPER screen-unit is still aware of its navigation logic. Therefore, it couldn’t provide a complete solution for exposing an independent screen out of a flow:

We can see here that each screen/VIPER unit is coupled with navigation logic from it.

It also presented other disadvantages for us:

1. As an MVP-based pattern, the state of the presenter is not preserved through configuration changes, like in Android’s ViewModel class.

2. The presenter controls the view directly. If we considered using Jetpack Compose on some of our screens, it would be more suitable to maintain a state in ViewModel that lets the passive view merely observe and react.

So while we wanted to make our project Compose-ready, and not worry about configuration change state-loss, we found ourselves drawn to the MVVM pattern.

Ok, but what does it have to do with navigation?
While examining the MVVM pattern, we realized something. If the view can be decoupled from the business logic by being totally passive, only reacting to state changes triggered by LiveData / StateFlow Then why not use that same technique to decouple navigation logic as well?

So we decided to build a coroutine-SharedFlow based mechanism for posting navigation events.

Our solution for decoupling navigation from screen-units

With this goal in mind, we realized that we needed to differentiate between two key types of navigation events:

Flow-level navigation (navigation from fragment)

Melio’s mobile SDK offers a set of predefined screen-flows, each of which consist of a single activity that navigates different fragments using the Jetpack Navigation graph.

Inspired by VIPER and MVVM, I came up with a pattern combining the benefits of both patterns, named VIVA.
In VIVA, each screen-unit consists of the following components:
View (Fragment), Interactor (acts as a data provider), ViewModel, and a new addition of an Actions sealed class.
It also has its own dependency injection component, creating a totally decoupled unit.

A contract and a generic BaseVIVAFragment class are used to bind those screen-unit-elements together, as such:

abstract class BaseVIVAFragment<V : Contract.View, 
VM : Contract.ViewModel>
: Fragment(), Contract.View

Here is an example of a contract for a simple SignUp screen-unit:

Let’s dive into the navigation aspects of the pattern…

Note: the Action sealed class specifies events that require navigation to another screen. This class corresponds to the NavigationEvent interface, which will allow us to use this Action as a payload for posting a navigation event.

The CoordinatorEvent is the actual event we post when a screen needs to navigate.
In the NavigateFromFragment data class, we pass the relevant Action (that implements NavigationEvent interface), containing all relevant information for navigation:

For the event-firing mechanism, we use a CoordinatorEventBus object that holds a SharedFlow of CoordinatorEvents. It can be used from anywhere in the code.
It has a send function, to be used by the producer screen-unit:

fun send(event: CoordinatorEvent)

and a consume function, with a consume block to be executed when the flow is collected, to be used by the consumer coordinator/activity:

fun consumeEvents(consume: (T) -> Unit)

So, when the fragment requires navigation, it’s ViewModel simply fires a navigation event, like so:

CoordinatorEventBus.send(CoordinatorEvent.NavigateFromFragment
(
action =
SignUpFragmentContract.Action.CreateAccountCompleted(user)
)
)

The fragment is not aware of any navigation logic or destinations, it simply signals that it needs to navigate.

The consumer activity consumes this event from a lifecycle-scoped coroutine, which looks like this (simplified):

See how we can clearly view where each navigation event came from, and can easily follow the navigation logic for each screen unit!

If we need to take a certain screen out of the flow, and use it independently in a different context, it should be simple since it has no dependencies outside of its own self-provided DI component.

For Application-level navigation, we use a Coordinator class that triggers and coordinates the different screen-flows exposed by the SDK. When consuming events in the App-level Coordinator, it works similarly to the above example, only the events being consumed are of type NavigateFromActivity.

App-level navigation (navigation from activity)

The pattern described in this section is used for reacting to navigation events. To initiate navigation inside activities, we use a state-driven variation of this pattern (currently out of the scope of this article).

With the App-level pattern, the Coordinator works the logic and mechanics of the navigation, and the flow-activities merely post an “I’m done!” event.

Inside the NavigateFromActivity event, you can see that the activity is being passed as a property. That is in order for the Coordinator to have control over the manner and timing of starting a new activity and finishing the previous one. This is very useful for smooth transitions and animations.

Here we can see an overview of our navigation pattern:

Final thoughts

Like every pattern, VIVA has its pros and cons. It suited our needs well when building an SDK with strong decoupling in mind, but it may not fit every project.

Pros:

  • Screen-flows are agnostic to navigation logic
  • Screen-units are independent and can be used as stand-alones
  • Project is Compose-Ready
  • State is preserved through configuration changes
  • Clear separation of concerns with contract-binded units

Cons:

  • Many classes are needed for each screen-unit and flow-unit (see tip below)
  • It may be overkill for a smaller non-SDK project — but it’s definitely worth considering adopting the event-bus pattern part for decoupling the navigation

Tip: to generate a starter code for each screen unit, a cool tool that I found is Hygen — it allows you to create templates and auto-generate them. So in a matter of seconds, you can start working on a new screen with the VIVA pattern. The templates can be included in .git, so other team members can adapt the pattern consistently and easily.

I hope that next time you need to build a navigation pattern, you’ll consider VIVA for decoupling and clear visual layout of navigation logic.

If you have any questions or comments please feel free to add below.

Visit our career website

--

--