从 Sunflower 开始学习优雅的 Jetpack 架构
Google 大法 NB!!!(破音)
前言
Jetpack是 Google 推出的一系列 Android 软件集合,"使您可以更轻松地开发出色的 Android 应用。这些组件可帮助您遵循最佳做法、让您摆脱编写样板代码的工作并简化复杂任务,以便您将精力集中放在所需的代码上"。
Sunflower则是 Google 用来演示如何使用 Jetpack 进行 Android 开发的 Demo,有着非常优雅的架构与十分简洁的代码,可以帮助我们很好地学习 Jetpack 以及 MVVM 思想。
本文主要是结合 Sunflower 中的示例代码,分析 Jetpack 架构中各部分的作用,以及他们如何巧妙的搭配使用,方便指导日后对 Jetpack 的使用。
本文中的大部分代码、示意图除非特殊注明外,皆来自 Google 的Sunflower 工程或其他互联网资源,根据篇幅需要做了部分精简,所有权益归原作者所有。
下图是Google Jetpack 官网对 Jetpack 的介绍图:
## 对 Sunflower 的整体分析下图是 Sunflower 架构的简单示意图:

可以看到,APP 的界面有我的花园、植物目录和植物介绍三部分,这三者的切换逻辑通过实现。
每个界面的XML中的布局信息(包括数据、事件(clickListener等),RecycleView的LayoutManager,Adapter等等)通过DataBinding与ViewModel中的可观察数据LiveData绑定在一起,只要数据库中的数据有更新,就会通过LiveData主动通知布局更新界面;同时DataBinding还通过与Adapter(这些继承自ListAdapter的 Adapter 实现了的作用)将ItemView的ViewModel与布局XML中绑定在一起,通过BindingAdapter对XML中的数据做预处理(加载 imgUrl 中的图片到 ImageView 等等)。
在中指定这些DataBinding与之间以及ViewModel与数据库之间的逻辑关系,这些数据与操作都受着的影响。
ViewModel的数据来源——Model在这里的实现是一个数据库。每个ViewModel有一个XXXViewModelFactory类,用来使用数据类XXXRepository类的实例创建对应的ViewModel。XXXViewModelFactory向Activity等屏蔽了ViewModel的具体实现。
XXXRepository类的出现时为了将ViewModel与数据的具体实现解耦合,这样ViewModel只需要关心他要的操作而不必关系数据来源的具体实现。在本例中,XXXRepository类对应封装了这数据库AppDatabase中对两个表的操作。
数据库使用实现,从底层开始依次分为表Entity,数据访问对象DAO和数据库DataBase三个层次。每个 DAO 对应一个包装类XXXRepository类供ViewModel使用。
@Entity    GardenPlanting    //表,定义了存储的数据项及其格式
@Dao    GardenPlantingDao    //数据访问对象,定义了例如插入数据、查询数据等操作
 GardenPlantingRepository    //对DAO的封装,将数据的的具体实现与ViewModel对数据的操作解耦
@Database     AppDatabase    //数据库,包括表和对表的操作SeedDatabaseWorker。具体实现分析
首先看一下**View**部分,Sunflower 只有简单的 3 个页面,全都是用Fragment实现,由Activity通过Navigation控制切换:
GardenActivity主页面,唯一的一个 ActivityGardenFragment我的花园 界面,会显示用户在植物目录中选择并种植的植物信息PlantListFragment植物目录 界面,所有的植物信息列表PlantDetailFragment植物介绍 界面,当在“我的花园”或“植物列表”选择了某个植物后,会进入该界面显示植物详细介绍
Navigation 控制界面切换
先看一下Navigation的定义:
Navigation 是 APP 设计中的关键部分,可以用来定义用户从不同的界面切换、进入和推出的交互逻辑。
和布局文件一样,我们可以在编译器的可视化界面中,直接预览、设计不同界面切换效果。他可以负责Fragment、Activity、Navigation graphs 与 subgraphs 以及Custom destination types,他们之间通过不同的action连接起来。
通过官方文档可知,Navigation可以和AppBar,ToolBar等组合起来控制 Fragment 显示,此外可以通过ViewModel在绑定到同一个Activity的Fragment之间共享数据,或者也可以通过Bundle或Safe Args在两个Fragment之间传递数据。
那么,在Sunflower中Navigation是怎么控制界面切换的呢?
首先,在res/navigation/目录下面新建一个嵌套导航图(Nested navigation graphs),定义各个界面之前的切换关系:
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    app:startDestination="@+id/garden_fragment">
//app:startDestination定义了在这个导航图中首次启动展示的界面
    <fragment
        android:id="@+id/garden_fragment"
        android:name="com.google.samples.apps.sunflower.GardenFragment"
        android:label="@string/my_garden_title"
        tools:layout="@layout/fragment_garden">
//action定义了在各个界面的切换关系
        <action
            android:id="@+id/action_garden_fragment_to_plant_detail_fragment"
            app:destination="@id/plant_detail_fragment"
            app:enterAnim="@anim/slide_in_right"//enterAnim等指定执行action时的动画
          .../>
    </fragment>
    <fragment
        ...>
//argument定义了在切换界面时需要带的参数,需要androidx.navigation.safeargs的支持,具体见参考资料-Android Jetpack-Navigation 使用中参数的传递
        <argument
            android:name="plantId"
            app:argType="string" />//参数类型小写
    </fragment>
