QMUI实战(二)—Activity 和 Fragment,咱们该选择谁?

在一开始, 官方只提供了 Activity 来做为 UI 界面的载体,所以咱们也别无选择,只能用它。而在 Android 3.0 后,Fragment 也面世了,它一开始是用于适配平板的,以邮件列表与详情的适配为例,手机端够小,所以开始展现列表,点击进入详情,而平板够大,则能够列表显示在左侧,详情显示在右侧,点击列表只是切换详情。对于这种适配场景,列表页和详情页必须在同一个 Activity 里了,而这即是我所知道的 Fragment 诞生的场景了。可是随着 Fragment 框架的不断完善, 单 ActivityFragment 的架构也被提出了,甚至如今官方的 navigation 库都是这种模式, 可见官方对 Fragment 是多么的青睐了。android

固然,如今咱们写 UI 也不仅是这两种选择了, Reative Native, Flutter,以及未来的 compose 为咱们写 UI 提供了不少不少的选择,同时咱们学习的东西也愈来愈多了。今天咱们只讨论一些 ActivityFragment 相关的话题,对于另外的几个,我只是期待 compose 时代的早点到来。缓存

Activity

提到 Activity,可能最容易犯的一个错误就是忘记在 AndroidManifest 里注册了吧,直到运行出错,才想起要去注册,而后再编译,时间就是这样没了的。对于 Activity 的使用,咱们须要关注生命周期、启动模式、setContentView 外,咱们还须要掌握的一个关键知识点就是 SaveState 了。bash

iOS 端、 Web 端都是不存在 SaveState 的,于是这几乎是 Android 独有的。由于 Android 最初的年代是比较看轻 View 的。 在 Android 眼里, View 是廉价的,数据、状态是宝贵的,View 是能够随时销毁,而后根据状态进行恢复的。例如在横竖屏旋转、Dark Mode 切换这些场景中,Android 就选择销毁掉当前 Activity,而后再从新建立,从新建立时会去 resource 里读当前配置下的资源,例如字体大小、颜色等等,这些资源都是能够根据不一样状态在不一样文件件里配置成不一样的值,于是咱们就不须要去写各类判断。可是问题来了,当 UI 显示出来后,大多数状况是有数据渲染或状态变动的,那么销毁重建后,咱们的数据该如何恢复呢? 这就要靠 SaveState 机制了:在销毁时,咱们把数据保存下来,而后在重建时,咱们再把数据恢复出来。架构

固然,你也能够选择不要让系统重建你 Activity,而是本身处理配置的变动。作法就是在 AndroidManifest 文件里为 ActivityconfigChanges 属性里加上你想要本身处理的配置。例如:框架

android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|screenLayout|uiMode"
复制代码

具体的配置项能够看官方文档。 例如 orientation 即是横竖屏旋转, uiMode 能够用于接收 Dark Mode 的切换。当这些配置好后。 咱们在 Activity 里能够经过重写 onConfigurationChanged 来接受配置的变动。ide

咱们说回 SaveState。 当发生配置变动、或者由于内存等缘由被系统杀死的 Activity, 会调用 onSaveInstanceState 来保存 UI 状态,默认状况下会遍历 View, 而后以 View 的 id 为键来收集 View 的状态。固然每一个 View 都是能够重写 onSaveInstanceState 来更改默认行为, 例如 RecyclerView/ListView 就阻止了子 View 状态的保存。那么这里就有一个问题了。 若是同一个 Activity 里出现了两个 id 相同的 View, 那会怎样呢? 那样就会发生状态的覆盖,状态恢复也会混乱。不要觉得这种事情不会发生,我来列举下有可能出现的场景,看看你有没有遇到呢?函数

  1. 如今界面愈来愈复杂了,同一个 Activity 里可能会使用 ViewPagerViewPager 的现象,而不少人起 id 也很随意,不会业务化,都叫 viewPager,而后就会发现,我本来选中的是第二个 tab,怎么屏幕一旋转,就变成第三个了?这就是由于 ViewPager 会保存当前选中的是哪一个 tab,多个相同 id 的 ViewPager, 在状态保存是后者覆盖前者,恢复过来后永远是最后一个 ViewPager 选中的状态了。
  2. 有许多人切换 Fragment 时, 使用的是 add 而不是 replace。这样就形成同一时刻可能有不少 Fragment 共存,每个 Fragment 在界面上的表现就是一个 View, 这样无形增长了系统布局、渲染的时间外,也是有可能状态保存于恢复出错的, 例如每一个 Fragment 都有一个 RecyclerView,而后它的 id 是 recyclerView, 这样界面上就有不少个同 id 的 RecyclerView 了,而 RecyclerView 在状态保存时会保存滚动位置的,那么当屏幕旋转后,你全部的 RecyclerView 状态都恢复成了最后一个保存的 RecyclerView 的了,就会出现滚动到了开始或者某个奇奇怪怪的位置了。
  3. RecyclerView 没成熟以前,咱们作横向的翻页滚动,基本都是用 ViewPager 作的(又是它),若是咱们 ViewPager 的每个 Pager 都采用相同的 xml, 那又可能出现这种问题了,假想一下,你每一个 Pager 都有一个进度条,结果屏幕一旋转,进度全都乱套了。 那么你们如今就知道缘由了。 RecyclerView 不会出现这种问题,由于它拦截了 SaveState 的派发,彻底由 Adapter 的数据决定 UI 的渲染。

