前言

在之前分享过一篇 Jetpack 综合实战应用 Jetpack 实战:神奇宝贝 ,这个项目主要包了以下功能:

  1. 自定义 RemoteMediator 实现 network + db 的混合使用 ( RemoteMediator 是 Paging3 当中重要成员 )
  2. 使用 Data Mapper 分离数据源 和 UI
  3. Kotlin Flow 结合 Retrofit2 + Room 的混合使用
  4. Kotlin Flow 与 LiveData 的使用
  5. 使用 Coil 加载图片
  6. 使用 ViewModel、LiveData、DataBinding 协同工作
  7. 使用 Motionlayout 做动画
  8. App Startup 与 Hilt 的使用

而今天这篇文章主要来分析一下 神奇宝贝(PokemonGo) 项目,主要包含以下几个方面的内容:

  • 在 Repositories 或者 DataSource 中直接使用 LiveData 这种做法对吗?
  • Kotlin Flow 是什么?
  • Kotlin Flow 解决了什么问题?
  • Kotlin Flow 如何在 MVVM 中使用?
  • Kotlin Flow 如何与 Retrofit2 + Room 混合使用?

Google 推荐在 MVVM 中使用 Kotlin Flow

我相信如今几乎所有的 Android 开发者至少都听过 MVVM 架构,在 Google Android 团队宣布了 Jetpack 的视图模型之后,它已经成为了现代 Android 开发模式最流行的架构之一,如下图所示:

在官宣 Jetpack 的视图模型之后,同时 Google 在 Jetpack Guide 文章中的示例,也在 Repositories 或者 DataSource 中使用 LiveData,以至于在很多开源的 MVVM 项目中也是直接使用 LiveData,但是在 Repositories 或者 DataSource 中直接使用 LiveData 这种做法对吗?这是我一直以来的一个疑问?

直到我打开 Android 架构组件 页面,看了在页面上增加了最新的文章,这几篇文章大概的内容是说如何在 MVVM 中使用 Flow 以及如何与 LiveData 一起使用,当我看完并通过实践之后大概明白了,LiveData 是一个生命周期感知组件,它并不属于 Repositories 或者 DataSource 层,下文会有详细的分析。

Google 发布的 Jetpack 的成员 Paging3DataStore 等等,在其内部源码也大量的使用了 Flow

Paging3 分析及使用:

  • Jetpack成员Paging3 数据库实践及原理分析(一)
  • Jetpack成员Paging3网络实践及原理分析(二)
  • Jetpack成员Paging3获取网络分页数据并更新到数据库中(三)

DataStore 分析及使用:

  • 再见 SharedPreferences 拥抱 Jetpack DataStore(一)
  • 再见 SharedPreferences 拥抱 Jetpack DataStore(二)

不仅仅是 Jetpack 成员支持 Flow,在 Google 提供的 Demo 里面也都在使用 Flow,也有很多开源的 MVVM 项目也在逐渐切换到 Flow,为什么 Google 会推荐使用它呢,使用 Flow 能带来那些好处呢,为我们解决了什么问题?

Kotlin Flow 是什么?Kotlin Flow 解决了什么问题?

Flow 库是在 Kotlin Coroutines 1.3.2 发布之后新增的库,也叫做异步流,类似 RxJava 的 ObservableFlowable 等等,所以很多人都用 Flow 与 RxJava 做对比。

Flow 相比于 RxJava 简单的太多了,你还记得那些 RxJava 傻傻分不清楚的操作符吗 ObservableFlowableSingleCompletableMaybe 等等。

