0

Context

I have a multi-module architecture, with multiple feature modules, it kinda looks like this:

enter image description here

I have multiple feature modules that depend on a :core_library library module that contains all the common dependencies (Retrofit, Room, etc.) and then different feature modules for each of the different app flows. Finally, the :app application module ties everything together.

If you want to navigate between Activities in feature modules that don't know anything about each other I use an AppNavigator interface:

interface AppNavigator {
   fun provideActivityFromFeatureModuleA(context: Context): Intent
}

Then in the :app module Application class I implement this interface, and since the :app module ties everything together it knows each of the activities within each of the feature modules:

class MyApp : Application(), AppNavigator {
...
   override fun provideActivityFromFeatureModuleA(context: Context): Intent {
      return Intent(context, ActivityFromA::class.java)
   }
...
}

This AppNavigator component lives in a Dagger module up in :core_library and it can be injected in any feature module.

I have this :feature_login feature module that is for when the user creates a new account and has to go through the onboarding flow, things like inviting friends to join the app, checking for POST_NOTIFICATION permissions, adding any more details to its account, etc.

Each of the :feature_modules has one Activity and many Fragments I have a navigation graph to navigate between fragments.

The problem

The :feature_login navigation graph kinda looks like this:

enter image description here

The thing is that I need to reuse many of these Fragments across different parts of the App, more specifically, these Fragments

enter image description here

For example; When I open the app and land on the main screen, I check for POST_NOTIFICATION permissions, and if these haven't been granted, I want to prompt the PostNotificationFragment that checks for that and presents the user with a UI. The SelectSquadronFragment + SelectNumberFragment should be prompted if the user wants to change them from the Settings screen. When doing something I want to prompt the user with the InviteFriendsFragment.

The problem is that I don't know how to reuse these Fragments independently without having them navigate through the rest of the flow

What I have tried so far

  • Subgraphs don't really fix the issue. I can use the AppNavigator to either provide the hosting Activity I have in :feature_login or each individual Fragment, but the issue is still there. If the user opens SelectSquadronFragment + SelectNumberFragment from Settings, I don't want the user to have to go through FinishFragment afterward.

  • Extracting the navigation through an interface up to the Activity. Each Fragment in that navigation graph navigates through NavDirections. When I want to navigate from MedictFragment to InviteFriendsFragment I use MedicFragmentDirections. I was thinking about having the Activity provide these NavDirections, that way I could create customized Activities with the navigation routes that I want, but honestly, I would prefer to go with something that isn't that rocket science.

Please let me know if you need me to give you more info. Any feedback is welcome.

Example

Let me give you a precise example of what I'm struggling with here. Let's use something simple. Let's take the ChooseRoleFragment as an example.

This ChooseRoleFragment is a simple UI that shows three buttons with three roles ("Police", "Medic", and "Fireman") during the login flow, when the user clicks on one of these three buttons, he is taken to either the PoliceFragment, FiremanFragment or MedicFragment. This is in the login flow.

Now, I need to re-use this ChooseRoleFragment in the "Settings" section of the app. The only difference is that when I use it there, I don't want it to navigate to the FiremanFragment, MedicFragment or PoliceFragment, I just want it to go back to the "Settings" screen. The "Settings" screen is on a completely different feature module that doesn't know anything about :feature_login

To be more clear, the ChooseRoleFragment navigate to either the PoliceFragment, the FiremanFragment or the MedicFragment through a navigation graph. That means that in the ChooseRoleFragment once I click on each of the options I have something like this:

    class ChooseRoleFragment : Fragment() {
        //...
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            bindings.policeBtn.setOnClickListener {
                findNavController().navigate(ChooseRoleFragmentDirections.actionChooseRoleFragmentToPoliceFragment())
            }
            bindings.medicBtn.setOnClickListener {
                findNavController().navigate(ChooseRoleFragmentDirections.actionChooseRoleFragmentToMedicFragment())
            }
            bindings.firemanBtn.setOnClickListener {
                findNavController().navigate(ChooseRoleFragmentDirections.actionChooseRoleFragmentToFiremanFragment())
            }
        }
    }

So, this works perfectly for the login flow, but it won't do what I want for the "Settings" screen. So what I'm trying to figure out is, should I extract those NavDirections somewhere? That way when I want to re-use this Fragment in the "Settings" screen I can just override the NavDirections and have it navigate somewhere else.

4gus71n
  • 3,717
  • 3
  • 39
  • 66

1 Answers1

1

Your goal is to pass different interface implementation into the ViewModel's constructor, so that when accessed by the Fragment, it changes the destination of where you are going. You also want to extract these calls findNavController().navigate(ChooseRoleFragmentDirections.actionChooseRoleFragmentToPoliceFragment()) to come from outside, rather than directly hardcode what the actions must do. Also, if these fragments are re-used, they can't have their own actions, the actions must come from their enclosing NavGraph.

So how do you get a different interface implementation into your Fragment or ViewModel depending on current application state?

Good question, because neither ViewModel (it hardcodes <T extends ViewModel> as expected type) nor Hilt (global configuration) was meant for this. I can envision two options if you're stuck with Jetpack Navigation (which you are, as your app is big):

1.) define an interface in the Fragment that is "ActionHandler", and expect FQN of an ActionHandler implementation as a fragment argument. Then, use Class.forName(FQN).newInstance() to get a reference to it, and that will define the navigation actions the Fragment must do.

2.a) define an interface in the ViewModel that is "ActionHandler", and expect ActionHandler as a constructor argument. Pass in a different ActionHandler implementation from a custom ViewModelProvider.Factory (probably initializerViewModel {) depending on the current available NavGraph. This however requires seeing the NavController in your custom ViewModelProvider.Factory.

2.b) combine the two ideas. An initializerViewModel { receives the CreationExtras including the associated SavedStateHandle, therefore you can receive a "mode"-like enum (or FQN like before) into the ViewModelProvider.Factory through the SavedStateHandle, and setup the condition for the ActionHandler implementation based on a fragment argument.

This way, you can create a factory for your ActionHandler and not have to hard-code the action handling within your Fragment class. If you use FQN, make sure to @Keep the class with the @Keep annotation, or relevant Proguard rules. This ActionHandler implementation needs to see multiple NavGraphs, so it'll basically be some kind of Navigator defined in :app, or at least a module that can see multiple NavGraphs.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • Thanks for all the details! Q, couldn't I extract the `findNavController().navigate...` calls up to the hosting Activity. Let's say I need to launch the `ChooseRoleFragment ` from the "Settings" screen, then I'd launch an intent to the hosting Activity, pass some sort of configuration through the Intent bundle and use that to figure out which of the `Fragment` navigations should omit or not. Sounds correct? – 4gus71n Mar 08 '23 at 13:47
  • A piece of info that I think I left out is that many of the other feature modules in the app aren't still using this one-Activity-many-Fragments kind of navigation, they just have one Activity per screen, so it's not like I can rely on every other feature module having a navigation graph. – 4gus71n Mar 08 '23 at 13:48
  • I mean, I like the idea of extracting the navigation to the ViewModel, but I'm also trying to figure out the easiest and most digestible solution – 4gus71n Mar 08 '23 at 13:59
  • Also, I'm thinking, maybe, could I go with `ChooseRoleFragment` being `open` extract-method the `findNavController().navigate...` calls into functions, then create a new `StandaAloneChooseRoleFragment` that overrides those two functions so it does something else? – 4gus71n Mar 08 '23 at 14:11