在复杂的 UI 下,各类奇奇怪怪的状态问题老是容易出现,但问题的本质都是相同的,所以熟悉 UI 的人,在遇到这样的场景,会快速反应过来问题出如今哪里了,进而进行修复。而若是可以在写 UI 时就考虑到这种问题,起 id 名不随意,那就更妙了。布局

这其实也暴露了另一点,咱们在作自定义 View 时,是否有考虑这些问题,坦白点讲, QMUI 在这方面作得就不够好,咱们更多的强调自适应,而推荐用 configChange 来规避了 StateSave 的问题。性能

最后一点,若是咱们有大量的数据,这种 StateSave 的确定是不知足需求的,这个时候,Activity 提供了 onRetainNonConfigurationInstance 的方式来保存状态无关的数据,若是你使用 ViewModel, 你就根本不须要关系这些了, 在 Activity 销毁与重建后,使用的是同一个 ViewModel, 所以仍是早点用上 ViewModel 吧,它不止你表面上看到的那些东西。学习

Fragment

Fragment 虽然比 Activity 轻量,有不少特性,可是使用起来是比 Activity 复杂的, 相应的坑点也比 Activity 多。

第一点就是它的生命周期有两个,一个是 Fragment 的生命周期, 另一个就是 Fragment 管理的 View 的生命周期。为何会有两个呢? 前面已经说过了,Android 是轻 UI 的,那么从 FragmentA 切换到 FragmentB 时,就会去释放掉 FragmentA 管理的 View,从 FragmentB 返回到 FragmentA 时,再从新建立 View。 因此 Fragment.onCreateView(三参数)Fragment.onDestoryView()会被屡次调用,这或许是不少人都会疑惑的点。

咱们在切换 Fragment 的时候就会发生 View 的销毁,那么一样须要作 StateSave 了,所以,FragmentonSaveInstanceStateonViewStateRestored 基本上是与 View 的生命周期挂钩的,而不只仅是屏幕旋转、Dark Mode 切换时才调用了。固然,通常状况下,在 Activity 销毁与重建时,Fragment 也会销毁与重建。但若是你调用了 Fragment.setRetainInstance(true) ,它就会走 ActivityonRetainNonConfigurationInstance,而不会销毁重建了,可是 View 的生命周期依旧会走销毁重建。

Fragment 存在 View 这个生命周期的另一点就是咱们不能像在 Activity里同样随意操做 UI。 若是你在 onDestoryView 以后操做了 UI,那么当 View 重建后,以前的操做就白费了。这个时候 ViewModelLiveData 就显得特别重要了, 它可让你在数据在正确的生命周期里渲染到 UI 上。可是须要注意的是咱们对 LifeCircleOwner 的选择, Fragment 是有两个 LifeCircleOwner 的,若是与 UI 相关, 咱们应该选用 viewLifeCircleOwner, 而且应该在 onActivityCreated 里调用 LiveDataobserve 方法。

