Android 开发者指南片段导航模式
已发表: 2022-03-11多年来,我在 Android 中看到了许多不同的导航模式实现。 一些应用程序仅使用活动,而其他活动与片段和/或自定义视图混合。
我最喜欢的导航模式实现之一是基于“One-Activity-Multiple-Fragments”理念,或者简称为 Fragment Navigation Pattern,其中应用程序中的每个屏幕都是一个全屏 Fragment,所有或大部分这些 Fragment 都包含在一项活动。
这种方法不仅简化了导航的实现方式,而且具有更好的性能,从而提供更好的用户体验。
在这篇文章中,我们将看看Android中一些常见的导航模式实现,然后介绍基于Fragment的导航模式,并与其他导航模式进行比较和对比。 实现此模式的演示应用程序已上传到 GitHub。
活动世界
仅使用活动的典型 Android 应用程序被组织成树状结构(更准确地说是有向图),其中根活动由启动器启动。 当您在应用程序中导航时,操作系统会维护一个活动回栈。
一个简单的例子如下图所示:
Activity A1 是我们应用程序的入口点(例如,它表示启动屏幕或主菜单),用户可以从中导航到 A2 或 A3。 当您需要在活动之间进行通信时,您可以使用startActivityForResult()或者您可以在它们之间共享一个全局可访问的业务逻辑对象。
当您需要添加新活动时,您需要执行以下步骤:
- 定义新活动
- 在AndroidManifest.xml中注册
- 用另一个活动的startActivity()打开它
当然,这个导航图是一种相当简单的方法。 当您需要操作后台堆栈或必须多次重用相同的活动时,它可能会变得非常复杂,例如,当您希望通过一些教程屏幕导航用户但实际上每个屏幕都使用相同的活动作为根据。
幸运的是,我们有用于它的工具,称为任务和一些用于正确返回堆栈导航的指南。
然后,API 级别 11 出现了片段……
碎片世界
Android 在 Android 3.0(API 级别 11)中引入了 Fragment,主要是为了在平板电脑等大屏幕上支持更动态和灵活的 UI 设计。 因为平板电脑的屏幕比手机大得多,所以有更多的空间来组合和交换 UI 组件。 片段允许这样的设计,而无需您管理对视图层次结构的复杂更改。 通过将 Activity 的布局划分为片段,您可以在运行时修改 Activity 的外观,并将这些更改保存在由 Activity 管理的后退堆栈中。 – 引用自 Google 的片段 API 指南。
这个新玩具允许开发人员构建多窗格 UI 并在其他活动中重用组件。 一些开发人员喜欢这一点,而其他开发人员则不喜欢。 是否使用片段是一个流行的争论,但我想每个人都会同意片段带来了额外的复杂性,开发人员确实需要了解它们才能正确使用它们。
Android中的全屏片段噩梦
我开始看到越来越多的示例,其中片段不仅代表屏幕的一部分,而且实际上整个屏幕是包含在活动中的片段。 有一次我什至看到一个设计,其中每个活动都只有一个全屏片段,仅此而已,而这些活动存在的唯一原因就是托管这些片段。 除了明显的设计缺陷之外,这种方法还有另一个问题。 从下面看一下图表:
A1如何与F1通信? 好吧,A1 完全控制了 F1,因为它创建了 F1。 A1 可以传递一个包,例如,在创建 F1 时,也可以调用它的公共方法。 F1如何与A1通信? 好吧,这更复杂,但可以通过 A1 订阅 F1 并且 F1 通知 A1 的回调/观察者模式来解决。
但是 A1 和 A2 如何相互通信呢? 这已经涵盖了,例如通过startActivityForResult() 。
而现在真正的问题来了:F1 和 F2 如何相互通信? 即使在这种情况下,我们也可以拥有一个全局可用的业务逻辑组件,因此它可以用于传递数据。 但这并不总能带来优雅的设计。 如果 F2 需要以更直接的方式将一些数据传递给 F1 怎么办? 好吧,使用回调模式,F2 可以通知 A2,然后 A2 以结果结束,该结果由通知 F1 的 A1 捕获。
这种方法需要大量样板代码,很快就会成为错误、痛苦和愤怒的根源。
如果我们可以摆脱所有的活动并只保留其中一个来保留其余的片段怎么办?
片段导航模式
多年来,我开始在我的大多数应用程序中使用“One-Activity-Multiple-Fragments”模式,并且我仍然使用它。 有很多关于这种方法的讨论,例如这里和这里。 然而,我错过的是一个具体的例子,我可以看到并测试自己。
让我们看一下下面的图表:
现在我们只有一个容器活动,并且我们有多个片段,它们又具有树状结构。 它们之间的导航由FragmentManager处理,它有自己的后台堆栈。
请注意,现在我们没有startActivityForResult()但我们可以实现回调/观察者模式。 让我们看看这种方法的一些优点和缺点:
优点:
1.更干净、更易维护的AndroidManifest.xml
现在我们只有一个 Activity,我们不再需要在每次添加新屏幕时更新清单。 与活动不同,我们不必声明片段。
这似乎是一件小事,但对于有 50 多个活动的大型应用程序,这可以显着提高AndroidManifest.xml文件的可读性。
查看具有多个屏幕的示例应用程序的清单文件。 清单文件仍然非常简单。
<?xml version="1.0" encoding="utf-8"?> package="com.exarlabs.android.fragmentnavigationdemo.ui" > <application android:name= ".FragmentNavigationDemoApplication" android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" android:theme="@style/AppTheme"> <activity android:name="com.exarlabs.android.fragmentnavigationdemo.ui.MainActivity" android:label="@string/app_name" android:screenOrientation="portrait" android:theme="@style/AppTheme.NoActionBar" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
2. 集中导航管理
在我的代码示例中,您将看到我使用NavigationManager ,在我的例子中它被注入到每个片段中。 该管理器可用作日志记录、回栈管理等的集中位置,因此导航行为与其他业务逻辑分离,并且不会分散在不同屏幕的实现中。

