英文:
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 opensSelectSquadronFragment
+SelectNumberFragment
from Settings, I don't want the user to have to go throughFinishFragment
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 fromMedictFragment
toInviteFriendsFragment
I useMedicFragmentDirections
. I was thinking about having theActivity
provide theseNavDirections
, 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 opensSelectSquadronFragment
+SelectNumberFragment
from Settings, I don't want the user to have to go throughFinishFragment
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 fromMedictFragment
toInviteFriendsFragment
I useMedicFragmentDirections
. I was thinking about having theActivity
provide theseNavDirections
, 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 <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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论