Fragment 存在另一个问题就是转场动画的问题了。若是在转场动画过程当中发生了数据渲染,那么就会产生动画卡顿现象,由于数据渲染形成了 UI 从新 layout,而 Activity 是 window 动画,不受这个影响。对比起来,可能 Activity 的动画更加流畅了。 而咱们也没办法解决 Fragment 的这个问题,只能避免在动画过程当中进行数据渲染了,所以 QMUI 提供了 runAfterAnimation 方法。

Fragment 是用 FragmentManager 来管理的,而后经过 FragmentTransaction 来实现 Fragment 的添加、删除与切换等。 固然 Fragment 提供了 childFragmentManager,这使得咱们能够一层一层的添加 Child Fragment。 而当 Activity 切换到不一样生命周期状态时,会沿着 FragmentManager 派发给全部的 Fragment。 这是颇有用的,例如在作 ViewPagerPager 时,咱们能够采用自定义 View, 也能够采用 Fragment。可是当 Pager 须要在 onResumeonPause 等时机作一些事情时,若是用自定义 View 时,那就须要咱们写一堆的代码来控制这一切,而采用 Fragment 时,则能够方便的运用这个特性来实现。所以官方也提供了 FragmentPagerAdapter。 若是咱们有一些公用的业务 UI 大组件, 不妨也经过它来封装,方便生命周期的管理。

QMUI 为 Activity 和 Fragment 添加的功能

QMUI 的 arch 库,为 ActivityFragment 添加了一些新的功能。

首先就是手势返回,这个在 iOS 上是自带的,而 Android 却须要开发者本身去实现。一个主要的缘由就是 Android 是主张 View 能够随时建立与销毁。 以 Fragment 为例,当咱们从 FragmentA 切换到 FragmentB 后, FragmentA 的 View 已经销毁了,这个时候手势返回时确定没法获取到以前那个 View 的,所以默认是没法实现相似 iOS 那种手势返回的。但产品们就是但愿看到这种手势返回效果, 因此咱们也必须作,对于 Fragment 而言, QMUI 就是在 QMUI 层用成员变量保存了 Fragment 建立的 View, 而后在下次建立的时候直接传入缓存的 View, 这直接打破了本来的官方的逻辑,也就会引入新的翔, 好比咱们可能会在 onActivityCreated 为 TopBar 添加子 View,但由于 View 缓存后,就会出现重复添加的问题,所以 QMUI 也提供了 onViewCreated(一参数) 规避这个问题。Fragment 的手势返回大量的运用了发射来更改 FragmentManager 里记录的数据信息,这是基于阅读 FragmentManager 源码后找到的插入点,通常而言,这种实现是有版本兼容问题的:若是官方更新了实现,那个人反射也就会失败,但目前 Fragment 框架比较稳定了,也不会轻易去改核心功能的,因此影响其实还好。

相比于 Fragment 的手势返回。 Activity 的手势返回实现会简单不少,可是会存在一些没法解决的问题。目前主流的有三种实现方式:

  1. 手势返回将 Activity 透明掉, 这样就能够看到背后的 Activity 了。可是调用透明 Activity 的方法是反射,并且比较耗时,而且背后的 Activity 没办法移动, 作不了视差。
  2. 手势返回时将前一个 Activity 的 View 取下来,而后添加到当前 Activity 的 View 的下层。 手势返回后再将 View 放回去。这个方案存在 ViewWindow 的添加与移除,并且没法处理背后的 Activity有显示 Dialog 的场景。
  3. 手势返回时将前一个 Activity 的 View 以及 Dialog 的 View 绘制到当前 Activity 的背后,这个是 QMUI 提供的方案,性能多是最优的, 由于只是多了绘制,但问题是若是前一个 ActivityonPause 以后更改了 View 的显示,那就可能在手势返回时有错误的表现(例如 FaceBook 提供的 DraweeView),另外它对 SurfaceView 等支持也不太好,对于这种场景,直接禁用掉手势返回比较好,其它的手势返回其实也不能很好的支持 SurfaceView 这些。

当 Android 10 出来后,它提供了新的 Navigation Gesture,不少国产机已经开始这么作了,这这种场景下,系统的手势返回会优先于咱们的手势返回,这个时候这套手势返回就有点鸡肋了(尽管它的交互必系统的更优雅,更贴合 iOS)。

