Android开发经验谈Android开发Android技术知识

Android JetPack之ViewModel

2018-07-13  本文已影响93人  皮球二二

谷歌在今年的I/O大会上推出了Jetpack的概念,意图在于统一框架与UI组件。所以我也将项目架构往这一概念上靠齐。

Jetpack
今天我们来研究架构层最重要的两个类LiveDataViewModel之一的ViewModel
本文涉及到的代码已上传到Github,欢迎star、fork

ViewModel简介

ViewModel是以关联生命周期的方式来存储和管理UI相关的数据的类,即使configuration发生改变(比如旋转屏幕),数据仍然可以存在不会销毁

ViewModel的功能

引入ViewModel的原因有以下几点:

所以引入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,完成数据的刷新

ViewModel使用流程

实例

很简单的一个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:adapterapp: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对象的。

ViewModelProviders

进入我们所调用的那个of()方法

ViewModelProvider才是真正创建和存储ViewModel的类。of()方法对应的参数分别为ViewModel所绑定到的Activity/Fragment对象以及ViewModelProvider.Factory。如果没有设置Factory,那默认采用ViewModelProvider.AndroidViewModelFactory.getInstance(application)

ViewModelProviders的of方法

深入到每一行代码去,首先是checkApplication(),它负责检查Application是否可用

checkApplication方法

如果之前没有传ViewModelProvider.Factory对象,默认就是AndroidViewModelFactory

getInstance方法

下面一步就是获取ViewModelStore对象了,它里面引用一个HashMap来保存ViewModel对象

ViewStore

注意这里有一个HolderFragment

ViewModelStore的of方法

关键来了,我们进入HolderFragmentholderFragmentFor()负责创建Fragment并与其所在的Activity的Lifecycle相关联

holderFragmentFor方法

HolderFragment的构造方法里有一句setRetainInstance(true);正是因为setRetainInstance()这个方法被设置为true了,其所在的Activity才在重建时不会将该Fragment销毁。如果不设置或者设置为false,那么HolderFragment会在Activity重建时同样发生重建。

HolderFragment创建的同时也完成了ViewModelStore的创建,而ViewModelStore里面保存的都是ViewModel,所以ViewModel也就被保存下来了

ViewModelStore的创建 HolderFragment的构造方法

HolderFragment销毁时,其所保存的ViewModel对象就会被清理掉

onDestroy()方法

通过ViewModelStoreViewModelProvider.Factory对象,我们就得到了ViewModelProvider,进入ViewModelProvider类来查看如何通过get()方法来得到VIewModel对象。这里有一个Key值DEFAULT_KEY + ":" + canonicalName,同时还有ViewModel的类modelClass

get()方法

继续向下看,系统会优先从已有的ViewModelStore中去查找这个Key对应的ViewModel,如果找到则直接返回,否则则通过mFactory.create(modelClass)创建并保存到ViewModelStore中再返回。我们来看下create()

get()

由于我这里自定义了ViewModelProvider.Factory,所以他会调用我自己创建实力的方法。那如果我没有自定义它会怎么做呢?很简单,反射

create()方法

总结一下,ViewModel运行流程大致就是这样,理解起来应该不复杂

ViewModel运行流程

参考文章

【大揭秘】Android架构组件ViewModel来龙去脉
Android架构组件三 Android Architecture Components ViewModel组件解析

上一篇 下一篇

猜你喜欢

热点阅读