Blog Infos
Author
Published
Topics
Author
Published

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

  1. We will create one activity for each module as an entry point
  2. 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 corefeature_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 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)
}
view raw AppModule.kt hosted with ❤ by GitHub

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

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, , ,

Navigation in a Multiplatform World: Choosing the Right Framework for your App

Navigation in mobile, desktop, and web applications is such a fundamental part of how we structure our architecture. In order to both obtain functional clarity, and abstraction from platform level implementation.
Watch Video

Navigation in a Multiplatform World: Choosing the Right Framework for your App

Ash Davies
Senior Android Developer

Navigation in a Multiplatform World: Choosing the Right Framework for your App

Ash Davies
Senior Android Devel ...

Navigation in a Multiplatform World: Choosing the Right Framework for your App

Ash Davies
Senior Android Developer

Jobs

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()
}
}
view raw MainActivity.kt hosted with ❤ by GitHub

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)
}
view raw MainActivity.kt hosted with ❤ by GitHub

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

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
I recently found a bug that would cause a crash in all the apps…
READ MORE
blog
Typically apps go from the navigation bar to the status bar. With the release…
READ MORE
blog
This article is for those who are faced with the choice of implementing their…
READ MORE
blog
This is one of those posts where I could have spilled out the answer…
READ MORE
Menu