[Android翻译]在大型银行应用中使用导航架构组件

503 阅读8分钟

原文地址:medium.com/google-deve…

原文作者:medium.com/@david.vavr…

发布时间:2019年2月24日 - 6分钟阅读

来自Jetpack的导航库最近达到了RC1,所有安卓开发者都应该开始考虑将其用于新的应用程序。我负责德国航空银行的应用架构--这是一家新的移动优先的德国银行。我们的应用有一个多模块、单活动的架构,采用Architecture Components的ViewModels。集成导航组件是一个合乎逻辑的步骤,但也不是没有一些问题。在这篇博文中,我想分享我们如何解决这些问题。这是一个比较高级的帖子,所以我假设读者对官方文档有一定的了解。

在一个多模块项目中,导航XML文件应该放在哪里?

多模块项目是一种推荐的结构化新应用程序的方式。它的优点是构建时间更快,代码库中的关注点分离得更好。这是我们Gradle模块的一个简化图。

image.png

有几个选项,可以把导航XML文件放在哪里。

app "模块

官方文档和Android Studio假设是这样。但你需要能够从一个功能导航到另一个功能(而功能对'app'模块没有依赖性)。这可以通过将所有导航目的地的id放到'common-android'模块内的ids.xml文件中来解决。然后导航就可以工作了,但你不能使用安全参数。你需要手动构建Bundles来向目的地传递参数。

主图在'app'模块中,子图在功能模块中

这类似于在'app'模块中拥有一个大图,但更加结构化。你可以在一个特征中使用安全参数,但在导航到一个不同的特征时不能使用。也可以使用'common-android'模块中的id的变通方法。

共用机器人 "模块

所有的功能都依赖于'common-android',所以你可以在任何地方使用Safe Args进行导航。然而,它有两个缺点。

  • 由于'common-android'模块没有对功能的依赖性,在Android Studio中,Fragment类是红色的。你没有Fragment类的自动完成功能,但它的编译没有任何问题。
  • 一个错误,它阻止了为深层链接生成意图过滤器。它已经被分配了,所以希望它能在最终版本中被修复。

这两个问题对我们来说不是一个障碍,在任何地方使用Safe Args确实是一个很大的优势。所以,我们的首选是把文件放在'common-android'模块中。

如何从ViewModels导航?

我们的应用程序使用Android架构组件中推荐的MVVM架构。文档显示了如何从Fragments开始导航,但这个逻辑应该在ViewModels中。我们试图把所有的模板代码放到BaseFragment和BaseViewModel中,以简化所有其他Fragments/ViewModels的代码。

命令

我们使用命令模式在ViewModel和Fragment之间通信。这些是我们用于导航的命令。

sealed class NavigationCommand {
  data class To(val directions: NavDirections): NavigationCommand()
  object Back: NavigationCommand()
  data class BackTo(val destinationId: Int): NavigationCommand()
  object ToRoot: NavigationCommand()
}

ViewModel将它们发布到一个LiveData对象中,由Fragments来监听。但它需要是一个一次性的事件。为此,我们使用架构蓝图中的SingleLiveEvent

BaseFragment

BaseFragment像这样监听来自ViewModel的导航命令。

