Google 推荐在 MVVM 中使用 Kotlin Flow我相信如今几乎所有的 Android 开发者至少都听过 MVVM 架构,在 Google Android 团队宣布了 Jetpack 的视图模型之后,它已经成为了现代 Android 开发模式最流行的架构之一,如下图所示
在官宣 Jetpack 的视图模型之后,同时 Google 在 [Jetpack Guide](https://developer.android.com/jetpack/guide#fetch-data) 文章中的示例,也在 Repositories 或者 DataSource 中使用 LiveData,以至于在很多开源的 MVVM 项目中也是直接使用 LiveData,但是在 Repositories 或者 DataSource 中直接使用 LiveData 这种做法对吗?这是我一直以来的一个疑问?
直到我打开[ Android 架构组件 ](https://developer.android.com/topic/libraries/architecture/index.html)页面,看了在页面上增加了最新的文章,这几篇文章大概的内容是说如何在 MVVM 中使用 Flow 以及如何与 LiveData 一起使用,当我看完并通过实践之后大概明白了,LiveData 是一个生命周期感知组件,它并不属于 Repositories 或者 DataSource 层,下文会有详细的分析。
在 Google 发布的 Jetpack 的最新成员 Paging3,在其内部的源码实现也是使用的 Flow,关于 Paging3 的使用可以参考以下链接:
不仅仅是 Jetpack 成员支持 Flow,在 Google 提供的 Demo 里面也都在使用 Flow,也有很多开源的 MVVM 项目也在逐渐切换到 Flow,为什么 Google 会推荐使用它呢,使用 Flow 能带来那些好处呢,为我们解决了什么问题
Kotlin Flow 是什么?
Kotlin Flow 解决了什么问题?
Flow 库是在 Kotlin Coroutines 1.3.2 发布之后新增的库,也叫做异步流,类似 RxJava 的 Observable
、 Flowable
等等,所以很多人都用 Flow 与 RxJava 做对比。
Flow 相比于 RxJava 简单的太多了,你还记得那些 RxJava 傻傻分不清楚的操作符吗 Observable
、 Flowable
、 Single
、 Completable
、 Maybe
等等。
那么 Flow 为我们解决了什么问题,我主要从以下几个方面思考:
Observable
、 Flowable
、 Single
等等,如果我们不去了解背后的原理,造成内存泄露是很正常的事,大家可以从 StackOverflow 上查看一下,有很多因为 RxJava 造成内存泄露的例子而相对于以上的不足,Flow 有以下优点:
Kotlin Flow 如何在 MVVM 中使用
Jetpack 的视图模型 MVVM 架构由 View + DataBinding + ViewModel + Model 组成,如下所示,我相信下面这张图大家非常熟悉了,
接下来我们一起来探究一下 Kotlin Flow 在 MVVM 当中每层是如何实现的。
Kotlin Flow 在数据源中的使用
在 [PokemonGo](https://github.com/hi-dhl/PokemonGo) 项目中,进入详情页,会检查本地是否有数据,如果没有会去请求 [pokeapi] (https://pokeapi.co/)详情页接口,获得最新的数据,然后存储在数据库中。
Flow 是协程的扩展,如果要在 Room 和 Retrofit 中使用,Room 和 Retrofit 需要支持协程才可以,在 Retrofit >= 2.6.0 和 Room >= 2.1 版本都支持协程,我们来看一下 Room 和 Retrofit 数据源的配置。
Room: PokemonGo/app/src/main/java/com/hi/dhl/pokemon/data/local/PokemonInfoDao.kt
@Query("SELECT * FROM PokemonInfoEntity where name = :name")
suspend fun getPokemon(name: String): PokemonInfoEntity?
或者直接返回 Flow
@Query("SELECT * FROM PokemonInfoEntity where name = :name")
fun getPokemon(name: String): Flow
Retrofit: PokemonGo/app/src/main/java/com/hi/dhl/pokemon/data/remote/PokemonService.kt
@GET("pokemon/{name}")
suspend fun fetchPokemonInfo(@Path("name") name: String): NetWorkPokemonInfo
如上所见在方法前增加了用 suspend
进行了修饰,只有被 suspend
修饰的方法,才可以在协程中调用。
按照如上配置,在数据源的工作就完成了,相比于 RxJava 的 Observable
、 Flowable
、 Single
、 Completable
、 Maybe
使用场景要简单太多了,我们来看一下在 Repositories 中是如何使用的。
如果我们想在 Flow 中使用 Retrofit 或者 Room 进行网络请求或者查询数据库的操作,我们需要将使用 suspend
修饰符的操作放到 flow { ... }
中执行,最后使用 emit()
方法更新数据,将数据发送给 ViewModel,代码如下所示: PokemonGo/app/src/main/java/com/hi/dhl/pokemon/data/repository/PokemonRepositoryImpl.kt
flow {
val pokemonDao = db.pokemonInfoDao()
// 查询数据库是否存在,如果不存在请求网络
var infoModel = pokemonDao.getPokemon(name)
if (infoModel == null) {
// 网络请求
val netWorkPokemonInfo = api.fetchPokemonInfo(name)
// 将网路请求的数据,换转成的数据库的 model,之后插入数据库
infoModel = netWorkPokemonInfo.let {
PokemonInfoEntity(
name = it.name,
height = it.height,
weight = it.weight,
experience = it.experience
)
}
// 插入更新数据库
pokemonDao.insertPokemon(infoModel)
}
// 将数据源的 model 转换成上层用到的 model,
// ui 不能直接持有数据源,防止数据源的变化,影响上层的 ui
val model = mapper2InfoModel.map(infoModel)
// 更新数据,将数据发送给 ViewModel
emit(model)
}.flowOn(Dispatchers.IO) // 通过 flowOn 切换到 IO 线程
将上面的代码简化如下所示:
flow {
// 进行网络或者数据库操作
emit(model)
}.flowOn(Dispatchers.IO) // 通过 flowOn 切换到 IO 线程
正如你所见,将耗时操作放到 flow { ... }
里面,通过 flowOn(Dispatchers.IO)
切换到 IO 线程,最后通过 emit()
方法将数据发送给 ViewModel,接下来我们来看一下如何在 ViewModel 中接受 Flow 发送的数据。
在 ViewModel 中使用 Flow 之前在 Jetpack 成员 Paging3 实践以及源码分析(一) 文章也有提到, 这里我们在深入分析一下,在 ViewModel 中接受 Flow 发送的数据有三种方法,根据实际情况去调用。 PokemonGo/app/src/main/java/com/hi/dhl/pokemon/ui/detail/DetailViewModel.kt
方法一
在 LifeCycle 2.2.0 之前使用的方法,使用两个 LiveData,一个是可变的,一个是不可变的,如下所示:
// 私有的 MutableLiveData 可变的,对内访问
private val _pokemon = MutableLiveData()
// 对外暴露不可变的 LiveData,只能查询
val pokemon: LiveData = _pokemon
viewModelScope.launch {
polemonRepository.featchPokemonInfo(name)
.onStart {
// 在调用 flow 请求数据之前,做一些准备工作,例如显示正在加载数据的进度条
}
.catch {
// 捕获上游出现的异常
}
.onCompletion {
// 请求完成
}
.collectLatest {
// 将数据提供给 Activity 或者 Fragment
_pokemon.postValue(it)
}
}
viewModelScope.launch
方法中执行协程代码块collectLatest
是末端操作符,收集 Flow 在 Repositories 层发射出来的数据,在一段时间内发送多次数据,只会接受最新的一次发射过来的数据_pokemon.postValue
方法将数据提供给 Activity 或者 Fragment方法二
在 LifeCycle 2.2.0 之后,可以用更精简的方法来完成,使用 LiveData 协程构造方法 (coroutine builder),这个方法也是在 PokemonGo 项目中用到的方法。
@OptIn(ExperimentalCoroutinesApi::class)
fun fectchPokemonInfo(name: String) = liveData {
polemonRepository.featchPokemonInfo(name)
.onStart { // 在调用 flow 请求数据之前,做一些准备工作,例如显示正在加载数据的进度条 }
.catch { // 捕获上游出现的异常 }
.onCompletion { // 请求完成 }
.collectLatest {
// 更新 LiveData 的数据
emit(it)
}
}
liveData{ ... }
协程构造方法提供了一个协程代码块,产生的是一个不可变的 LiveData,emit()
方法则用来更新 LiveData 的数据collectLatest
是末端操作符,收集 Flow 在 Repositories 层发射出来的数据,在一段时间内发送多次数据,只会接受最新的一次发射过来的数据PS:需要注意的是 flow { ... }
和 liveData{ ... }
内部都有一个 emit()
方法。
方法三:
调用 Flow 的扩展方法 asLiveData()
返回一个不可变的 LiveData,供 Activity 或者 Fragment 调用。
@OptIn(ExperimentalCoroutinesApi::class)
suspend fun fectchPokemonInfo3(name: String) =
polemonRepository.featchPokemonInfo(name)
.onStart {
// 在调用 flow 请求数据之前,做一些准备工作,例如显示正在加载数据的按钮
}
.catch {
// 捕获上游出现的异常
}
.onCompletion {
// 请求完成
}.asLiveData()
因为 polemonRepository.featchPokemonInfo(name)
是一个用 suspend
修饰的方法,所以在 ViewModel 中调用也需要使用 suspend
来修饰。
为什么说调用 asLiveData()
方法会返回一个不可变的 LiveData,我们来看一下源码:
fun Flow.asLiveData(
context: CoroutineContext = EmptyCoroutineContext,
timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData = liveData(context, timeoutInMs) {
collect {
emit(it)
}
}
asLiveData()
方法其实就是对 方法二 中的 liveData{ ... }
的封装
asLiveData
是 Flow 的扩展函数,返回值是一个 LiveDataliveData{ ... }
协程构造方法提供了一个协程代码块,在 liveData{ ... }
中执行协程代码collect
是末端操作符,收集 Flow 在 Repositories 层发射出来的数据emit()
方法更新 LiveData 的数据在 PokemonGo 项目中使用了 DataBinding 进行的数据绑定。
DataBinding(数据绑定)实际上是 XML 布局中的另一个视图结构层次,视图 (XML) 通过数据绑定层不断地与 ViewModel 交互,如下所示: PokemonGo/app/src/main/res/layout/activity_details.xml
......
......
这是获取神奇宝贝的详细信息,通过 DataBinding 以声明方式将数据(神奇宝贝的体重)绑定到界面上,更多使用参考项目中的代码。
如果不使用数据绑定,在 Activity 或者 Fragment 中如何处理 ViewModel 的三种方式。 PokemonGo/app/src/main/java/com/hi/dhl/pokemon/ui/detail/DetailsFragment.kt
方式一:
使用两个 LiveData,一个是可变的,一个是不可变的,在 Activity 或者 Fragment 中调用对外暴露不可变的 LiveData 即可,如下所示:
// 方法一
mViewModel.pokemon.observe(this, Observer {
// 将数据显示在页面上
})
方式二:
使用 LiveData 协程构造方法 (coroutine builder) 提供的协程代码块,产生的是一个不可变的 LiveData,处理方式 同方法一,在 Activity 或者 Fragment 中调用这个不可变的 LiveData 即可,如下所示:
// 方法二
mViewModel.fectchPokemonInfo2(mPokemonModel.name).observe(this, Observer {
// 将数据显示在页面上
})
方式三:
调用 Flow 的扩展方法 asLiveData()
返回一个不可变的 LiveData,在 Activity 或者 Fragment 调用这个不可变的 LiveData 即可,如下所示:
// 方法三
lifecycleScope.launch {
mViewModel.apply {
fectchPokemonInfo3(mPokemonModel.name).observe(this@DetailsFragment, Observer {
// 将数据显示在页面上
})
}
}
到这里关于 Kotlin Flow 在 MVVM 当中每层的实践就分析完了,如果使用过 RxJava 的小伙伴们应该会非常熟悉,对于没有使用过 RxJava 的小伙伴们,入门的门槛也是非常低的,强烈建议至少体验一次,体验过之后,我认为你会跟我一样爱上它的。
更多的高级Kotlin强化实战完整学习资料可以扫码免费领取!
● Kotlin 概述
● Kotlin 与 Java 比较
● 巧用 Android Studio
● 认识 Kotlin 基本类型
● 走进 Kotlin 的数组
● 走进 Kotlin 的集合
● 完整代码
● 基础语法
● 方法入参是常量,不可修改
● 不要 Companion、INSTANCE?
● Java 重载,在 Kotlin 中怎么巧妙过渡一下?
● Kotlin 中的判空姿势
● Kotlin 复写 Java 父类中的方法
● Kotlin “狠”起来,连TODO都不放过!
● is、as` 中的坑
● Kotlin 中的 Property 的理解
● also 关键字
● takeIf 关键字
● 单例模式的写法
● 从一个膜拜大神的 Demo 开始
● Kotlin 写 Gradle 脚本是一种什么体验?
● Kotlin 编程的三重境界
● Kotlin 高阶函数
● Kotlin 泛型
● Kotlin 扩展
● Kotlin 委托
● 协程“不为人知”的调试技巧
● 图解协程:suspend
● 协程是什么
● 什么是Job 、Deferred 、协程作用域
● Kotlin协程的基础用法
● 协程调度器
● 协程上下文
● 协程启动模式
● 协程作用域
● 挂起函数
● 协程异常的产生流程
● 协程的异常处理
● Android使用kotlin协程
● 在Activity与Framgent中使用协程
● ViewModel中使用协程
● 其他环境下使用协程
● 协程的常用环境
● 协程在网络请求下的封装及使用
● 高阶函数方式
● 多状态函数返回值方式