</navigation>然后在 Activity 对应的 XML 中插入该导航:
<LinearLayout ...>
	<fragment
        android:id="@+id/garden_nav_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/nav_garden" />
</LinearLayout>之后就可以在 Activity 或者 Fragment 中获取该导航的实力,用来切换界面了:
//activity
val navController = Navigation.findNavController(this, R.id.garden_nav_fragment)
//fragment 或其他地方
val direction = GardenFragmentDirections//嵌套导航图中 Fragment 自动生成的类
.ActionGardenFragmentToPlantDetailFragment(plantId)
it.findNavController().navigate(direction)DataBinding 绑定布局和数据
Navigation 解决了不同的布局间交互的逻辑,DataBinding 则充当布局 View 和数据(ViewModel、LiveData)之间的桥梁,将二者联系起来。
从官网的表述中我们知道,DataBinding 使用在 XML 中声明的方式(而非编程的方式),将布局中的组件捆绑到 APP 中使用到的数据上,这样当数据更新时,布局也会随之自动更新。
DataBinding 在 XML 中的形式如下:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable
            name="viewmodel"
            type="com.myapp.data.ViewModel" />
    </data>
    <ConstraintLayout... /> <!-- UI layout's root element -->
</layout>需要注意的是原先的页面布局信息<ConstraintLayout... />包裹在<layout... />中,同时多了一个数据域<data... />,我们可以在其中定义一些变量<variable... />,并在布局中使用:
<TextView
    android:text="@{viewmodel.userName}" />除了常见的android:text,android:onClick等通用的属性可以直接绑定外,我们还可以通过自定义Binding adapters支持更多形式的属性绑定:
@BindingAdapter("app:goneUnless")
fun goneUnless(view: View, visible: Boolean) {
    view.visibility = if (visible) View.VISIBLE else View.GONE
}上面的代码就支持了app:goneUnless的解析,我们只要在 XML 中为组件加上这个属性就可以实现相应的效果:
<TextView
          android:text="@{viewmodel.userName}"
          app:goneUnless="@{viewmodel.isGone}"/>最后,我们需要在对应的 Activity 或 Fragment 中,用如下代码将布局与页面绑定到一起:
//setContentView(R.layout.activity_main)
val binding: ActivityMainBinding = DataBindingUtil.setContentView(
            this, R.layout.activity_main)
binding.viewmodel = ...这里的ActivityMainBinding类是DataBinding根据 XML 文件的名字自动替我们生成的,规律是XML文件名+Binding的驼峰命名。
在 Sunflower 中有类似的应用有很多处:
<?xml version="1.0" encoding="utf-8"?>
<!--
  ~ Copyright 2018 Google LLC ...
  -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
                name="hasPlantings"
                type="boolean" />
    </data>
    <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/garden_list"
                app:isGone="@{!hasPlantings}"
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
                tools:listitem="@layout/list_item_garden_planting"/>
    </FrameLayout>
</layout>ViewModel 管理数据与页面的交互
DataBinding通过标记的形式将数据和组件绑定,在这个过程中他使用的数据则是来自于ViewModel的。在页面Activity(或Fragment) 中,我们可以处理这两者之间的关系。
ViewModel是设计用来以一种可以感知生命周期(lifecycle)的方式存储和管理与 UI 相关的数据,它可以允许数据在诸如屏幕旋转的变化中存活下来,也就是说VideModule的数据生命周期可能要比他附着的Activity或Fragment的生命周期长。
同时,UI controller可以在Activity等不再需要数据时,自动调用ViewModel的onCleared()方法清除这些数据以避免内存泄漏。
下图是ViewModel和Activity的生命周期对比:
val viewModel = ViewModelProviders.of(this, factory).get(GardenPlantingListViewModel::class.java)
class PlantDetailViewModelFactory(args:Any) : ViewModelProvider.NewInstanceFactory() {...}还可以将ViewModel于LiveData结合,这样在Activity等地方对LiveData进行订阅后,当LiveData的值发生变化时Activity等可以及时得到通知,而做出相应变化。此外ViewModel与lifecycle的结合可以保证在Activity等生命周期结束后数据得到及时的清理。
Room 保存数据
Room 持久性库在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 强大功能的同时,能够流畅的访问数据库。——Android Developers
Room需要 3 个元素:
Database数据库,可以提供对表格的操作方法@DAO。是一个继承自RoomDatabase的抽象类。Entity表格,规定了每个表格可以保存的数据格式。是一个普通类。Dao数据访问结构(Data Access Object),定义了对表格@Entity中的数据的操作。是一个接口或者抽象类。
此外,还可以对@DAO进行进一步的封装得到一个XXXRepository类,ViewModel通过这个XXXRepository类来操作数据,从而将其与数据的具体实现解耦。
WorkManager 管理任务
WorkManager用来管理即时或定时任务,官方定义是在指定约束条件成熟时可靠的在后台执行对应的任务。
具体使用可以参考这个GIST。
和他相关的有下面几个关键类:
Worker定义要执行的任务内容WorkRequest代表一项单独的任务,明确具体要执行的任务内容(Worker)、任务的类型(WorkRequest.Builder 的子类,决定任务一次性还是重复的)以及任务执行的条件(Constraints,如联网、电池电量等等)- WorkManager 执行管理 WorkRequest,安排执行 Worker 中的工作内容。
 