那么 Flow 为我们解决了什么问题,我主要从以下几个方面思考:

  • LiveData 是一个生命周期感知组件,最好在 View 和 ViewModel 层中使用它,如果在 Repositories 或者 DataSource 中使用会有几个问题

    • 它不支持线程切换,其次不支持背压,也就是在一段时间内发送数据的速度 > 接受数据的速度,LiveData 无法正确的处理这些请求
    • 使用 LiveData 的最大问题是所有数据转换都将在主线程上完成
  • RxJava 入门的门槛很高,学习过的朋友们,我相信能够体会到从入门到放弃是什么感觉
  • RxJava 虽然支持线程切换和背压,但是 RxJava 那么多傻傻分不清楚的操作符,实际上在项目中常用的可能只有几个例如 ObservableFlowableSingle 等等
  • 如果我们不去了解背后的原理,很容易造成内存泄露,在 StackOverflow 上有很多因为 RxJava 造成内存泄露的例子
  • RxJava 的链式调用虽然方便,在复杂的业务逻辑里面,层层的 RxJava 的链式调用 ,让代码难易阅读
  • 成本很高,团队所有人都要了解 RxJava 原理以及用法,自然也出现了很多种不可思议的 RxJava 用法
  • 解决回调地狱的问题

而相对于以上的不足,Flow 有以下优点:

  • Flow 是对 Kotlin 协程的扩展,让我们可以像运行同步代码一样运行异步代码,使得代码更加简洁,提高了代码的可读性
  • Flow 支持线程切换、背压
  • Flow 入门的门槛很低,加上 Google 的支持,API 调用更加简单,没有那么多傻傻分不清楚的操作符,新的 Jetpack 源码也在大量使用 Flow
  • 简单的数据转换与操作符,如 map 等等
  • 易于做单元测试

Kotlin Flow 如何在 MVVM 中使用

Jetpack 的视图模型 MVVM 架构由 View + DataBinding + ViewModel + Model 组成,如下所示,我相信下面这张图大家非常熟悉了,

接下来我们一起来探究一下 Kotlin Flow 在 MVVM 当中每层是如何实现的。

Kotlin Flow 在数据源中的使用

在 PokemonGo 项目中,进入详情页,会检查本地是否有数据,如果没有会去请求 pokeapi 详情页接口,获得最新的数据,然后存储在数据库中。

Flow 是协程的扩展,如果要在 Room 和 Retrofit 中使用,Room 和 Retrofit 需要支持协程才可以,在 Retrofit >= 2.6.0Room >= 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<PokemonInfoEntity>

@Query("SELECT * FROM PokemonInfoEntity where name = :name")
fun getPokemon(name: String): Flow<PokemonInfoEntity>

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 的 ObservableFlowableSingleCompletableMaybe 使用场景要简单太多了,我们来看一下在 Repositories 中是如何使用的。

Kotlin Flow 在 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 不能直接持有数据源,防止数据源的变化,影响上层的 uival model = mapper2InfoModel.map(infoModel)// 更新数据,将数据发送给 ViewModelemit(model)
}.flowOn(Dispatchers.IO) // 通过 flowOn 切换到 IO 线程

将上面的代码简化如下所示:

flow {// 进行网络或者数据库操作emit(model)
}.flowOn(Dispatchers.IO) // 通过 flowOn 切换到 IO 线程

正如你所见,将耗时操作放到 flow { ... } 里面,通过 flowOn(Dispatchers.IO) 切换到 IO 线程,最后通过 emit() 方法将数据发送给 ViewModel,接下来我们来看一下如何在 ViewModel 中接受 Flow 发送的数据。

Kotlin Flow 在 ViewModel 中的使用

