从 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 中的工作内容。