Android JetPack之ViewModel
谷歌在今年的I/O大会上推出了Jetpack的概念,意图在于统一框架与UI组件。所以我也将项目架构往这一概念上靠齐。
Jetpack
今天我们来研究架构层最重要的两个类LiveData和ViewModel之一的ViewModel
本文涉及到的代码已上传到Github,欢迎star、fork
ViewModel简介
ViewModel是以关联生命周期的方式来存储和管理UI相关的数据的类,即使configuration发生改变(比如旋转屏幕),数据仍然可以存在不会销毁
ViewModel的功能
引入ViewModel的原因有以下几点:
- Activity或Fragment这类应用组件都有自己的生命周期,他们的生命周期都是被Framework所管理。Framework可能会根据用户的一些操作以及设备的状态对Activity或Fragment进行销毁和重建。作为开发者,这些行为我们是无法干预的。伴随着Activity或Fragment的销毁和重建,它们当中的数据也会随着一起销毁和重建。对于一些简单的数据,Activity可以使用onSaveInstanceState()方法,并从onCreate的bundle中重新获取,但这一方法仅仅适合一些简单的UI状态,对于列表型这种庞大的数据类型并不适合
- Activity或Fragment经常会做一些异步的耗时操作,随之就需要管理这些异步操作得到的数据,并在destroyed的时候清理它们,从而避免内存溢出这类问题的发生。但是这样的处理会随着项目扩大而变得十分复杂
- Activity或Fragment本身需要处理很多用户的输入事件并和操作系统打交道,当它们还要花时间管理那些数据资源时,它们所在的类就会变得异常庞大,造就出所谓的god activities和god fragments,这样很尴尬
所以引入ViewModel之后,数据就可以从UI中分离出来,让每个模块的职责更加清晰合理。并且当Activity或Fragment重建的时候,ViewModel会自动保留之前的数据并给新的Activity或Fragment使用。
ViewModel的生命周期
当ViewModel的实例生成之后,它会一直待在内存中,直到对应的Lifecycle彻底结束。下图展示了一个Activity经过旋转然后到结束运行这段期间各生命周期的状态,在Activity的生命周期旁边显示了其中ViewModel的生命周期。虽然这个图描述的是Activity的状态,但是Fragment和它是类似的。
ViewModel的生命周期从图中可以看出,在第一次调用Activity对象的onCreate()方法时创建了一个ViewModel。在Activity运行过程中可能会多次调用onCreate()方法(比如当设备屏幕旋转时),但是ViewModel一直存在,直到Activity结束并销毁。这意味着ViewModel不会因为它的创建者的一个配置变化而被销毁,Activity 的新实例将与现有的ViewModel重新连接。
至于为什么ViewModel有如此功能,稍后的源码分析中将给大家解惑
ViewModel使用流程
后面我给出的范例将会按照官方图例所列出的结构进行展示,这里先介绍一下流程。
UI,也就是Activity或Fragment,它的职责仅仅是视图的管理(大部分是刷新工作),相当于是一个ViewController的角色。ViewModel类相当于数据集散地,UI要这个数据了,ViewModel就去帮它在仓库找好,无论是数据库还是网络都行。ViewModel拿到数据之后就通知UI,通常情况下这个通知由LiveData来完成。最后通过LiveData去找DataBinding,完成数据的刷新
实例
很简单的一个Demo,获取列表信息
来看看项目的结构。UI部分在adapter与activity包下,自定义DataBinding属性在binding包下,数据结构在model包下,数据请求仓库在repository,viewmodel就不用多说了
项目结构这是典型的DataBinding+LiveData+ViewModel结构模式,如需了解更多,可以参考谷歌范例android-architecture-components
ViewModel部分
我们先把流程架子搭起来
在ViewModel部分,这里定义了两个LiveData。当id
所持有的数据发生变化的时候,switchMap
关联的func
就会执行,将Repos.getMainResponse(input)
请求的返回值(返回值类型也是LiveData<ArrayList<Data1>>
)赋值给responses
,这样后续网络请求所得到的值就会通知到responses
的观察者
class MainViewModel : ViewModel() {
val id = MutableLiveData<String>()
var responses: LiveData<ArrayList<Data1>>? = null
init {
responses = Transformations.switchMap(id) { input ->
if (input == null) {
MutableLiveData()
} else {
Repos.getMainResponse(input)
}
}
}
}
再来看看Repository部分,这里返回类型是LiveData<ArrayList<Data1>>
。这里没什么好说的,就是将结果通知到观察者
object Repos {
fun getMainResponse(input: String) : LiveData<ArrayList<Data1>> {
val temp = MutableLiveData<ArrayList<Data1>>()
OKHttpHelper.getInstance().okHttpUtils.asyncGet(
"http://www.mocky.io/v2/5b470ea0320000b332301d9f",
object : OKHttpUtils.RequestListener {
override fun onSuccess(string: String?) {
val gson = Gson()
val listType = object : TypeToken<ArrayList<Data1>>(){}.type
val value = gson.fromJson<ArrayList<Data1>>(string!!, listType)
temp.value = value
}
override fun onError() {
temp.value = null
}
override fun onStart() {
}
})
return temp
}
}
Activity做了ViewModel的绑定,完成了观察者的监听
vm = ViewModelProviders.of(this).get(MainViewModel::class.java)
vm?.responses?.observe(this, Observer<ArrayList<Data1>> {
Toast.makeText(this, "得到结果", Toast.LENGTH_LONG).show()
})
vm?.id?.value = "1"
流程我们已经打通,现在用DataBinding完成UI部分
UI部分
既然是RecyclerView,那我们就从Adapter开始
布局文件仅仅是数据加载
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="data1"
type="com.example.administrator.livedatademo.model.Data1"></variable>
</data>
<LinearLayout
android:orientation="horizontal" android:layout_width="match_parent"
android:layout_height="50dip">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@{data1.name}"
android:gravity="center"/>
</LinearLayout>
</layout>
Adapter里面没有什么,之前我们在介绍DataBinding的时候也说过了
class MainAdapter(private val beans: ArrayList<Data1>) : RecyclerView.Adapter<MainAdapter.MainViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MainViewHolder {
val viewDataBinding = DataBindingUtil.inflate<AdapterMainBinding>(LayoutInflater.from(parent.context), R.layout.adapter_main, parent, false)
return MainViewHolder(viewDataBinding)
}
override fun getItemCount() = beans.size
override fun onBindViewHolder(holder: MainViewHolder, position: Int) {
holder.viewDataBinding.setVariable(BR.data1, beans[position])
holder.viewDataBinding.executePendingBindings()
}
class MainViewHolder(viewDataBinding: ViewDataBinding) : RecyclerView.ViewHolder(viewDataBinding.root) {
val viewDataBinding: ViewDataBinding = viewDataBinding
}
}
Adapter这边没有什么好稀奇的,精彩的部分都在Activity里面。我在Activity的布局文件里面添加了2个自定义属性app:adapter
与app:refresh
,前者方便我们对重复的Adapter配置文件进行封装,后者方便我们在ViewModel中进行UI刷新
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="adapter"
type="com.example.administrator.livedatademo.adapter.MainAdapter"></variable>
<variable
name="rf"
type="com.example.administrator.livedatademo.model.AutoRefresh"></variable>
</data>
<android.support.constraint.ConstraintLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MainActivity">
<android.support.v7.widget.RecyclerView
android:layout_width="368dp"
android:layout_height="495dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:adapter="@{adapter}"
app:refresh="@{rf.value}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
</layout>
AutoRefresh
数据类是这样的,value
类型是ObservableBoolean
data class AutoRefresh(var value: ObservableBoolean?)
来看看我的自定义属性,自定义属性都不复杂,关键在于你要记住单向绑定这个概念,随时都要将这个思想沉浸在心中,用Model驱动View,不然你的Activity就又臃肿了
object BindingAdapters {
@JvmStatic
@BindingAdapter(value = ["adapter"])
fun <T : RecyclerView.ViewHolder> setAdapter(recyclerView: RecyclerView, adapter: RecyclerView.Adapter<T>) {
recyclerView.setHasFixedSize(true)
recyclerView.layoutManager = LinearLayoutManager(recyclerView.context)
recyclerView.adapter = adapter
}
@JvmStatic
@BindingAdapter(value = ["refresh"])
fun autoRefreshAdapter(recyclerView: RecyclerView, boolean: Boolean) {
if (boolean) {
recyclerView.adapter.notifyDataSetChanged()
}
}
}
这样我就可以在ViewModel处理数据刷新。因此之前请求的数据就可以从Activity中传到ViewModel里面去了
class MainActivity : AppCompatActivity() {
private var vm: MainViewModel? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewDataBinding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
viewDataBinding.rf = AutoRefresh(ObservableBoolean(false))
vm = ViewModelProviders.of(this, MainViewModelFactory(viewDataBinding.rf!!)).get(MainViewModel::class.java)
vm?.responses?.observe(this, Observer<ArrayList<Data1>> {
Toast.makeText(this, "得到结果", Toast.LENGTH_LONG).show()
vm?.notifyDataSetChanged(it!!)
})
viewDataBinding.adapter = vm?.adapter
vm?.id?.value = "1"
}
}
既然请求的数据传到ViewModel,那么数据集肯定也在ViewModel里面定义,adapter当然也要在里面才合适。最终VIewModel里面应该是这样的
class MainViewModel(private val autoRefresh: AutoRefresh) : ViewModel() {
val id = MutableLiveData<String>()
var responses: LiveData<ArrayList<Data1>>? = null
private var beans = ArrayList<Data1>()
val adapter: MainAdapter by lazy {
MainAdapter(beans)
}
init {
responses = Transformations.switchMap(id) { input ->
if (input == null) {
MutableLiveData()
} else {
Repos.getMainResponse(input)
}
}
}
fun notifyDataSetChanged(temp: ArrayList<Data1>) {
beans.addAll(temp)
autoRefresh.value!!.set(true)
}
}
与之前写法相比,这里多一个入参。一旦ViewModel有参数传进来,ViewModelProviders
在绑定的时候就需要使用ViewModelProvider.Factory
。
class MainViewModelFactory(private val autoRefresh: AutoRefresh) : ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (ViewModel::class.java.isAssignableFrom(modelClass)) {
return MainViewModel(autoRefresh) as T
}
return super.create(modelClass)
}
}
来看看效果
效果
读者可以自己体会一下此种项目结构是不是相比之前MVP或者MVC更加清晰简洁
源码分析
之前已经说了,ViewModel即使发生configuration改变(比如旋转屏幕),数据仍然可以存在不会销毁,那我们就从源码里面来一探究竟,看看这个牛X的功能到底是如何实现的
先从ViewModelProviders
入手,因为ViewModel的创建就在这里完成。来到之前Activity的代码
ViewModelProviders.of(this, MainViewModelFactory(viewDataBinding.rf!!)).get(MainViewModel::class.java)
ViewModelProviders
有4个of()
方法,他们都是用来创建ViewModelProvider
对象的。
进入我们所调用的那个of()
方法
ViewModelProvider
才是真正创建和存储ViewModel的类。of()
方法对应的参数分别为ViewModel所绑定到的Activity/Fragment对象以及ViewModelProvider.Factory
。如果没有设置Factory
,那默认采用ViewModelProvider.AndroidViewModelFactory.getInstance(application)
深入到每一行代码去,首先是checkApplication()
,它负责检查Application是否可用
如果之前没有传ViewModelProvider.Factory
对象,默认就是AndroidViewModelFactory
下面一步就是获取ViewModelStore
对象了,它里面引用一个HashMap来保存ViewModel对象
注意这里有一个HolderFragment
关键来了,我们进入HolderFragment
,holderFragmentFor()
负责创建Fragment并与其所在的Activity的Lifecycle相关联
在HolderFragment
的构造方法里有一句setRetainInstance(true);
,正是因为setRetainInstance()
这个方法被设置为true了,其所在的Activity才在重建时不会将该Fragment销毁。如果不设置或者设置为false,那么HolderFragment
会在Activity重建时同样发生重建。
在HolderFragment
创建的同时也完成了ViewModelStore
的创建,而ViewModelStore
里面保存的都是ViewModel
,所以ViewModel
也就被保存下来了
当HolderFragment
销毁时,其所保存的ViewModel对象就会被清理掉
通过ViewModelStore
与ViewModelProvider.Factory
对象,我们就得到了ViewModelProvider
,进入ViewModelProvider
类来查看如何通过get()
方法来得到VIewModel对象。这里有一个Key值DEFAULT_KEY + ":" + canonicalName
,同时还有ViewModel的类modelClass
继续向下看,系统会优先从已有的ViewModelStore
中去查找这个Key对应的ViewModel,如果找到则直接返回,否则则通过mFactory.create(modelClass)
创建并保存到ViewModelStore
中再返回。我们来看下create()
由于我这里自定义了ViewModelProvider.Factory
,所以他会调用我自己创建实力的方法。那如果我没有自定义它会怎么做呢?很简单,反射
总结一下,ViewModel运行流程大致就是这样,理解起来应该不复杂
ViewModel运行流程参考文章
【大揭秘】Android架构组件ViewModel来龙去脉
Android架构组件三 Android Architecture Components ViewModel组件解析