Android Jetpack 架构组件最佳实践

Android Jetpack介绍

Android Jetpack 是一套组件、工具和指导,可以帮助您快速构建出色的 Android 应用。

  • Google在17年的I/O大会上推出了架构组件(Architecture Component)。

  • 随后在18年I/O大会上发布了 Android Jetpack,Jetpack 是Android开发组件工具集,旨在帮助我们轻松构建更稳定、更健壮、以及更可维护的应用程序。

  • 紧接着Google推出AndroidX,将许多Google认为是正确的方案和实践集中起来了。

    • AndroidX 是对support library的重大改进。

    • AndroidX中的所有软件包名都以字符串androidx.开头,位于一致的命名空间中。

    • 与support支持库不同,AndroidX中各个组件可单独维护和更新。

    • 所有新的支持库开发都将在AndroidX库中进行。

目前很多组件库新版本都迁移到了androidx。比如Lifecycle2.0.0+、Paging2.0.0+、ViewPager2等,非官方库也积极响应,比如lottie2.8.0+版本之后就使用了androidx实现。由此可以得出,官方在已有组件新版本实现和全新组件的开发都将只支持androidx,所以尽快将自己的项目迁移到androidx吧。

  • Jetpack结构图(2018年版本),目前图中大部分组件都推出了稳定的release版本。
android-jetpack.png

Jetpack主要分为4个部分,基础、架构、行为、界面。从图中得知,Jetpack并不全是些新东西,只要是能够帮助开发者更好更方便地构建应用程序的组件,Google都将其归纳入了Jetpack,可以看出Google对jetpack很重视,对开发者很上心。

刚刚结束的Google I/O 2019大会上,Jetpack又迎来了新组件CameraXSavedStateViewModelJetpack Compose等等,提出Kotlin first ,并强调大部分新的Jetpack Apis和功能将会优先提供 Kotlin 版本。参考文章

每个 Jetpack 组件均可单独采用,并且它们可以流畅地协作。Android开发请一定关注和使用Jetpack+Kotlin。

本文重点是在架构组件的使用上,我们先来看看官方推荐的架构实现。

官方推荐架构

官方架构图

使用此架构能带来什么好处?

  • UI和业务逻辑解耦。
  • 有效避免生命周期组件内存泄漏。
  • 提高模块可测试性。
  • 提高应用稳定性,有效降低以下异常发生概率。
    • Can not perform this action after onSaveInstanceState
    • WindowManager$BadTokenException, is your activity running?
    • OOM 、 NullPointerException
    • ...

测试每个组件

  • 界面和交互:使用 Android 界面插桩测试。基于此架构只需mock 一个ViewModel即可完成界面测试。

  • ViewModel:使用 JUnit 测试。只需mock一个类,即 Repository

  • Repository:使用 JUnit 测试。只需mock两个类,XxxDao,XxxService;由于XxxDao,XxxService都是接口,还可以创建虚拟实现来完成复杂测试用例。

  • XxxDao:可以使用插桩测试来测试 DAO 类。这里注意对于每个测试,都请创建内存中数据库以确保测试没有任何副作用(例如更改磁盘上的数据库文件)。

  • XxxService:就Retrofit而言可以使用MockWebServer模拟本地服务器。

评价一个架构好不好主要看三点:稳定性、易维护、可测试程度

提到架构组件库,不得不说的是Lifecycle库。文章后面部分就分别从该库中的Lifecycle、ViewModel、LiveData这三个类来简要分析其实现原理以及使用建议。

Lifecycle

Lifecycle是一个类,它包含组件(Activity或Fragment)生命周期状态的信息,并允许其他对象观察此状态。

那lifecycle是如何跟踪组件生命周期的呢?

lifecycle-states

上图中states表示组件状态,events表示组件生命周期事件。其实Lifecycle代码内部使用了两个主要枚举(EventState)来跟踪其关联组件的生命周期状态。

  • 其中枚举Event的值和Activity或Fragment组件的生命周期回调事件一一对应。
  • 而枚举State则表示被跟踪组件的当前状态,其中 STARTEDRESUMED 为活跃状态,配合LiveData使用时,只有组件处于活跃状态才能接受到数据更新通知。

实践示例:工具类LifecycleHandler,一个具有生命周期感知的Handler。

LifecycleOwner和LifecycleRegistry

  • LifecycleOwner是一个单一的方法接口,表示该类具有生命周期。support包从26.1.0版本开始,Fragment和Activity默认实现了该接口,这样直接和LiveData使用就能获取组件的生命周期感知能力。
  • LifecycleRegistry: Lifecycle接口的实现类,协助组件处理生命周期,可处理多个观察者。如果你想自定义LifecyclerOwner请参考support包中Fragment和Activity实现。

ViewModel

ViewModel 是用来保存应用UI数据的类,它会在配置变更(Configuration Change)后继续存在。

