在多模块项目中导航



在开发Android应用程序中导航非常重要,您应该三思而后行最适合哪个库(或您自己的解决方案),以及在应用程序变大时如何方便使用。 另外,最好考虑将您的实现更改为另一种实现有多么容易。

在我们开始之前,让我讲一个故事。 我们这样称呼它为“我们如何使项目模块化以及为什么讨厌导航”。

我们只有一个模块项目,除构建时间外,其他一切工作正常,这是因为我们使用了Dagger,并且生成单个组件花费的时间太长。 因此,我们决定按功能将项目分为模块,例如登录功能,帮助功能等。

我们遇到了很多困难,因为我们不得不将网络逻辑移到其自己的Gradle模块中,将域登录移入其模块中,并且导航也遇到了问题。

在我们的实现中,我们有一个路由器,它知道每个Fragment,并且负责它们之间的转换。 不幸的是,每个Fragment都知道有路由器,并且知道其他Fragment,因为Fragment向路由器说了应该打开哪个Fragment,或者Fragment正在等待另一个Fragment的结果。 这些事情破坏了制作独立功能模块的想法。

因此,我一直在思考如何使其变得更好,如何断开Fragment之间的这些连接,如何摆脱有关存在某些路由器的知识。 在本文中,我将展示我的所作所为。

我们会做什么



该应用程序将非常简单,但将是模块化的。 将有一个应用程序模块和三个功能模块:

  • 应用模块-该模块了解有关应用的所有信息,我们将在其中实现导航。 我们将使用JetPack的导航组件。
  • 问题模块-此模块负责问题功能。 该模块对App模块或任何其他模块一无所知。 将有一个QuestionsFragmentQuestionsNavigation接口。
  • 问题模块-将有一个问题功能。 作为前一个模块,它不了解其他模块。 该模块将包含一个QuestionFragmentQuestionNavigation接口。
  • 结果模块-这是结果功能。 而且,它是完全独立的模块。 将会有一个带有RightAnswerNavigation接口的WrongAnswerFragment和一个WrongAnswerNavigation接口的WrongAnswerNavigation

该应用程序的源代码可在以下位置找到: GitHub

实作


在本文中,我将使用ComponentsManager库来获取功能的导航。 在真实的项目中,我将提供Dagger的功能导航,但是对于这个小项目,则没有必要。 此外, ComponentManager库还可以帮助您在多模块项目中使用Dagger

准备好了吗 走吧

问题模块


创建一个Android模块并将其称为问题 。 之后,确保在模块中配置了Kotlin,并将ConstraintLayoutComponentsManager添加为依赖项。

 //build.gradle (for the questions module) apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "com.github.valeryponomarenko.componentsmanager:androidx:2.0.1" ... } 

而不是在Fragment中使用RouteNavigator ,我们将创建一个定义所需转换的接口QuestionsNavigation 。 因此,对于Fragment而言,这些过渡的实现方式无关紧要,它只需要该接口并依赖于何时调用该方法,将打开所需的屏幕。

 interface QuestionsNavigation { fun openQuestion(questionId: Long) } 

要打开一个带有问题的屏幕,我们只需调用openQuestion(questionId: Long)方法即可。 我们不在乎如何打开屏幕,甚至不在乎是Fragment还是Activity或其他东西。

这是QuestionsFragment及其布局

 class QuestionsFragment : Fragment() { private val navigation: QuestionsNavigation by lazy { XInjectionManager.findComponent<QuestionsNavigation>() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_questions, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button_first_question.setOnClickListener { navigation.openQuestion(1) } button_second_question.setOnClickListener { navigation.openQuestion(2) } button_third_question.setOnClickListener { navigation.openQuestion(3) } } } 

问题模块


首先,创建一个Android库模块并将其称为问题 。 模块的build.gradle必须包含与问题模块的build.gradle文件相同的依赖项。

 //build.gradle (for the question module) apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "com.github.valeryponomarenko.componentsmanager:androidx:2.0.1" ... } 

之后,我们创建一个定义模块导航的界面。 其他模块中的导航思路与上一个模块相同。

QuestionFragment用户可以打开错误的答案屏幕或正确的答案屏幕,因此该界面将具有两种方法。

 interface QuestionNavigation { fun openWrongAnswer() fun openRightAnswer() } 

最后一件事是片段。 对于此片段,我们将添加一个伴随方法,该方法返回Bundle以便我们能够传递问题的ID并使用它。 片段的布局

 class QuestionFragment : Fragment() { companion object { private const val EXTRA_QUESTION_ID = "me.vponomarenko.modular.navigation.question.id" fun createBundle(questionId: Long) = Bundle().apply { putLong(EXTRA_QUESTION_ID, questionId) } } private val navigation: QuestionNavigation by lazy { XInjectionManager.findComponent<QuestionNavigation>() } private val questionId: Long by lazy { arguments?.getLong(EXTRA_QUESTION_ID) ?: throw IllegalStateException("no questionId") } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_question, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) text_question.text = getString(R.string.question, questionId) button_right_answer.setOnClickListener { navigation.openRightAnswer() } button_wrong_answer.setOnClickListener { navigation.openWrongAnswer() } } } 

结果模块


最终功能模块是结果模块。 将有两个片段显示正确和错误的答案。 创建一个Android库模块并将其命名为result ,然后更改该模块的build.gradle,使其与以前的功能模块具有相同的依赖关系。

 //build.gradle (for the result module) apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation "com.github.valeryponomarenko.componentsmanager:androidx:2.0.1" ... } 

让我们从正确的答案片段开始。 导航界面只有一种方法,因为用户只能打开所有问题屏幕。

 interface RightAnswerNavigation { fun openAllQuestions() } 

