要看本系列其他文章,可访问此链接Jetpack架构学习 | Stars-One的杂货小窝

原文地址:Jetpack架构组件学习(2)——ViewModel和Livedata使用 | Stars-One的杂货小窝

Jetpack架构推荐使用MVVM结构,为此推出了几个MVVM的组件库供我们开发者快速接入,首先要讲的就是ViewModel

个人理解:Activity为View,VM就是ViewModel,负责数据的逻辑处理,Model则是数据源

ViewModel

介绍

ViewModel能做什么?

ViewModel生命周期与Activity独立,可以优雅的保存内存中的数据(在屏幕旋转的横竖屏切换时,数据可以得到保留)

可以将ViewMoel看做是数据的处理器和数据仓库,其只负责处理数据

基本使用

首先,导入依赖

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"

这里,由于我是使用了Kotlin,所以使用的是具有kotlin特性的版本,如果是纯Java,可以使用下述依赖

def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-extensions:$lifecycle_version"

下面来个简单的计数器例子:

class ViewModelActivity : AppCompatActivity() {
//4.声明变量
lateinit var myViewModel:MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_model)
//5.通过ViewModelProvider获取单一实例对象
myViewModel = ViewModelProvider(this).get(MyViewModel::class.java) //7.设置按钮的点击监听器
btnPlus.setOnClickListener {
myViewModel.countPlus()
refreshCount()
} refreshCount()
} //6.设置更新数据(暂时,后面会调整为livedata形式)
fun refreshCount() {
tvContent.text = myViewModel.count.toString()
}
} //1.定义ViewModel
class MyViewModel : ViewModel() {
//2.定义数据
var count = 0 //3.对外暴露方法,用来修改数值
fun countPlus() {
count++
}
}

效果如下所示:

为什么上面需要使用ViewmodelProvider来获取单一对象?原因是ViewModel的生命周期是独立于Activity,可以临时保存数据

上述还是使用的比较传统的方式,在对应按钮的点击监听对UI进行修改,之后会使用LiveData进行改造

ViewModel的构造函数传参

由于之前说过ViewModel是单例模式,所以想要传参,需要借助ViewModelProvider.Factory这个接口类来实现

假如上面我MainViewModel需要接收一个Activity中传来的参数,我们可以这样写:

将原来的MainViewModel类增加个构造方法

class MyViewModel(val saveCount: Int?) : ViewModel() {
var count = 0 init {
//不传参数的话,则默认是0
count = saveCount ?: 0
} fun countPlus() {
count++
}
}

之后,定义个工厂类MyViewModelFactory去实现ViewModelProvider.Factory接口

class MyViewModelFactory(val myCount: Int) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
//这里使用构造方法传参
return MyViewModel(myCount) as T
}
}

PS:可以把MyViewModelFactory写在MyViewModel中,这样没必要再整多一个文件了

在Activity中,我们需要新建一个MyViewModelFactory对象和ViewModelProvider一起使用即可,代码如下所示

myViewModel = ViewModelProvider(this,MyViewModelFactory(12)).get(MyViewModel::class.java)

可以看到,现在默认是从12开始了,如下图所示

PS:感觉想要实现Activity中给ViewModel传参的话,步骤是有点多的,不过考虑下架构,这种使用构造方法进行传参其实不太符合MVVM架构。

因为Activity改变数据,触发对应的数据更改即可,而不是在构造方法的时候传参;不过,也可以会有特殊需求(比如说ViewModel中需要context对象),所以才留下了这个实现方式吧

AndroidViewModel(ViewModel扩展类)

上文说到,如果ViewModel中需要Context对象,我们怎么办呢?

经常遇到的情况,是需要获取一个Context上下文对象,可能你想到,我们把当前的Activity传入到ViewModel中不就可以了吗?

这样做虽然是可以,不过会引起其他的问题,会导致ViewModel与Activity耦合过深,原本设计ViewModel就是为了减少耦合,这样做却是本末倒置了

使用ViewModel的时候,需要注意的是ViewModel不能够持有View、Lifecycle、Acitivity引用,而且不能够包含任何包含前面内容的类。因为这样很有可能会造成内存泄漏。

开发团队也是考虑到这样的问题,提供了一个子类AndroidViewModel供我们使用,其也是继承于ViewModel,其中存在有个application实例(即application对象)

可以看下AndroidViewModel的源码