先看看官方给出的ViewModel生命周期图解

viewmodel-lifecycle.png

关于ViewModel的生命周期就一句话:在Activity、Fragment等组件整个生命周期过程中,ViewModel的实例有且只有一个。

这样设计好处在哪呢?
  • 可用ViewModel存储数据,它能安全度过手机旋转等配置变更场景。

  • ViewModel能很好的实现多个Fragment之间的数据共享。

如果界面和业务都比较复杂,ViewModel会不会爆掉?对于这种场景,官方也给出了解决思路:单一责任原则。

viewmodel_duty

上图为官方中文视频截图,从单一责任原则考虑提出了实现建议。

  • Actvity或Fragment只显示UI和接收互动。

  • 为避免ViewModel臃肿,可创建presenter处理UI数据。

  • Repository 数据源操作入口。(便于单元测试)

  • 还可配合其它架构组件使用。

关于ViewModel的最佳实践

  • 如何时候都不要将Context传入ViewModel。

  • 如果要在ViewModel中使用Application实例,请使用AndroidViewModel类。

  • ViewModel+LiveData+Databinding 可构建反应式UI。(请查看文末提供的参考资料)

  • ViewModel与onSaveInstanceState要配合使用。

    ViewModel onSaveInstanceState
    能度过配置变更 能度过配置变更和进程关闭
    能存储大量数据 只可存储少量数据
    xx 必须序列化

    其实ViewModel和onSaveInstanceState是相辅相成的,当进程被关闭时,ViewModel会被销毁,而onSaveInstanceState不会受影响,所以可用onSaveInstanceState存储少量关键数据(如userId),并在该场景恢复时用来加载页面数据。

在使用ViewModel时,如果页面仅仅是简单的展示数据没什么交互,一个LiveData就能轻松搞定,但实际情况是大多数页面复杂且交互多,就想着怎样更好的处理ViewModel和View之间的通信,直到看到了这篇文章,参考之后得出了下图实现。

ViewModel和View之间通信模型
Communication between ViewModel and View
  • UserProfileActivity引用UserViewModel,可观察其提供的UserLiveData、StatusLiveData、PageStateLiveData数据源变更分别处理数据显示、页面loading、跳转等UI操作。
  • 注意Activity和ViewModel之间是单向引用。为避免内存泄漏,ViewModel不能持有任何Context引用。

该模型如何响应用户事件的?比如点击某个按钮,需要提交信息给server,并在成功响应后刷新UI,这个过程中ViewModel和View是如何通信的?这里简单描述下该过程,首先是Activity将更新事件传递给ViewModel,ViewModel有将其委托给Presenter处理,Presenter将处理状态和结果,通过给图中指定的LiveData设置数据,liveData就能将新数据回调给Activity,这样页面上所有操作就都能通过数据来驱动了。

另外,如果Activity中存在多个View组件(Fragment),这些组件可直接依赖Activity的ViewModel进行交互。

LiveData

LiveData是一个具有生命周期感知特性的可观察的数据保持类。

  • LiveData只通知活跃状态( STARTED or RESUMED )的Observer更新,并在 DESTROYED状态时自动移除Observers,来避免内存泄漏。
  • 始终保持最新数据。举例:1.退后台的Activity在返回前台后会立即收到最新数据。2. 配置变更导致Activity重建后也会立即收到最新数据。
  • 共享资源。单利模式共享同一个LiveData。
  • SingleLiveEvent 只通知一次事件,适用于Navigation event、SnackBar等等场景。
    参考文章: SingleLiveEvent or EventWrapper

LiveData、MutableLiveData、MediatorLiveData三者关系?

  • 继承关系:MediatorLiveData -> MutableLiveData -> LiveData。 所以MediatorLiveData功能最强大。
  • LiveData 是一个具有生命周期感知的可观察的数据保持类。
  • MutableLiveData 在LiveData基础上打开了修改Value的方法权限。
  • MediatorLiveData 可管理多个LiveData。

Transformations

用过RxJava的朋友都知道,它可以很方便地在Observable之间转换。LiveData也提供了类似的功能。

  • map : 将一种数据类型的LiveData<A> 转换为另一种类型的 LiveData<B>

    // 示例代码:观察将被转换LiveData<User>,待其数据源变更后转换为LiveData<String>并通知订阅者。
    LiveData<User> userLiveData = ...;
      LiveData<String> userName = Transformations.map(userLiveData, user -> {
          user.name + " " + user.lastName
    });
    
  • switchMap : 和map类似。差别在于triggerLiveData变更后,会触发和等待另外一个LiveData获取数据。

    // 示例代码:将addressInputLiveData转换为postalCodeLiveData.
    class MyViewModel extends ViewModel {
      private final PostalCodeRepository repository;
      private final MutableLiveData<String> addressInput = new MutableLiveData();
      public final LiveData<String> postalCode =
              Transformations.switchMap(addressInput, (address) -> {
                  return repository.getPostCode(address);
               });
    
      public MyViewModel(PostalCodeRepository repository) {
        this.repository = repository
      }
    
     // addressInputLiveData变更时触发repository.getPostCode,
     // 待其回去成功后,再将数据设置给postalCodeLiveData。
      private void setInput(String address) {
              addressInput.setValue(address);
          }
    }
    
    

