Dynamic Feature navigation with Deep Links on Android
tl;dr; There is a way to use deep linking for dynamic feature modules and prevent a chooser from popping up.
Dynamic Feature Navigation
Let’s imagine for a minute we are working on an Android app for selling robot parts. And from a list of parts in the app, we want to go to a details page of a specific part. For this we created a PartDetailsActivity
. Now to navigate to this Activity using an Intent
we have several options in Android. At first we might use the tried and tested way of directly using the Activity name in the Intent. Like this:
But then we realise that it might not be so nice that we have direct references to Activities all over the place. We are only interested that the details are being shown, and we shouldn’t care who does this.
Luckily for us, there is the <intent-filter>
which will allow us to do just that. This way we can use links or deep links to navigate within our app. And as an added bonus, we could use those same links if we had an iOS version of the app as well.
Happy with how we are already removing some tight coupling, we realise we could now move our details page to a feature module. This would decouple even more and create a cleaner architecture. And it will also open the door for creating an Instant App by using App Links to go straight to the Details page if needed.
Happy about all we have achieved so far, we want to go even further and convert our normal, boring Feature Module into one of these brand spanking new Dynamic Feature modules you have heard so much about. The biggest change is that the dependency between the modules is now inverted. The details
module now depends on the app module. But the app no longer has any dependency on the details
module. This would have been a problem if we were still referring to the actual PartDetailsActivity
, which is now no longer visible from the rest of the app. Luckily enough, we do not have this problem. We already abstracted that away behind the usage of deep links. So for us, everything should still work as if nothing changed, “inverted, inschmerted” we think!
Still gloating from how our fancy deep links saved the day, we click on a robot part in our app to confirm what we already know, everything will still work. Wait, what just happened!? We do end up on our Details Activity, but something happened in between. For a short moment (which felt like an eternity to our non-believing eyes) a chooser dialog popped up, with our details activity as the only option. Then as quickly as it appeared, it vanished again, as if it was never there. Maybe we imagined it? We press back and try again. And again. I think, by law, we have to check at least 15 times to make sure it’s not us. But it happens every time we try. We can’t ship this to our users. Even if they will only be 1/10th as confused as we are, the’d still be very confused indeed. And they will let us know. They always let us know…
Finding a solution
So let’s go and Sherlock Holmes our way out of this.
Let’s first figure out if it’s just us doing something wrong. What would be the correct way of doing this? Well, there is the Google’s App Bundles Sample which does the same thing. Maybe we can see how they did this.
A quick clone and compile of the project later we can observe the correctly functioning sample. Oh wait, no we can’t. It has the same problem! We are doing this correctly, but for some reason this will always happen. After some more research we find others with the same problem. There is a bug report for it, and a report on stack overflow. The only ‘fix’ that is mentioned in several places is by hardcoding the actual complete name of the Activity. So not only would we be back to square one, by directly using the Activity, it’s a bit worse as we now will be using a hardcoded String. One which would be outdated as soon as we would rename or move the Activity, which could potentially crash our app.
We are not giving up yet. Let’s dig a little further, by using Google’s App Bundle Sample to figure out what’s going on. We remember that we can find out what’s going to happen with an intent by calling resolveActivity()
on it. So we fire up the trusty old debugger and have a look. First with a working url that starts the MainActivity:
Here we see that this would indeed directly start the MainActivity like we expected. Now let’s try the url that has the problem:
This is not what we would expect. It shows that the ResolverActivity will be started. This indicates that the system thinks there are multiple candidates for the Intent
and it needs to show a chooser. But we only defined the <intent-filter>
once in the Manifest. Just to be sure, let’s analyze the app bundle in Android Studio, to see what the final AndroidManifest files look like. We can already see there is a base
and a url
module. And when we look at the url
modules manifest file, we can see the correct entry:
Could it be that the same entry was merged into the base
manifest as well? Let’s check:
Well, that explains a lot. There are actually 2 entries for the same Activity. One in the base
and one in url
feature module. So the system thinks there are 2 options, even though they are identical. Let’s go back to the debugger and verify this is really the case.
We can use the method queryIntentActivities
provided by PackageManager
to get all the activities supported by an Intent. Let have a look with our failing Intent:
Bingo! Two identical entries for the Activity. So it seems the system first thinks there are 2 options for the intent and then for some unknown reason show the chooser, after which it figures out there is actually only one option. It then quickly selects that one, and then promptly hides it self, hoping nobody noticed the boo-boo it made.
The Fix
Now, could we somehow fix this?
While debugging we noticed that the queryIntentActivities
actually knows exactly which Activity it was about to start, even though we only gave it our deeplink:
A possible solution dawns on us. What if we create our Intent as normal. But before we try starting it, we ask the system through queryIntentActivities
which activities it thinks it will start. It will then return the list with the two identical options. We can take the first one and get the name of the Activity from it with activityInfo.name
, and add it to our original Intent and start it like normal. With the hardcoded name of the Activity now added to the Intent, it should now start it directly, skipping the chooser. Let’s check it in the debugger:
Success! We can now navigate to the dynamic feature with a deep link, without getting chooser. To make this a bit easier to use, let’s create an extension function for Intent
so we can easily change them before we start them:
Conclusion
With this in place, it should now be possible to use the same method of navigation in your app for everything. Like external links, Instant Apps, Dynamic Feature and maybe even your iOS app.