在 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<PokemonInfoModel>()// 对外暴露不可变的 LiveData,只能查询
val pokemon: LiveData<PokemonInfoModel> = _pokemonviewModelScope.launch {polemonRepository.featchPokemonInfo(name).onStart {// 在调用 flow 请求数据之前,做一些准备工作,例如显示正在加载数据的进度条}.catch {// 捕获上游出现的异常}.onCompletion {// 请求完成}.collectLatest {// 将数据提供给 Activity 或者 Fragment_pokemon.postValue(it)}
}

  • 准备一私有的 MutableLiveData,只对内访问
  • 对外暴露不可变的 LiveData
  • 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<PokemonInfoModel> {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 <T> Flow<T>.asLiveData(context: CoroutineContext = EmptyCoroutineContext,timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {collect {emit(it)}
}

asLiveData() 方法其实就是对 方法二 中的 liveData{ ... } 的封装

  • asLiveData 是 Flow 的扩展函数,返回值是一个 LiveData
  • liveData{ ... } 协程构造方法提供了一个协程代码块,在 liveData{ ... } 中执行协程代码
  • collect 是末端操作符,收集 Flow 在 Repositories 层发射出来的数据
  • 最后调用 LiveData 中的 emit() 方法更新 LiveData 的数据

DataBinding(数据绑定)

在 PokemonGo 项目中使用了 DataBinding 进行的数据绑定。

DataBinding(数据绑定)实际上是 XML 布局中的另一个视图结构层次,视图 (XML) 通过数据绑定层不断地与 ViewModel 交互,如下所示: PokemonGo/app/src/main/res/layout/activity_details.xml

<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><variablename="viewModel"type="com.hi.dhl.pokemon.ui.detail.DetailViewModel" /></data>......<androidx.appcompat.widget.AppCompatTextViewandroid:id="@+id/weight"android:text="@{viewModel.pokemon.getWeightString}"/>......</layout>

这是获取神奇宝贝的详细信息,通过 DataBinding 以声明方式将数据(神奇宝贝的体重)绑定到界面上,更多使用参考项目中的代码。

如何处理 ViewModel 的三种方式

如果不使用数据绑定,在 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 的小伙伴们,入门的门槛也是非常低的,强烈建议至少体验一次,体验过之后,我认为你会跟我一样爱上它的。

神奇宝贝 (PokemonGo) 基于 Jetpack + MVVM + Repository + Data Mapper + Kotlin Flow 的实战项目,我也正在为 PokemonGo 项目设计更多的场景,也会加入更多的 Jetpack 成员,可以点击下方链接前往查看。

PokemonGo 仓库地址:https://github.com/hi-dhl/PokemonGo

结语

全文到这里就结束了,如果有帮助 点个赞 就是对我最大的鼓励!

致力于分享最新技术原创文章,涉及 Kotlin、Jetpack、算法、译文、系统源码相关的文章


最后推荐我一直在更新维护的项目和网站:

  • 计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,欢迎前去查看:

AndroidX-Jetpack-Practice​github.com

  • LeetCode / 剑指 offer / 国内外大厂面试题 / 多线程 题解,语言 Java 和 kotlin,包含多种解法、解题思路、时间复杂度、空间复杂度分析
    • 剑指 offer 及国内外大厂面试题解:在线阅读
    • LeetCode 系列题解:在线阅读
  • 最新 Android 10 源码分析系列文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,仓库持续更新,欢迎前去查看

hi-dhl/Android10-Source-Analysis​github.com

  • 整理和翻译一系列精选国外的技术文章,每篇文章都会有译者思考部分,对原文的更加深入的解读,仓库持续更新,欢迎前去查看

Technical-Article-Translation​github.com

  • 「为互联网人而设计,国内国外名站导航」涵括新闻、体育、生活、娱乐、设计、产品、运营、前端开发、Android 开发等等网址,欢迎前去查看

Hi World | 为互联网人而设计的国内国外名站导航​site.51git.cn

sharedpreferences使用方法_Google 推荐在 MVVM 架构中使用 Kotlin Flow相关推荐

  1. Android开发之关于MVVM架构中视图数据绑定框架dataBinding的基本用法

    dataBinding是Google官方开发的第三方视图数据绑定框架.优缺点如下: 优点:很好用 缺点:调试bug不易,部分AS版本中不太友好 首先说下如何使用: 在gradle中的android模块 ...

  2. MVVM架构之自动增删改的极简RecycleView的实现

    先上个源代码的链接:github.com/whenSunSet/- RecycleView是Google替代ListView的一种方案,其有着很高的解耦度,让许多开发者抛弃了以往的ListView,那 ...

  3. Android的MVVM架构的单Activity应用实践

    前言 谈Android架构大家很容易想到MVC.MVP和MVVM. 1.MVC 首先分析一下上面各层之前对应的Android代码,layout.xml里面的xml文件就对应于MVC的view层,里面都 ...

  4. Android MVVM 架构应用实现

    以前项目中虽然也使用MVVM架构,但由于整体框架不是我自己搭建的,导致我对于MVVM架构的整体还是很不熟悉,所以这次就自己搭建并实现一次MVVM架构. MVVM架构使用的组件有ViewModel.Li ...

  5. 《Android构建MVVM》系列(一) 之 MVVM架构快速入门

    前言 本文属于<Android构建MVVM>系列开篇,共六个篇章,详见目录树. 该系列文章旨在为Android的开发者入门MVVM架构,掌握其基本开发模式. 辅以讲解Android Arc ...

  6. android数据流分类,【Android工程之类】1 MVVM架构 - MVVM与单向数据流

    前言 这个系列将讲述使用MVVM架构.LiveData.Room.Kodein.Retrofit.EventBus来建立一个统一的.优雅的.可维护的TODO程序,本系列分为多个章节,从0开始一步一步引 ...

  7. 深入解析MVVM架构

    mvvm 前言 我想做Android开发的大佬都用过MVP,在谷歌没有提出MVVM这种架构模式的时候,大多数开发者都是从传统的MVC过度到MVP的,的确MVP相对MVC来说的确有了很大的改进,具体改进 ...

  8. 大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid客户端

    前言:苟有恒,何必三更眠五更起:最无益,莫过一日曝十日寒. 前言 之前一直想写个 WanAndroid 项目来巩固自己对 Kotlin+Jetpack+协程 等知识的学习,但是一直没有时间.这里重新行 ...

  9. 浅析MVC、MVP、MVVM 架构

    MVC 模型 Model 指数据逻辑和实体模型 View 指布局文件 Controllor 指Activity,既要负责页面的展示和交互,还得负责数据的请求和业务逻辑之类的工作. 看起来MVC架构很清 ...

最新文章

  1. Nginx 源码编译安装
  2. vue从创建到完整的饿了么(12)miste.vue
  3. Spring Cloud--Honghu Cloud分布式微服务云系统—System系统管理
  4. php编码 js解码,浅谈php和js中json的编码和解码
  5. win7怎么运行linux,win7系统运行linux shell脚本的操作方法
  6. 有关fwrite语句的用法
  7. 性能测试总结(一)---基础理论篇(转载)
  8. Mina网络通信框架
  9. 第三章 续:时间控件(TimePicker)
  10. 计算机知识点背诵了就忘了怎么办,背得滚瓜烂熟的知识点,为什么一上考场全忘了?这样做,事半功倍…...
  11. Asp组件初级入门与精通系列之六
  12. UVA344 UVALive5452 Roman Digititis【Ad Hoc】
  13. mybatis框架搭建学习初步
  14. INFOR ERP LN 创建表
  15. jhu研究生录取 计算机,背景一般获约翰霍普金斯大学JHU信息安全硕士录取
  16. Android Studio真机测试
  17. 用PS制作一只梦幻的小鹿插画
  18. 七彩虹SL500 闪迪05141开卡
  19. 「TShark学习」TShark抓包笔记
  20. 八、基于多源数据建成区提取——Landsat数据大气校正

热门文章

  1. javascript的Foreach语法
  2. JS中的关于类型转换的性能优化
  3. leetcode 76 python
  4. php7慢,php-finfo在7.3和7.2上明显慢
  5. 递归下降分析法的基本思想。_还不懂这八大算法思想,刷再多题也白搭!
  6. 集结号!四大国产开源数据库共聚申城,共话未来技术演进
  7. 【广州】openGauss Meetup (12月19日)| 活动预告
  8. 云图说|初识云数据库GaussDB(for Cassandra)
  9. 安全开发Java:日志注入,并没那么简单
  10. 如何只用一个小时定制一个行业AI 模型?