由于每个APP只有一个application对象,所以就不用担心会出现上述问题

使用的话,和上述是一样,也需要使用到Factory接口,不过无需我们去实现了的,我们使用内置的ViewModelProvider.AndroidViewModelFactory这个类即可

具体代码如下所示:

class MyViewModel(application: Application) : AndroidViewModel(application) {

    fun getCacheDirPath() :String {
//MyApplication是我的自定义Application入口类,如果没有使用自定义的Application,这里直接写Application即可
var application = getApplication<MyApplication>()
//使用application对象获取缓存目录路径
return application.cacheDir.path
}
}

Activity中使用:

 myViewModel = ViewModelProvider(this,ViewModelProvider.AndroidViewModelFactory(application)).get(MyViewModel::class.java)

可以看到TextView设置的数值即为路径

进阶用法

  • 由于之前提及到ViewModel实际是单例模式的,且生命周期与Activity独立,所以可以使用ViewModel进行Activity和Fragment之间或Fragment之间的数据共享

LiveData

上述ViewModel只是提供了个数据仓库,如果我们使用传统的对象是无法实现MVVM架构的,这个时候就得使用LiveData

LiveData即相当于给数据加多一层包装,让数据可以被观察

由于LiveData内部也是使用的LifeCycle实现的,所以它设计成当数据发生改变时候,只要在页面可见状态才会触发页面改变,节省资源及错误的发生

基本使用

1.导入依赖

首先还是导入依赖,版本与上述使用的版本一致即可,根据项目所属类型选择对应的版本

//kotlin特性版本
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" //Java版本
implementation "androidx.lifecycle:lifecycle-livedata-extensions:$lifecycle_version"

2.使用MutableLiveData来包装数据

LiveData中提供了MutableLiveDataLiveData两个类用来包装数据,这里先以MutableLiveData为例讲解下使用方式,两者的不同在下文再补充

我们以上文为例,稍微修改了(主要修改了237步)

class ViewModelActivity : AppCompatActivity() {
//4.声明变量
lateinit var myViewModel:MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_view_model)
//5.通过ViewModelProvider获取单一实例对象
myViewModel = ViewModelProvider(this).get(MyViewModel::class.java) //监听数据,UI更改
myViewModel.count.observe(this){
tvContent.text = it.toString()
} //7.设置按钮的点击监听器
btnPlus.setOnClickListener {
//点击操作只处罚对应的数据修改,不做更新UI操作
myViewModel.countPlus()
} } } //1.定义ViewModel
class MyViewModel : ViewModel() {
//2.定义数据
var count = MutableLiveData<Int>(0) //3.对外暴露方法,用来修改数值
fun countPlus() {
val value = count.value as Int
count.value = value+1
}
}

这里的主要区别就是,我们不在点击事件中去更改UI,更改UI的操作则是写了个数据监听方法,去监听数据更改再更新UI

虽然这里感觉还是和之前一样,也是要通过TextView对象去修改数值,但是,从代码上来看,数据处理逻已经和页面渲染分离开来了,也是方便我们的编写测试用例

setValue()postValue()的区别

上述代码中,我们通过count.value=value+1来设置数据,这里由于是kotlin的写法所以看不出来是setValue()方法,如果是Java的话,是要调用setValue()方法来设置数据

除了setValue(),LiveData还提供了postValue()方法,这两种的方法区别在于setValue()要再主线程(UI线程)才能操作,而postValue()则是在子线程或主线程都可以

我们把上述代码中的第7步稍微改动下,点击按钮就开启一个线程,然后等待1s后才更新数据

然后运行的时候报错了,如下图所示

把第3步的设置数据操作改下

就可以正常运行了,点击按钮会等1s后,数据才会发生变化,如下图:

map

此方法主要是将LiveData包装的数据类转为单一另外类型的LiveData,举个例子说明

比如我们有个User类,里面包含两个字段(姓名和年龄),而我们页面只需要观察姓名的改变,而不关心年龄的改变,那么我们就可以没有必要把整个对象都观察

只需要像下面这样写,可以把MutableLiveData<User>转为MutableLiveData<String>

注:以下代码是在ViewMode抽出来的一段代码

data class User(var name: String, var age: Int)
//改为private,不对外提供访问
private val user = MutableLiveData<User>(User("张三", 5)) val userName = Transformations.map(user){ it.name }

