Hey Android Devs, in this article we will see the implementation of navigation across multiple feature modules.
What is the Problem?
Recently, I was setting up a non-compose project in a multi-modular way. According to my use case, I was not convinced to go for Single Activity Architecture, instead, I was looking for something like at least one Activity per feature module, i.e; every feature module must have at least one activity as an entry point.
Jetpack navigation can work pretty well in a multi-modular approach. Still, it is mainly based on fragments. I feel that for big projects, there will be many instances where it would be better to have Activities instead of fragments because activities will have their own lifecycle and life will be much easier.
Why DeepLinks?
There are other ways as well for navigation purposes across modules but the main reason to choose the DeepLink approach is that it is platform-independent. In future, there might be cases when we launch some features and want our users to directly land to that particular feature module with just a tap on a link. Then it would be much easier and comparatively faster to scale.
The approach
- We will create one activity for each module as an entry point
- For each of the activities, we will create a processor which launches one particular activity after ensuring that the incoming deep link was supposed to be for that activity.
Enough talking, now let’s implement it
First of all, set up a non-compose project and create two feature modules called feature_01 and feature_02 and one core module besides the app module.
Module dependency can be defined as
app depends on all the modules core, feature_01 and feature_02
feature_01 depends on core
feature_02 depends on core
core depends on nothing
Here, the core is the common module for all the other modules.
Next, inside the core module, create a package for navigation which will contain some classes and interfaces which will be used to define deeplink processors for each of the feature modules.
First of all, create a DeeplinkProcessor interface. It will have two functions, one to match the deeplink and the other one to execute the launching of activity after matching.
interface DeeplinkProcessor { | |
fun matches(deeplink: String): Boolean | |
fun execute(deeplink: String) | |
} |
Next, create a handler which will have only one function called process whose responsibility would be to execute the processor.
interface DeeplinkHandler { | |
fun process(deeplink: String): Boolean | |
} |
After this, we will create a DefaultHandler which will be an implementation of this DeeplinkHandler. It will call execute function for the first processor found matching the deeplink.
class DefaultDeeplinkHandler constructor( | |
private val processors: Set<@JvmSuppressWildcards DeeplinkProcessor> | |
): DeeplinkHandler { | |
override fun process(deeplink: String): Boolean { | |
processors.forEach { | |
if (it.matches(deeplink)) { | |
it.execute(deeplink) | |
return true | |
} | |
} | |
return false | |
} | |
} |
Now, we will create activities for each of the feature modules, namely Feature01Activity and Feature02Activity. Corresponding to them we will create DeeplinkProcessors for both of them as well, namely Feature01DeeplinkProcessor and Feature02DeeplinkProcessor.
@Singleton | |
class Feature01DeeplinkProcessor @Inject constructor( | |
private val context: Context | |
) : DeeplinkProcessor { | |
override fun matches(deeplink: String): Boolean { | |
return deeplink.contains("/feat01") | |
} | |
override fun execute(deeplink: String) { | |
val intent = Intent(context, Feature01Activity::class.java) | |
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | |
context.startActivity(intent) | |
} | |
} |
For Feature02DeeplinkProcessor, we can handle some extra data as well coming along with the link.
@Singleton | |
class Feature02DeeplinkProcessor @Inject constructor( | |
private val context: Context | |
) : DeeplinkProcessor { | |
override fun matches(deeplink: String): Boolean { | |
return deeplink.contains("/feat02") | |
} | |
override fun execute(deeplink: String) { | |
val extraData = deeplink.split("/feat02/").getOrNull(1) | |
val intent = Intent(context, Feature02Activity::class.java) | |
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) | |
intent.putExtra("extraData",extraData) | |
context.startActivity(intent) | |
} | |
} |
Till here, we have all the building blocks to navigate from feature_01 to feature_02. Now we just have to combine all of this in action.
Now maintaining instances of all these processors can be tedious as we are going to have n number of modules in our application.
To help us in this we will use Dagger-Hilt to provide and execute all the processors defined across the modules.
In our app module, create an object called AppModule which will provide application context and a deeplink handler to handle all the defined processors.
@Module | |
@InstallIn(SingletonComponent::class) | |
object AppModule { | |
@Provides | |
@Singleton | |
fun providesContext(@ApplicationContext context: Context): Context = context | |
@Provides | |
@Singleton | |
fun providesDefaultDeeplinkHandler( | |
processors: Set<@JvmSuppressWildcards DeeplinkProcessor> | |
): DeeplinkHandler = DefaultDeeplinkHandler(processors) | |
} |
In providesDefaultDeeplinkHandler() we need to provide a set of processors but since DeeplinkProcessor is an interface, we can’t just provide it by instantiation, that’s why we need to bind it using hilt only.
@Module | |
@InstallIn(SingletonComponent::class) | |
interface DeepLinkProcessorModule { | |
@Binds | |
@IntoSet | |
fun bindFeat01Processors( | |
feature01DeeplinkProcessor: Feature01DeeplinkProcessor | |
): DeeplinkProcessor | |
@Binds | |
@IntoSet | |
fun bindFeat02Processors( | |
feature02DeeplinkProcessor: Feature02DeeplinkProcessor | |
): DeeplinkProcessor | |
} |
Job Offers
This will bind all the processors to the set required to create DefaultHandler.
Now we can inject this Default Handler inside our MainActivity and handle deeplinks.
private fun handleIntent(intent: Intent) { | |
intent.data?.toString()?.let { | |
deeplinkHandler.process(it) | |
finish() | |
} | |
} |
Before moving to the actual navigation part we need to define the scheme and host for MainActivity in the manifest.xml file.
<intent-filter | |
android:autoVerify="true"> | |
<action android:name="android.intent.action.VIEW" /> | |
<category android:name="android.intent.category.DEFAULT" /> | |
<category android:name="android.intent.category.BROWSABLE" /> | |
<data | |
android:scheme="raystatic" | |
android:host="multi.module.app"/> | |
</intent-filter> |
Now we can try navigation like this:
binding.btnGoto.setOnClickListener { | |
// Navigate to feature_01 | |
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("raystatic://multi.module.app/feat01")) | |
startActivity(intent) | |
// Navigate to feature_02 | |
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("raystatic://multi.module.app/feat02")) | |
startActivity(intent) | |
} |
Here is a demo of navigation across modules.
Check out the sample repository here.
Let’s connect on LinkedIn and Twitter!
Happy coding!
This article was originally published on proandroiddev.com on July 10, 2022