以上为使用示例代码,其内部使用的MediatorLiveData实现,较简单,感兴趣的朋友请自行查阅源码。

几个问题

LifecycleOwner组件是如何与liveData通信的?

  • SupportActivity 通过添加一个空的ReportFragment来处理生命周期状态变更回调;Fragment则在自身生命周期函数中处理。
  • LifecycleOwner组件,通过LifecycleRegistry类中handleLifecycleEvent -> dispatchEvent方法与liveData通信,从而使liveData具有感知组件生命周期的能力
  • 组件销毁时,LifecycleRegistry会通知liveData移除observer。

ViewModel如何做到一直在内存中,直到Activity销毁或Fragment被移除时才被清除的?

1.x.x版本实现

  • Activity或Fragment会添加一个空的HolderFragment,而ViewModelStore实例被HolderFragment持有,所以就保证了整个生命周期中ViewModelStore实例始终唯一,也就保证了其缓存的ViewModel实例会一直存在直到组件销毁(在onDestroy中会调用ViewModelStore.clear()方法清除其缓存的ViewModel实例)。
  • 由于这个HolderFragment设置了setRetainInstance(true), 这样在Activity重建时它不会执行onDestroy回调,这就是它能度过配置变更的原因

2.x.x版本-对应androidx-fragment-v1.0.0

  • Activity
    • 缓存:在onRetainNonConfigurationInstance()回调方法中将ViewModelStore实例缓存到NonConfigurationInstances中。
    • 恢复:在onCreate中通过getLastNonConfigurationInstance()获取重建前的状态并回复ViewModelStore。
  • Fragment
    • 缓存:在FragmentActivity.onSaveInstanceState -> FragmentManager.saveAllState -> FragmentManager.saveNonConfig方法中,将ViewModelStore实例缓存到了FragmentManagerNonConfig中,最终通过FragmentActivity将其缓存到NonConfigurationInstances中
    • 恢复:方法调用栈FragmentActivity.onCreate -> FragmentManager.restoreAllState(arg1, nonConfig) -> FragmentState.instantiate(x,x,x,nonConfig, viewModelStore);其中在FragmentState.instantiate(x,x,x,nonConfig, viewModelStore)方法会创建一个新的Fragment并将ViewModelStore变量赋值。

以上结论是我分别从lifecycle库1.x和2.x版本源码分析后得出。关于ViewModel的生命周期实现原理,各个版本实现略有不同,其中lifecycle2.x版本已改用Androidx实现,所以ViewModel的缓存实现还和androidx组件版本有关系。感兴趣的朋友,请自行查阅源码。

当Fragment被detach后再attach回来,会导致添加多个Observer?

  • 分析出现的原因

    由于Fragment默认实现是在onDestroy才通知liveData 移除observers,而我们每次在onCreateView都会add新的observer实例,这样就会导致数据更新时,LiveData会同时通知多个Observer,界面就会快速刷新多次。

  • 解决方案

    • 当你在onActivityCreated方法中添加LiveData.observer(LifecycleOwner owner, Observer<T> observer)时,第一个参数使用Fragment.getViewLifecycleOwner()方法返回值。(如果你没有找到该方法,请更新你依赖的support包版本,Google已在新版本中提供该方法)
    • LiveData.observer(LifecycleOwner owner, Observer<T> observer)放在onCreate回调中,在Fragment显示时手动触发数据刷新,当然最好还是更新support版本来解决。

总结

Android Jetpack是一套组件开发工具集,旨在帮助我们轻松构建更稳定、更健壮、以及更可维护的应用程序。对于Google而言,推出Jetpack可以更好的管理和维护组件库;对开发者而言,使用Jetpack可以快速开发出高质量应用,也能看到官方在不同技术方案上的选择,以及新技术发展方向。从目前Jetpack组件布局来看,AndroidX、kotlin是需要我们使用和掌握的。

本文后半部分介绍了架构组件中lifecycle库的一些原理和最佳实践,但还不够全面深入,后面我会一一从源码角度分析各种组件实现原理,敬请关注。

既然是最佳实践,怎么能没有代码,这里分享下作者使用架构组件实现的项目代码,重点关注wanandroid模块,其实现使用到了ViewModel+LiveData+Lifecycle+Room,按照官方推荐的架构实现,并完成各个组件独立的单元测试。

image.png

限于作者水平有限,文中定有错误和疏漏之处,恳请指出,与君共勉;若有不明白之处,欢迎随时评论交流。

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容