Keeping our Android Navigation Safe
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.
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.
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.
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!
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.