如何在多模块架构中在导航图内重复使用片段?

huangapple go评论73阅读模式
英文:

How to reuse Fragments within a Nav Graph in a multi-module architecture?

问题

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

如何在多模块架构中在导航图内重复使用片段?

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 is that I need to reuse many of these Fragments across different parts of the App, more specifically, these Fragments:

如何在多模块架构中在导航图内重复使用片段?

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 navigates 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.

英文:

Context

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

如何在多模块架构中在导航图内重复使用片段?

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:

如何在多模块架构中在导航图内重复使用片段?

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

如何在多模块架构中在导航图内重复使用片段?

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.

答案1

得分: 1

您的目标是将不同的接口实现传递到ViewModel的构造函数中,以便在Fragment访问时更改您要前往的目标位置。您还希望提取这些调用findNavController().navigate(ChooseRoleFragmentDirections.actionChooseRoleFragmentToPoliceFragment()),而不是直接硬编码操作应该执行什么。此外,如果这些片段被重复使用,它们不能具有自己的操作,操作必须来自它们封装的NavGraph。

那么,如何根据当前应用程序状态为Fragment或ViewModel提供不同的接口实现呢?

好问题,因为ViewModel(它将<T extends ViewModel>硬编码为预期类型)和Hilt(全局配置)都不适合这种情况。如果您必须使用Jetpack Navigation(因为您的应用程序很大),我可以想象有两个选项:

1.) 在Fragment中定义一个名为“ActionHandler”的接口,并将ActionHandler实现的FQN作为Fragment参数的期望。然后,使用Class.forName(FQN).newInstance()来获取对它的引用,从而定义Fragment必须执行的导航操作。

2.a) 在ViewModel中定义一个名为“ActionHandler”的接口,并期望ActionHandler作为构造函数参数。根据当前可用的NavGraph,从自定义的ViewModelProvider.Factory(可能是initializerViewModel {)中传递不同的ActionHandler实现。但是,这需要在自定义的ViewModelProvider.Factory中看到NavController。

2.b) 结合这两个想法。initializerViewModel {通过CreationExtras接收了关联的SavedStateHandle,因此您可以通过SavedStateHandle将类似“mode”的枚举(或类似之前的FQN)传递到ViewModelProvider.Factory,并根据Fragment参数设置ActionHandler实现的条件。

这样,您可以为您的ActionHandler创建一个工厂,而不必在Fragment类中硬编码操作处理。如果使用FQN,请确保使用@Keep注释来保留类,或者相关的Proguard规则。这个ActionHandler实现需要查看多个NavGraphs,因此它基本上是在:app中定义的某种Navigator,或者至少可以查看多个NavGraphs的模块。

英文:

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 &lt;T extends ViewModel&gt; 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.

huangapple
  • 本文由 发表于 2023年3月3日 23:08:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/75628754.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定