An approach to a multi module app with Navigation Component and fragment Result API

Juan Mengual
androidxx
5 min readNov 19, 2020

--

I usually work in a very big app which has been some years around. We have refactored some of the features several times and we have done a huge effort in modularisation in the last years, but app navigation is one of the big pending topics (and pain points) we, the devs, face every day. The time to tackle it has finally come to our sprint in form of a navigation redesign and we are gonna use this chance to improve our lives.

If you are of the one ones who want to go to the code first, this is the Git Hub project: https://github.com/juanmeanwhile/navresult

Before getting into detail, here comes the mandatory cool image…

Android devs playing over modules
Some android devs playing over some modules. Photo by Serge Kutuzov on Unsplash

Defining the goal

There are some points that are important for our use case:

  • Use Navigation Component to handle navigation and deeplinks, but isolate the implementation so, if in the future we want to move to something different, it’s not all spread across all the app code.
  • Each feature lives in its own module and the app is the only one knowing about the features. Still, feature A should be able to open feature B but we don’t want dependencies between them.

The approach

Following Navigation Component recommendations, the app will use a single Activity where each screen is a fragment. We’ll have three sections which can be accessed via bottom navigation, and then buttons to open certain sections or go to deeper level screens in each of the features.

When a section wants to navigate to a different screen, will have to notify that to MainActivity, which will use the Navigation Component to perform the change. In this way, we keep Navigation Component in one single module (app module in this case, but could be a separate one).

Abstracting from the Navigation Component

We’ll use a module which will have all the possible destinations of the app defined, plus a few extensions to use Results API to communicate navigation events to our activity.

For destinations, we’ll use the power of sealed classes:

/**
* Represents an app screen which the user can navigate to
*/

sealed class Destination() : Parcelable {
@Parcelize
object Dashboard : Destination()

@Parcelize
object Home: Destination()
@Parcelize
class Notifications(val someData: String) : Destination()
@Parcelize
object NotificationSecondLevel : Destination()
}

To communicate the desire to navigate to a destination we want to use fragment’s Result API. With Result API a fragment associates a result with a request key and the one who wants to get the result registers a listener associated with that same request key. We’ll create some extension functions to add some robustness to this register-listener / set-result logic.

const val REQ_KEY_MAIN_DESTINATION = "mainNavigationResult"

private const val PARAM_DATA = "bundleData"
/**
* Indicates the desire to open a different destination
*/
fun Fragment.navigateToDestination(destination : Destination) {
requireActivity().supportFragmentManager.setFragmentResult(REQ_KEY_MAIN_DESTINATION, bundleOf(PARAM_DATA to destination))
}

/**
* Some sugar to simplify how the fragment listener is set. In this way we use a fixed key for navigation events while the fragment can still use Result API
* to deliver other results which are not related to navigation
*/
fun FragmentManager.setFragmentNavigationListener(lifecycleOwner: LifecycleOwner, listener: (destination : Destination) -> Any) {
setFragmentResultListener(REQ_KEY_MAIN_DESTINATION, lifecycleOwner) { _, bundle ->
val destination = bundle.getParcelable<Destination>(PARAM_DATA)!!
listener.invoke(destination)
}
}

With this extensions we now have a reserved key for navigation events and our code in fragments and activity will be cleaner. Another important point is that Result API uses Bundles to share data but with the extensions we are hiding from them and using Destination class instead, which is nicer to work with.

This is how a fragment will sent a navigation event to the activity:

class DashboardFragment : Fragment() {

override fun onCreateView(inflater: LayoutInflater,container: ViewGroup?,savedInstanceState: Bundle?): View? {

button.setOnClickListener {
navigateToDestination(Destination.Notifications("Whatever data"))
}
}

Now the activity just needs to register its listener using the extension we have previously created. It will receive the navigation events from the fragments and will use Navigation Component to navigate to them.

supportFragmentManager.setFragmentNavigationListener(this) { destination ->
val navController = findNavController(R.id.nav_host_fragment)
when (destination) {
is Destination.Notifications -> {
val args = NotificationsFragment.generateArgs(destination.someData)
navController.navigate(R.id.action_navigation_dashboard_to_navigation_notifications, args)
}
is Destination.Home -> {
navController.navigate(R.id.action_global_navigation_home)
}
is Destination.Dashboard -> {
navController.navigate(R.id.action_global_navigation_dashboard)
}
is Destination.DestinationWithOrigin -> {
navController.navigate(R.id.action_navigation_notifications_to_secondLevelFragment2)
}
}
}

And that’s it, only app module knows that we are using Navigation Component.

A look to the dependencies

Let’s take a look on how the modules are relating between each other.

App knows about everything, which is expected since it has to know the fragments for being able to open them. It also knows about Navigation, since it’s using Destination and the extension to hide dome Result API details (like request key) and Bundles.

Features depend on navigation because they are also using the extension to hide requestKey and Bundles and also need to know about destinations.

Pros & cons

With this approach we have a pretty simple solution which offers a way to share Navigation events between features through the parent Activity. It prevents the leaks by using Result API and adds some sugar so devs only need to interact with Sealed classes and avoid the Bundles.

Not everything is perfect, one of the points I’m less happy with is that having all the Destinations defined in a single module, means that any change to that module (like adding a new destination) will produce the whole app to be recompiled. We could change the sealed class to use normal classes and inheritance instead (so each module defines it’s Destinations), but we would loose the autocomplete of all the possible cases with the when, which is something that I like.

Next steps

This post covers opening feature A from feature B and also handling several levels of depth in features. We have other cases in our app, like some events which happen in MainActivity and must be communicated to a feature fragment. This is already solved in the GitHub repo, maybe a topic for a sequel of this article. I hope you might find it useful, thanks for reaching the end.

--

--