
在开发Android应用程序中导航非常重要,您应该三思而后行最适合哪个库(或您自己的解决方案),以及在应用程序变大时如何方便使用。 另外,最好考虑将您的实现更改为另一种实现有多么容易。
在我们开始之前,让我讲一个故事。 我们这样称呼它为“我们如何使项目模块化以及为什么讨厌导航”。
我们只有一个模块项目,除构建时间外,其他一切工作正常,这是因为我们使用了Dagger,并且生成单个组件花费的时间太长。 因此,我们决定按功能将项目分为模块,例如登录功能,帮助功能等。
我们遇到了很多困难,因为我们不得不将网络逻辑移到其自己的Gradle模块中,将域登录移入其模块中,并且导航也遇到了问题。
在我们的实现中,我们有一个路由器,它知道每个Fragment,并且负责它们之间的转换。 不幸的是,每个Fragment都知道有路由器,并且知道其他Fragment,因为Fragment向路由器说了应该打开哪个Fragment,或者Fragment正在等待另一个Fragment的结果。 这些事情破坏了制作独立功能模块的想法。
因此,我一直在思考如何使其变得更好,如何断开Fragment之间的这些连接,如何摆脱有关存在某些路由器的知识。 在本文中,我将展示我的所作所为。
我们会做什么
该应用程序将非常简单,但将是模块化的。 将有一个应用程序模块和三个功能模块:
- 应用模块-该模块了解有关应用的所有信息,我们将在其中实现导航。 我们将使用JetPack的导航组件。
- 问题模块-此模块负责问题功能。 该模块对App模块或任何其他模块一无所知。 将有一个
QuestionsFragment
和QuestionsNavigation
接口。 - 问题模块-将有一个问题功能。 作为前一个模块,它不了解其他模块。 该模块将包含一个
QuestionFragment
和QuestionNavigation
接口。 - 结果模块-这是结果功能。 而且,它是完全独立的模块。 将会有一个带有
RightAnswerNavigation
接口的WrongAnswerFragment
和一个WrongAnswerNavigation
接口的WrongAnswerNavigation
。
该应用程序的源代码可在以下位置找到:
GitHub 。
实作
在本文中,我将使用
ComponentsManager库来获取功能的导航。 在真实的项目中,我将提供
Dagger的功能导航,但是对于这个小项目,则没有必要。 此外,
ComponentManager库还可以帮助您在多模块项目中使用
Dagger 。
准备好了吗 走吧
问题模块
创建一个Android模块并将其称为
问题 。 之后,确保在模块中配置了Kotlin,并将
ConstraintLayout和
ComponentsManager添加为依赖项。
而不是在
Fragment
中使用
Route
或
Navigator
,我们将创建一个定义所需转换的接口
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文件相同的依赖项。
之后,我们创建一个定义模块导航的界面。 其他模块中的导航思路与上一个模块相同。
从
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,使其与以前的功能模块具有相同的依赖关系。
让我们从正确的答案片段开始。 导航界面只有一种方法,因为用户只能打开所有问题屏幕。
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。 它必须具有所有创建的模块和带有组件管理器的导航组件库。
在实现与导航一起使用的类之前,我们必须添加导航本身。 我们将使用导航组件来构建它。
创建一个导航资源文件,并将其
命名为nav_graph.xml 。 将有三个连接:
- 带
QuestionFragment
- 带有
WrongAnswerFragment
的QuestionFragment
- 具有
RightAnwerFragment
的QuestionFragment
。 这种连接略有不同。 如果用户位于RightAnswerFragment
,并且用户按下返回按钮,则他或她将返回到QuestionsFragment
。 如何做到这一点? 只需选择将questionFragment
与rightAnswerFragment
连接的箭头,然后在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() }
就是这样,现在您可以运行该应用程序并查看其工作方式。
总结
干得好! 我们制作了一个导航系统,该系统:
- 断开不同功能模块中片段之间的连接,
- 可以轻松更改
- 易于测试。
交叉路口