override fun onActivityCreated(savedInstanceState: Bundle?) {
  super.onActivityCreated(savedInstanceState)
  vm?.navigationCommands?.observe { command ->
    when (command) {
      is NavigationCommand.To ->      
        findNavController().navigate(command.directions)
        …

ViewModel

BaseViewModel有这些辅助方法。

fun navigate(directions: NavDirections) {
  navigationCommands.postValue(NavigationCommand.To(directions))
}

然后从ViewModel导航真的很简单,就像这样。

navigate(CardListFragmentDirections.cardDetail(cardId))

如何在ViewModels中使用参数?

ViewModels通常需要导航参数来加载一些数据。我们的BaseFragment会像这样自动将参数传递给ViewModel。

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  if (savedInstanceState == null) {
    vm?.setArguments(arguments)
    vm?.loadData()
  }
}

然后,ViewModel可以像这样轻松地使用参数。

override fun loadData() {
  val cardId = CardDetailFragmentArgs.fromBundle(args).cardId
  load(cardsRepository.getCard(cardId)) {
  …

如何显示带有深度链接的登录界面?

官方文档没有足够详细地描述条件导航。它基本上建议在最终目的地处理条件性导航。例如,先到个人资料界面,再到登录界面,在用户登录时弹出。出于以下原因,我们不喜欢这种方法。

  • 登录屏幕应该表现得像另一个根目的地。当用户在登录屏幕上按下返回按钮时,应用程序应该关闭。用户不应该去前一个屏幕--因为前一个屏幕需要登录,所以用户最终会陷入一个无限循环。
  • 特别是深层链接,当用户没有登录时,目标Fragment根本就不应该被创建。创建Fragment需要一些资源,它开始进行一些网络调用,因为用户没有登录,所以会失败,等等。如果在所有这些发生之前就显示登录,那就更好了。

我们尝试在一个NavHostFragment中替换导航图,并为登录屏幕创建一个不同的根。但在配置改变后,这并不奏效。最后,我们决定使用一个不同的活动来登录。我们的应用程序不再是严格意义上的单一活动,但登录是一个独立的流程,在这种情况下是合理的。登录可以有它自己的导航图,它可以按照我们的要求工作--作为另一个导航根。

应用程序直接或通过MainActivity的深度链接启动。我们可以通过尽快完成MainActivity并显示LoginActivity来防止加载任何片段。

override fun onStart() {
  super.onStart()  
  if (sessionRepository.isLoggedOut()) {
    startActivity<LoginActivity>()
    finish()
  }
}

但是深层链接怎么办?幸运的是,我们可以将所有的intent参数传递给LoginActivity。

val intent = Intent(this, LoginActivity::class.java)
intent.putExtra("deepLinkExtras", this.intent.extras)
startActivity(intent)
finish()

而当用户登录后,我们可以将参数传回给MainActivity。

val intent = Intent(this, MainActivity::class.java)
intent.putExtras(this.intent.getBundleExtra("deepLinkExtras"))
startActivity(intent)
finish()

这样,我们总是在需要的时候显示登录,深层链接在两种情况下都能工作(用户注销或登录),而且后退按钮也能按预期工作。

我们对这种方法有一个问题。从深层链接登录后,根目标没有被添加到MainActivity的后堆中。我们通过将所有活动的启动模式改为 "singleTask "来解决这个问题。

如何导航到对话框?

我们的设计有很多底层的对话框。它们包含一些多步骤的流程,比如激活一个卡片。我们需要向对话框传递参数,所以把它们作为其他片段保留在导航图中是很方便的。有一个官方支持的功能请求,但它没有什么优先权。幸运的是,该库是相当可扩展的,其他导航目的地类型也是可能的。我们使用这个gist来实现DialogNavigator。

然后你可以像这样指定对话框片段。

<dialog
  android:id="@+id/nav_close_account"
  android:name="de.innoble.abx.closeacc.AccountCloseConfirmDialog"
    <argument
      android:name="iban"
      app:argType="string" />
</dialog>

安全参数和其他一切的工作都与普通的Fragments相同。

然而,有一个很大的区别--对话框不会被添加到后退栈中。当有多个对话框并按下返回按钮时,用户希望看到下面的片段,而不是前面的对话框。这有一个恼人的副作用:当从一个对话框导航时,你需要使用下面的片段的FragmentDirections,而不是对话框。这有点违反直觉,但在开发过程中很快就发现了(应用程序崩溃的异常是 "导航目的地对这个NavController未知")。

如何用一个结果导航回来?

类似于startActivityForResult()的东西会非常方便。Google推荐使用共享的ViewModel来实现这个功能,但是API并不直观。有一个关于这个的功能请求,但它的优先级很低。在官方支持到来之前,我们正在使用我们自己的解决方案。首先定义这个接口。

interface NavigationResult {
    fun onNavigationResult(result: Bundle)
}

在你想接收结果的片段中实现这个接口。从另一个Fragment发送的结果必须通过一个Activity进行路由。把这个方法添加到你的活动中。

fun navigateBackWithResult(result: Bundle) {
  val childFragmentManager = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)?.childFragmentManager
  var backStackListener: FragmentManager.OnBackStackChangedListener by Delegates.notNull()
  backStackListener = FragmentManager.OnBackStackChangedListener {
    (childFragmentManager?.fragments?.get(0) as NavigationResult).onNavigationResult(result)
    childFragmentManager.removeOnBackStackChangedListener(backStackListener)
  }
  childFragmentManager?.addOnBackStackChangedListener(backStackListener)
  navController().popBackStack()
}

注意,这个解决方案只适用于Fragment导航目的地。

TLDR;

  • 在一个多模块的项目中,将导航XML放入'common-android'模块。
  • 通过SingleLiveEvents的命令从ViewModels导航。把所有的模板放到BaseFragment/BaseViewModel中。
  • 使用Safe Args并将Fragment参数自动传递给ViewModel。
  • 为登录屏幕使用不同的活动。来回传递Intent extras以支持深度链接。
  • 这个gist的帮助下,在导航图中包含对话框。请注意,对话框不会被添加到后面的堆栈中。
  • 在官方支持到来之前,使用我们的解决方案,用一个结果进行导航。

www.deepl.com 翻译