Keeping our Android Navigation Safe
By: Victoria Gonda
Tags: Android, Testing, Android Modularization, Android App Development, Automated Testing

At Meetup we try to keep the code in our Android app organized and easy to work in the best we can. One way we organize our code is through the use of feature modules. This means that a Search feature might be in one module, while an Event feature might be in another. In other words, our features each live in their own place to keep the code in an understandable order and easier to work in. It gives each module their own responsibility so we know where something is when we’re looking for it.

One thing this means is that features don’t have access to each other, keeping those distinctions clean.

App module has reference to feature modules, but feature modules do not

In this organization, the App module knows about Feature A and Feature B, but the feature modules don’t know about each other or about the App module. In this situation, how can you navigate from a screen in Feature A to Feature B?

Setting up our Navigation

Normally, without the feature module architecture, you would navigate from Feature A to Feature B by referencing the destination Activity to create the Intent , or directions. You can think of an Activity as a screen and an Intent as the directions for how to get there. It would look something like this:

// Navigate to Feature B, referencing the activity

startActivity(

Intent(context, FeatureBActivity::class.java)

)

This is really safe and you’re almost always guaranteed that it will work. Even if you need to change the name of the feature, you can use developer refactoring tools to make sure it updates everywhere you’re using it all at once so it never breaks.

Because our feature modules don’t have a reference to each other, we need another option. Thankfully, we can use the full string name of the Activity to create the Intent .

// Navigate to Feature B, using the activity name

startActivity(

Intent().apply {

setClassName(“com.example”, “com.example.FeatureBActivity”)

}

)

The downside of this is that if we change the name of the feature from Feature B to Feature C, we have to go back to every place where we have this navigation code and update the string. This can be a lot of work in a full app with many features and ways to get to them.

It can also be error-prone. What if you miss a spot? You won’t know until you run the app and it crashes when you try to navigate in the spot you missed.

Unable to find explicit activity class FeatureBActivity

A common solution for this is to create an additional module that helps with this navigation. This navigation module doesn’t know about any other modules, but the feature modules know about it.

All features reference the Navigation module

Now the Navigation module can handle creating the Intent with the string, and if you change the name of the feature Activity , you only need to change it in one place.

// In navigation module

object Activities {

fun getFeatureBIntent() = Intent().apply {

setClassName(“com.example”, “com.example.FeatureBActivity”)

}

}

Then to use it:

// In feature module, navigate to Feature B, using the navigation module

startActivity(

Activities.getFeatureBIntent()

)

Great! Problem solved.

…until you still forget to change the name in the Navigation module after refactoring an Activity and you find out later when the app crashes.

Catching it Before it Crashes

One way we make sure things are working before we release a new version of the app is by running automated tests. We run these tests regularly so we find out about issues before sharing a new version.

That sounds like a great way to ensure we always have our navigation intact.

One option might be to write tests in the navigation module to launch the Intent and see if it crashes. While that might work, it could make for some slow tests with all that Activity launching.

For another way to accomplish these tests, we need to have a reference to everything in both the navigation and feature modules. Where do we have that? In the App module!

App module has reference to all other modules

To write these tests, we can get the reference to the feature Activity , similar to the safe way to navigate we considered first, and compare it to what we get from the Navigation module. That way, if one of them changes without the other being updated, we’ll be notified.

val intent = Activities.getFeatureBIntent()

assertEquals(

expected = FeatureBActivity::class.java.name,

actual = intent.component!!.className

)

Voila! Now we can navigate and make changes safely and securely.