AndroidX Navigation: Building on the Wrong Abstraction

Isaac Udy
ProAndroidDev

I recently read an article by Jesse Wilson titled “Building on the Wrong Abstraction”. If you haven’t read the article already, I would highly recommend it.

Reading Jesse’s article started me thinking about one of my favourite problems in the Android world: Navigation. Navigation is a key part of any application that has more than two screens. Which is probably most of them. But navigation on Android sucks.

To navigate from Screen A to Screen B, Screen A needs to know Screen B’s arguments, the exact names of these arguments, and whether Screen B is an Activity or a Fragment. This results in a situation where Screen A needs to know a lot about the implementation details of Screen B.

Luckily, the great people working on the AndroidX libraries also recognised this was a problem. Unfortunately, they built their solution on the wrong abstraction.

Understanding AndroidX Navigation

In its own words, AndroidX Navigation is “a framework for navigating between ‘destinations’ within an Android application that provides a consistent API whether destinations are implemented as Fragments, Activities, or other components.

Alongside AndroidX Navigation, there’s a Gradle plugin called Safe Args, which describes itself as “a Gradle plugin that provides type safety when navigating and passing data between destinations.

Putting these two together, we can avoid many of the problems present in standard Android Fragment/Activity navigation. Assuming you already have AndroidX Navigation set up in your project, here’s what the workflow looks like:

Define a Destination in the Navigation Graph:

Define an Action for that Destination:

Use the generated Directions object to perform Navigation:

This is much better than regular Activity/Fragment navigation! Directions are type-safe, and the screen performing navigation doesn’t care about the implementation of the other screen; a Fragment Destination could be swapped for an Activity Destination and the screen referencing the Directions for the Destination wouldn’t care.

What’s wrong with AndroidX Navigation?

AndroidX Navigation is better than regular Activity/Fragment navigation, so what’s the problem? There are a few problems that I have with AndroidX Navigation. It requires that you define arguments in XML to generate code that contains the same information you’ve already defined in XML. It uses Navigation Graphs as a centralised place for definitions and ties the Navigation Graphs to Activities rather than Applications. You can define arguments on both Actions and Destinations, which is confusing. These aren’t problems with abstraction though, they’re just design decisions I disagree with. The key abstraction problem is the relationship between Destinations and Actions.

AndroidX Navigation has the following relationships between components:

  1. Navigation Graph
    A container for defining Destinations and Actions
  2. Destination
    Represents a screen that can be navigated to, and the arguments for that screen
  3. Action/Direction
    Represents the intent to open a Destination (and sometimes the arguments for that Destination)

When thinking in terms of these relationships, AndroidX Navigation wants you to start with a Destination (Activity/Fragment/Other) and then create an Action that opens the Destination. This means that a screen wanting to navigate using AndroidX Navigation must have a mutual reference with the screen that it is navigating to. For Screen A to navigate to Screen B, Screen A must know about an Action that points towards a Destination for Screen B. This reference is hidden in the XML, but there’s a direct dependency between Screen A and Screen B.

The dependency flows like this:
Screen A → Action B → Destination B → Screen B

This chain of dependency is the key problem with AndroidX Navigation because it means that navigation between screens in a multi-module project becomes difficult. Screen A and Screen B can’t be in independent modules, because they need a reference to each other through the Navigation Graph.

There are ways to make AndroidX Navigation work in a multi-module project, but none of these solutions is ideal. They all introduce problems and restrictions of their own.

Here are a few approaches for using AndroidX Navigation in a multi-module project:

  1. Define the Navigation Graph in a common module
    It is possible to define a Navigation Graph in a common module and use fully qualified class names for Fragments/Activities in other modules. This will generate the Safe Args Directions that can be used for navigation. Unfortunately, this also means your Navigation Graph will be filled with unresolved class references. These unresolved class references may be missed by refactoring operations, and are hard to maintain. This might work, but the IDE shows these as errors!
  2. Define Destination IDs in a common module
    If you define all resource IDs that will be used as Destination IDs in a common module, and the Destinations in feature modules use these common resource IDs, other feature modules will be able to create Actions that reference these Destinations via the shared resource IDs. However, if you do this, you’ll have to re-define a Destination’s arguments for each Action, creating a huge amount of duplication and a fragile system where changes to the arguments for a Destination will need to be manually propagated throughout the codebase.
  3. Use deep links
    One of the best pieces of AndroidX Navigation is its support for deep links. Instead of trying to use Safe Args, you could just use deep link URIs to navigate around your Application. If you do this, however, you lose type safety, and again put yourself in a situation where it’s hard to make changes to the arguments for a particular Destination should you need to.

What’s the alternative?

Instead of having Actions depend on Destinations, we can flip this relationship around, here’s how that could look using the AndroidX Navigation components:

  1. Action/Direction
    Represents the intent to open a screen, and the arguments for the screen
  2. Destination
    Represents the binding between a specific screen and an Action/Direction
  3. Navigation Graph
    A container to hold the bindings between Destinations and Actions/Directions

When thinking in terms of these relationships, we start with an Action and then create a Destination that fulfils the Action. In the case of Screen A wanting to open Screen B, Screen A only knows about the Action for Screen B, but the Action doesn’t have a reference to the Destination, breaking the chain of dependency. This means that the Actions for a project can be defined in a common or core module, which can be used by independent modules that define Destinations and are tied together by an Application module.

The dependency flows like this:
Screen A → Action B Destination B → Screen B

This is the thinking that I’ve used when developing Enro, an alternative to AndroidX Navigation.

How does this work in Enro? Instead of starting with a Destination, we start by writing the equivalent of an Action or Direction, which Enro calls a NavigationKey. Assuming you’ve got Enro set up in your project, here’s what the workflow looks like:

Define a NavigationKey:

Define a NavigationDestination for that NavigationKey:

Use the NavigationKey to perform navigation:

Using a different abstraction has allowed Enro to see some big advantages over using AndroidX Navigation. Here’s a few of them:

  1. There’s less code to write
    Binding MyFragment as a Destination takes one line of code (the annotation), as compared to 8 lines of XML in the AndroidX example.
  2. Relevant code stays nearby
    The arguments for MyFragment (represented by MyDestinationKey) are right there above MyFragment’s definition in the annotation, not hidden away in XML.
  3. IDE tools work out of the box
    For example, if you wanted to find out where MyFragment is opened from, you can right-click on the reference to MyDestinationKey, which is right there in the annotation at the top of MyFragment’s definition, and use the IDE’s “Find usages” tool.
  4. Multi-module is a breeze
    In a single-module project, you might be defining NavigationKeys right next to the Fragments or Activities that use them, but in a multi-module project you can simply move these into a common module and everything continues to work as expected.

Conclusion

AndroidX Navigation is built on the wrong abstraction. AndroidX Navigation creates Destinations first and then builds Actions that target specific Destinations. Because of this, you are unable to cleanly support multi-module Applications using AndroidX Navigation, along with a whole host of other minor usability issues.

Enro takes a different approach. Enro creates Actions first and then builds Destinations that fulfil specific Actions. This allows Enro to be easier to use, less verbose, and integrate much more easily with multi-module projects.

Please check out Enro here, I’d love to hear your feedback and feature requests.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Isaac Udy

Android Engineer @ ANZx, builds and maintains Enro, likes coffee and large Android projects.

Responses (4)

What are your thoughts?