让我们想象一种情况,我们想要启动一个屏幕,用户可以在其中从人员列表中选择一些项目。 您还想传递一些过滤参数,例如年龄、职业和性别。
如果是活动,你会写:
Intent intent = new Intent(); intent.putExtra("age", 40); intent.putExtra("occupation", "developer"); intent.putExtra("gender", "female"); startActivityForResult(intent, 100);
然后你必须在下面的某个地方定义onActivityResult并处理结果。
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); }
我个人对这种方法的问题是这些参数是“额外的”并且它们不是强制性的,所以我必须确保接收活动在缺少额外的情况下处理所有不同的情况。 稍后,当进行一些重构并且不再需要例如“年龄”额外内容时,我必须在开始此活动的代码中的所有位置进行搜索,并确保所有额外内容都是正确的。
此外,如果结果(人员列表)以 _List 的形式到达不是更好吗
在基于片段的导航的情况下,一切都更加简单。 您所要做的就是在NavigationManager中编写一个名为startPersonSelectorFragment()的方法,其中包含必要的参数和回调实现。
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", new PersonSelectorFragment.OnPersonSelectedListener() { @Override public boolean onPersonsSelected(List<Person> selection) { [do something] return false; } });
或使用 RetroLambda
mNavigationManager.startPersonSelectorFragment(40, "developer", "female", selection -> [do something]);
3. 更好的屏幕间交流方式
在活动之间,我们只能共享一个可以保存原语或序列化数据的 Bundle。 现在有了片段,我们可以实现一个回调模式,例如,F1 可以监听 F2 传递任意对象。 请查看前面示例的回调实现,它返回一个 _List
4.构建片段比构建活动更便宜
当您使用具有例如 5 个菜单项的抽屉时,这一点变得很明显,并且在每个页面上都应该再次显示抽屉。
在纯活动导航的情况下,每个页面都应该膨胀并初始化抽屉,这当然是昂贵的。
在下图中,您可以看到几个根片段(FR*),它们是可以直接从抽屉中访问的全屏片段,并且只有在显示这些片段时才能访问抽屉。 图中虚线右侧的所有内容都是任意导航方案的示例。
由于容器活动包含抽屉,我们只有一个抽屉实例,因此在抽屉应该可见的每个导航步骤中,您不必再次膨胀和初始化它。 仍然不相信所有这些是如何工作的? 看看我的示例应用程序,它演示了抽屉的使用。
缺点
我最大的恐惧一直是,如果我在项目中使用基于片段的导航模式,在某个地方我会遇到一个无法预料的问题,围绕片段增加的复杂性、3rd 方库和不同的操作系统版本很难解决。 如果我必须重构到目前为止所做的一切怎么办?
事实上,我必须解决嵌套片段的问题,第 3 方库也使用片段,例如 ShinobiControls、ViewPagers 和 FragmentStatePagerAdapters。
我必须承认,获得足够的片段经验来解决这些问题是一个相当漫长的过程。 但在每种情况下,问题都不是哲学不好,而是我对片段的理解不够好。 也许如果你比我更了解 Fragments,你甚至不会遇到这些问题。
我现在可以提到的唯一缺点是,我们仍然会遇到难以解决的问题,因为没有成熟的库可以展示基于片段导航的复杂应用程序的所有复杂场景。
结论
在本文中,我们看到了在 Android 应用程序中实现导航的另一种方法。 我们将它与使用活动的传统导航理念进行了比较,我们已经看到了使用它优于传统方法的一些充分理由。
如果您还没有,请查看上传到 GitHub 实现的演示应用程序。 随意分叉或贡献更好的例子,这将更好地展示它的用法。