RightAnswerFragment ,其布局

 class RightAnswerFragment : Fragment() { private val navigation: RightAnswerNavigation by lazy { XInjectionManager.findComponent<RightAnswerNavigation>() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_right, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button_all_questions.setOnClickListener { navigation.openAllQuestions() } } } 

就像我之前说的,这个模块有两个Fragments ,所以让我们实现一个错误的答案屏幕。

在我的实现中,用户从错误的答案屏幕只能返回问题的屏幕,然后尝试再次回答问题,因此导航界面只有一种方法。

 interface WrongAnswerNavigation { fun tryAgain() } 

还有WrongAnswerFragment,其布局

 class WrongAnswerFragment : Fragment() { private val navigation: WrongAnswerNavigation by lazy { XInjectionManager.findComponent<WrongAnswerNavigation>() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? = inflater.inflate(R.layout.fragment_wrong, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) button_try_again.setOnClickListener { navigation.tryAgain() } } } 

应用模块


我们已经制作了模块,是时候连接它们并运行应用程序了。

首先,我们需要编辑应用程序模块的build.gradle。 它必须具有所有创建的模块和带有组件管理器的导航组件库。

 //build.gradle (for the app module) apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { ... } dependencies { ... implementation project(':questions') implementation project(':question') implementation project(':result') implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'com.github.valeryponomarenko.componentsmanager:androidx:2.0.1' implementation 'android.arch.navigation:navigation-fragment-ktx:1.0.0-alpha11' ... } 

在实现与导航一起使用的类之前,我们必须添加导航本身。 我们将使用导航组件来构建它。

创建一个导航资源文件,并将其命名为nav_graph.xml 。 将有三个连接:

  • QuestionFragment
  • 带有WrongAnswerFragmentQuestionFragment
  • 具有RightAnwerFragmentQuestionFragment 。 这种连接略有不同。 如果用户位于RightAnswerFragment ,并且用户按下返回按钮,则他或她将返回到QuestionsFragment 。 如何做到这一点? 只需选择将questionFragmentrightAnswerFragment连接的箭头,然后在Pop附近的下拉列表选择questionsFragment


这是导航图的XML表示形式。

 <?xml version="1.0" encoding="utf-8"?> <navigation xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/nav_graph" app:startDestination="@id/questionsFragment"> <fragment android:id="@+id/questionsFragment" android:name="me.vponomarenko.modular.navigation.questions.QuestionsFragment" android:label="QuestionsFragment"> <action android:id="@+id/action_questionsFragment_to_questionFragment" app:destination="@id/questionFragment" /> </fragment> <fragment android:id="@+id/questionFragment" android:name="me.vponomarenko.modular.navigation.question.QuestionFragment" android:label="QuestionFragment"> <action android:id="@+id/action_questionFragment_to_wrongAnswerFragment" app:destination="@id/wrongAnswerFragment" /> <action android:id="@+id/action_questionFragment_to_rightAnswerFragment" app:destination="@id/rightAnswerFragment" app:popUpTo="@+id/questionsFragment" /> </fragment> <fragment android:id="@+id/wrongAnswerFragment" android:name="me.vponomarenko.modular.navigation.result.wrong.WrongAnswerFragment" android:label="WrongAnswerFragment" /> <fragment android:id="@+id/rightAnswerFragment" android:name="me.vponomarenko.modular.navigation.result.right.RightAnswerFragment" android:label="RightAnswerFragment" /> </navigation> 

然后,创建一个Navigator类。 显然,该类负责在片段之间进行转换。 它实现了功能模块中的所有导航界面,请不要忘记添加相应动作的调用以打开所需的屏幕。 同样,该类具有绑定navController并将其解除绑定的方法。

 class Navigator : QuestionsNavigation, QuestionNavigation, RightAnswerNavigation, WrongAnswerNavigation { private var navController: NavController? = null override fun openQuestion(questionId: Long) { navController?.navigate( R.id.action_questionsFragment_to_questionFragment, QuestionFragment.createBundle(questionId) ) } override fun openWrongAnswer() { navController?.navigate(R.id.action_questionFragment_to_wrongAnswerFragment) } override fun openRightAnswer() { navController?.navigate(R.id.action_questionFragment_to_rightAnswerFragment) } override fun openAllQuestions() { navController?.popBackStack() } override fun tryAgain() { navController?.popBackStack() } fun bind(navController: NavController) { this.navController = navController } fun unbind() { navController = null } } 

在第8行,您可以看到我如何将问题的ID传递给QuestionFragment

之后,创建一个NavApplication类。 实际上,我必须添加此类以使Navigator可用于其他类,否则,很难在功能模块中获得导航器。 不要忘记将此类添加到清单。

 class NavApplication : Application() { override fun onCreate() { super.onCreate() XInjectionManager.bindComponentToCustomLifecycle(object : IHasComponent<Navigator> { override fun getComponent(): Navigator = Navigator() }) } } 

通过 4-6行,可以通过调用XInjectionManager.findComponent()获得功能导航界面的实现。

最后但并非最不重要的一点是,更改MainActivity类及其布局

 class MainActivity : AppCompatActivity() { private val navigator: Navigator by lazy { XInjectionManager.findComponent<Navigator>() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } override fun onResume() { super.onResume() navigator.bind(findNavController(R.id.nav_host_fragment)) } override fun onPause() { super.onPause() navigator.unbind() } override fun onSupportNavigateUp(): Boolean = findNavController(R.id.nav_host_fragment).navigateUp() } 

就是这样,现在您可以运行该应用程序并查看其工作方式。

总结


干得好! 我们制作了一个导航系统,该系统:

  • 断开不同功能模块中片段之间的连接,
  • 可以轻松更改
  • 易于测试。

交叉路口

Source: https://habr.com/ru/post/zh-CN443288/


All Articles