QMUI 提供的另一些功能就只是一些 util 性质的函数了。例如 startFragmentstartFragmentAndDestroyCurrent等,后一个是一个特殊化的实现,对于官方设计而言,他们以为应该遵循用户行为,我从 A 点击进入 B,而后进入 C, 那么我返回是就应该从 C 返回 B,再返回到 A。但产品需求可能不是这样,例如我把某篇文章分享给某个用户,那么我要先打开用户列表,选择某个用户,再进入用户的聊天界面。当我从聊天界面返回时,应该是返回的文章界面,而不是中间的用户列表界面。而咱们的 Fragment 并无 Activityfinish 方法来结束本身,这就是 startFragmentAndDestroyCurrent 存在的价值了。

另外须要提一下的是 FragmentManager.popBackStack() 这个方法,它的意思是将当前 Fragment 从堆栈中移除,咱们的返回就是经过它作的,可是它却不是任什么时候候都能调用的,通常在用户主动的点击行为下调用是没有问题的,但若是你想在 Fragment.onCreate 里调用,是不行的,即便是在 onResume 里也是不行的,对于上面那个分享的需求场景,若是你想以返回到用户列表时马上执行 popBackStack 来取代 startFragmentAndDestroyCurrent,那是会失败的,若是有兴趣,你能够本身试试,官方的 Fragment 会直接 crash 掉,而 QMUIFragment 则会忽略此次执行。其缘由是由于 FragmentManager 里的状态机要在 onResume 以后才赋值当前状态。 这是一个很常见的重入报错的问题,我简单写个相似的例子,看完后你估计就能猜到是怎么回事了:

interface Observer{
    fun doSomething()
}

class Observable{
    private val observers = arrayListOf<Observer>()
    
    fun addObserver(observer: Observer){
        observers.add(observer)
    }
    
    fun removeObserver(observer: Observer){
        observers.remove(observer)
    }
    
    fun dispatch(){
        val count = observers.size
        for(i in 0 until count){
            observers[i].doSomething()
        }
    }
}
复制代码

上面这个代码很简单,就是观察者模式的运用,你们可否看出其中错误呢?

假设个人调用是这样:

val observable = Observable()
val secondObserver = object : Observer {
    override fun doSomething() {
        //....
    }
}
val firstObserver = object : Observer {
    override fun doSomething() {
        observable.removeObserver(secondObserver)
    }
}
observable.addObserver(firstObserver)
observable.addObserver(secondObserver)
observable.dispatch()
复制代码

若是运行这段代码,你会发现报 IndexOutOfBoundsException,这是由于咱们在第一个 Observer 将第二个 Observer 移除了,从而形成 Observable.dispatch 里的 for 循环的 count 错误。 咱们的 Fragment.onResume 也处于这种阶段,若是在这里面去移除 Fragment,可能致使 FragmentManager 里的某些变量值出错。固然, FragmentManager 里是有保护的,是不容许你的某些操做的,你操做了就抛出错误,终止整个执行。

QMUI arch 框架目前也在尝试引入一些注解来简化代码,例如 FirstFragment 注解,例如 LatestVisitRecord 注解。在以后的实战过程当中,我会详细介绍这些注解使用的场景。

#总结 今天主要介绍了一些 ActivityFragment 的知识。知识点是比较零散的,须要咱们在使用过程当中不断地学习与掌握。并且咱们不是作选择题,而是二者都要会,在适合的场景引入 Fragment,能够极大的减小工做量。例如在插件化场景中,咱们就不须要解决 Activity 的注册问题了。 而 SaveStateViewModel、手势返回等都埋藏了不少细节的知识点,咱们要想熟练的驾驭它,就须要熟悉它的各类坑点以及坑点产生的缘由。新手每每写点 UI 就去看看效果,这是对各个控件极其不熟悉的缘由。若是能作到写半天的代码,一次编译,结果效果全是本身想要的,那编写 UI 的能力就是真正的上了一个台阶了。

GankWithQmui 会采用单 Activity 多 Fragment 的架构,这是我比较喜欢的架构。下一次咱们就开始构建咱们的第一个 Fragment 了。

下期博文:QMUI实战(三)——你是如何启动你的第一个 Fragment 的?