SafeArgs not generating Directions / Args classes on a multi module project

Viewed 2926

I created an empty project to try out the Navigation component. I wanted to see how it would behave with a multi module project (one common module with the majority of the dependencies, plus modules that would hold different parts of the app, and the :app module that would implement all of the modules).

The top level gradle file has the dependency like so:

dependencies {
    classpath "com.android.tools.build:gradle:3.6.1"
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61"
    classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.0-alpha04"
}

The common module has these dependencies (among others):

dependencies {
    …
    api "androidx.navigation:navigation-fragment-ktx:2.2.1"
    api "androidx.navigation:navigation-ui-ktx:2.2.1"
    api "androidx.navigation:navigation-dynamic-features-fragment:2.3.0-alpha04"
    …
}

And absolutely all the modules have this plugin applied on top:

apply plugin: "androidx.navigation.safeargs.kotlin"

Now, each "ui module" has fragments in it, and only the :app one implements a main_graph.xml that references to them. An example would be:

<fragment
    android:id="@+id/registerFragment"
    android:name="example.register.RegisterFragment"
    android:label="RegisterFragment">
    <action
        android:id="@+id/action_registerFragment_to_loginFragment"
        app:destination="@id/loginFragment" />
</fragment>

After all this is finished, a good Clean Project + Rebuild Project is done.

To me, this makes sense. However, when I try calling the theoretically auto generated file RegisterFragmentDirections is not there, not in the specific sub module, not in the main one. The NavDirections can be found (so the dependencies somehow are working), but not the generated ones.

I've tried implementing all the dependencies in each module, rolling back the navigation version to the previous alpha ones… no success.

6 Answers

I had the exact same requirement for my project as well. When I was searching for answers, I stumbled upon this, but none of the answer actually answered the question. So, here's how I solved it.

I have a main app module. Let's say app. I have about 7 feature modules with their own fragments and their own flow. From my experience this is what happens:

If you include all modules to main app module and create a single graph with all the included fragments, then the navigation directions and arguments files generated are generated for main app module. So, that would mean, you cannot use those inside a separate module. But, if you create navigation graphs in each module, and use that as included module from main graph, the main module will not know about those directions and arguments internal to the module's graph. And hence, therein lies the problem, catch 22 style.

How I solved it:

  1. Each module have their own navigation graph. Make sure they are id'ed in navigation tag
  2. Those graphs are included in main graph with include.
  3. In each module, I have defined an interface that defines all the navigation actions. For example, in Module1 I have
interface Module1Navigation {
    fun navigateToFragment2(arg1: String, arg2: Int)
    ...
}
  1. In the main app, I have a Navigator class that implements all these interfaces, like:
object Navigator: Module1Navigation, Module2Navigation, ... {
    private var navController: NavController? = null
    // bind in onResume for activity implementing the graph
    fun bind(navController: NavController) {
        this.navController = navController
    }

    // bind in onPause for activity implementing the graph
    fun unbind() {
        this.navController = null
    }
    // Implement all the members
    ...
}
  1. Each of the module level interfaces are injected with DI in each fragment that is gonna navigate away, or you could pass that in. I used DI to get it:
    private val navigation: Modudle1Navigation by lazy {
        XInjectionManager.findComponent<Modudle1Navigation>()
    }

With this setup now all modules are free to have their own graphs, their own safe args, and integrates well all together.

Best thing about this is also the fact that you module does not even need to know about this particular navigation framework, or how it is implemented, and is easy to scale.

Also, you can create each modules own app for more controlled module level quality assurance.

Hope this helps someone out there.

In a submodule's fragment that was navigated to via an include in main module's navigation graph, simply:

findNavController().setGraph(R.navigation.submodulegraph)

Then you should be able to use the directions of the destination you are currently on (which should be the initial destination of the submodule's graph since it was used in an include in main module's graph).

Update: I found that Fragments go AWOL using this technique on the current Activity, to overcome that I ensure that the navigation subsequent to setting the graph navigates to a new Activity and then finish the current Activity. That yields a fresh, reasonable navigation graph without any need to callback to the original Activity, at the cost of an extra Activity opening in the flow.

Try including in the main nav graph the tag and referencing the dynamic feature modules you need.

 <include-dynamic
        android:id="@+id/included_nav"
        app:graphPackage="org.example.dfm"
        app:graphResName="nav_tab"
        app:moduleName="example_dfm" />
    

Things you also need, and you didn't mention that you have:

  1. Your Android Manifest. Make sure, that you have
<activity
 android:name="YourActivity">
  <nav-graph android:value="@navigation/main_graph" />
</activity>
  1. Make sure your submodule includes the common one (by adding this line to your submodule build.gradle file).
implementation project(":yourCommonModule")
  1. I'm not sure if that is connected but make sure you have those lines a the top of the build.gradle files:
apply plugin: "kotlin-android"
apply plugin: "kotlin-android-extensions"
apply plugin: "kotlin-kapt"

you must add

id "kotlin-kapt"
id "androidx.navigation.safeargs.kotlin"

into your plugins { ... } body of your modules

or, with old approache

apply plugin "kotlin-kapt"
apply plugin "androidx.navigation.safeargs.kotlin"

Don't forget to make Rebuild Project after Gradle Sync

Related