之后,我们在Activity只需要对userName数据进行监听,改变UI即可

PS:补充一下,LiveData中是无法做到对对象中的某个字段进行监听,只能做到对对象内存地址进行监听

如果两个对象的内存地址是相同的,那么不会触发对应的数据改变监听事件

若是想要实现,目前个人摸索的方式就是使用koltin扩展方法copy(),如下代码,就是快速复制一个对象,且改变其的某个字段的数据,之后即可正常触发数据变更的监听事件

val user = User("zz", 11)
val newUser = user.copy(name = "hello")

switchMap

前面所讲述内容,LiveData的对象都是都是位于同个ViewModel中,但实际情况,我们需要从别的地方拿取数据,这个时候就是可以考虑使用此方法

假设我们User对象是要通过userId来获取,定义一个单例,实现上述功能

object UserRepository{
fun getUserById(userId: String) :LiveData<User>{
val userLiveData = MutableLiveData<User>()
userLiveData.value = User("张三$userId",15)
return userLiveData
}
}

在ViewModel中定个方法去实现获取数据

class MyViewModel : ViewModel() {

    fun getUser(userId: String): LiveData<User> {
return UserRepository.getUserById(userId)
}
}

这个时候,我们想要观察这个对象改变从而渲染UI,应该如何做呢?

估计大部分人都会想到下面的代码

myViewModel.getUser("111").observe(this){
//todo UI渲染
}

但是注意,之前UserRepository中的getUser方法每次返回的都是新的对象,所以每次观察的对象其实都是新的,而无法观察到

改造思路:

  1. 对userId进行数据监听
  2. userId变更,同时触发user对象的变更
class MyViewModel : ViewModel() {

    val userIdLiveData = MutableLiveData("")

    //user是MutableLiveData<User>对象
val user = Transformations.switchMap(userIdLiveData){
UserRepository.getUserById(it)
} fun getUser(userId: String){
userIdLiveData.value = userId
}
}

Activity中的代码:

myViewModel.user.observe(this){
tvContent.text = it.name
} //7.设置按钮的点击监听器
btnPlus.setOnClickListener {
//点击操作只处罚对应的数据修改,不做更新UI操作
myViewModel.getUser("445")
}

效果如下:

PS:如果是不传参数的,可以设置一个MutableLiveData<Any?>对象,并让其重新赋值即可实现更新数据,如下代码

object UserRepository{

    fun refresh() :LiveData<User>{
val userLiveData = MutableLiveData<User>()
userLiveData.value = User("张三", 15)
return userLiveData
}
} class MyViewModel : ViewModel() { val refreshLiveData = MutableLiveData<Any?>() fun refresh() {
//会触发对应的数据变更通知
refreshLiveData.value = refreshLiveData.value
} val user = Transformations.switchMap(refreshLiveData){
UserRepository.refresh()
} }

LiveData和MutableLiveData两者区别

LiveData是不可变的,MutableLiveData是可变的

LiveDataMutableLiveData子类,其里面的setValuepostValue不是public,因此无法在外部调用,其只能注册观察者

MutableLiveData重写了方法(如下图),并声明为public,所以我们才能在任意地方可以调用进行数值的修改(不过推荐还是在ViewModel中进行数据更改的操作)

代码优化

上述虽然实现了基本的使用,但是ViewModel中的封装的代码还存在不安全性,原因是我们的count变量,只要某个地方拿到了这个ViewModel的对象,就直接拿到这个count变量,从而进行修改

这样就会造成数据问题,所以我们得实现可信任源才能对数据进行修改

官方推荐的做法:

1.ViewModel对外提供不可变的可观察数据LiveData对象

2.由外部调用方法才能改变内部数据

class MyViewModel : ViewModel() {
//_count是只能在ViewModel内部修改
private val _count =MutableLiveData(0) //对外提供的变量,Activity中可注册观察者从而修改UI
val count :LiveData<Int> get() = _count //修改数据的方法
fun countPlus() {
val value = count.value as Int
_count.value = value+1
}
}

Activity代码没有做任何改变,测试可以发现,效果是一样的,只是优化了代码上的写法

参考

Jetpack架构组件学习(2)——ViewModel和Livedata使用的更多相关文章

  1. Jetpack架构组件学习(1)——LifeCycle的使用

    原文地址:Jetpack架构组件学习(1)--LifeCycle的使用 | Stars-One的杂货小窝 要看本系列其他文章,可访问此链接Jetpack架构学习 | Stars-One的杂货小窝 最近 ...

  2. Jetpack架构组件学习(3)——Activity Results API使用

    原文地址:Jetpack架构组件学习(3)--Activity Results API使用 - Stars-One的杂货小窝 技术与时俱进,页面跳转传值一直使用的是startActivityForRe ...

  3. Jetpack架构组件学习(4)——APP Startup库的使用

    最近在研究APP的启动优化,也是发现了Jetpack中的App Startup库,可以进行SDK的初始化操作,于是便是学习了,特此记录 原文:Jetpack架构组件学习(4)--App Startup ...

  4. Jetpack 架构组件 LiveData ViewModel MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  5. Android Jetpack 架构组件最佳实践之“网抑云”APP

    背景 近几年,Android 相关的新技术层出不穷.往往这个技术还没学完,下一个新技术又出来了.很多人都是一脸黑人问号? 不少开发者甚至开始哀嚎:"求求你们别再创造新技术了,我们学不动了!& ...

  6. Jetpack 架构组件 Room 数据库 ORM MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  7. Jetpack 架构组件 Lifecycle 生命周期 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  8. Android 架构:Android Jetpack 架构组件的学习和分析

    参考:https://mp.weixin.qq.com/s/n-AzV7Ke8wxVhmC6ruUIUA 参考:https://jekton.github.io/2018/06/30/android- ...

  9. Android官方架构组件介绍之ViewModel

    ViewModel 像Activity,Fragment这类应用组件都有自己的生命周期并且是被Android的Framework所管理的.Framework可能会根据用户的一些操作和设备的状态对Act ...

随机推荐

  1. FastAPI 学习之路(四)

    系列文章: FastAPI 学习之路(一)fastapi--高性能web开发框架 FastAPI 学习之路(二) FastAPI 学习之路(三) 之前的文章分享了如何去在请求中增加参数,本文我们将分享 ...

  2. 用C++实现的数独解题程序 SudokuSolver 2.3 及实例分析

    SudokuSolver 2.3 程序实现 用C++实现的数独解题程序 SudokuSolver 2.2 及实例分析 里新发现了一处可以改进 grp 算法的地方,本次版本实现了对应的改进 grp 算法 ...

  3. Install WSL

    Install WSL Prerequisites You must be running Windows 10 version 2004 and higher (Build 19041 and hi ...

  4. /usr/bin/python^M: bad interpreter: No such file or directory

    利用如下命令查看文件格式 :set ff 或 :set fileformat 可以看到如下信息 fileformat=dos 或 fileformat=unix 利用如下命令修改文件格式 :set f ...

  5. css实现水平-垂直居中的方法

    * 定宽居中: 1.absolute+负margin 2.absolute+margin:auto 3.absolute--calc 4.min-height:100vh + flex + margi ...

  6. MongoDB中如何优雅地删除大量数据

    删除大量数据,无论是在哪种数据库中,都是一个普遍性的需求.除了正常的业务需求,我们需要通过这种方式来为数据库"瘦身". 为什么要"瘦身"呢? 表的数据量到达一定 ...

  7. Sequence Model-week1编程题3-用LSTM网络生成爵士乐

    Improvise a Jazz Solo with an LSTM Network 实现使用LSTM生成音乐的模型,你可以在结束时听你自己的音乐,接下来你将会学习到: 使用LSTM生成音乐 使用深度 ...

  8. mybatis中的#和$的区别 以及 防止sql注入

    声明:这是转载的. mybatis中的#和$的区别 1. #将传入的数据都当成一个字符串,会对自动传入的数据加一个双引号.如:order by #user_id#,如果传入的值是111,那么解析成sq ...

  9. the Agiles Scrum Meeting 4

    会议时间:2020.4.12 20:00 1.每个人的工作 今天已完成的工作 yjy:基本完成广播功能,修复bug issues:小组任务1-增量开发组 Bug:冲刺 wjx:继续实现注销功能的后端 ...

  10. 【二食堂】Alpha - Scrum Meeting 3

    Scrum Meeting 3 例会时间:4.13 12:00 - 12:30 进度情况 组员 昨日进度 今日任务 李健 1. 继续学习前端知识,寻找一些可用的框架.issue 1. 搭建主页html ...