Google 推荐在 MVVM 架构中使用 Kotlin Flow

发表于 4年以前  | 总阅读数:943 次

前言

在之前分享过一篇 Jetpack 综合实战应用 神奇宝贝(PokemonGo) 眼前一亮的 Jetpack + MVVM 极简实战 ,这个项目主要包了以下功能:

  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 的使用

我近期也在开发另外一个 Jetpack + MVVM 实战应用,和神奇宝贝(PokemonGo) 有很多不同之处,神奇宝贝(PokemonGo) 主要偏向于 Paging3 的分页处理,以及 Flow 在 MVVM 中的实战。

而今天这篇文章主要来分析一下 神奇宝贝(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 的最新成员 Paging3,在其内部的源码实现也是使用的 Flow,关于 Paging3 的使用可以参考以下链接:

  • Jetpack 成员 Paging3 实践以及源码分析(一)
  • Jetpack 新成员 Paging3 网络实践及原理分析(二)
  • 自定义 RemoteMediator 实现 network + db 的混合使用

不仅仅是 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 那么多傻傻分不清楚的操作符,实际上在项目中常用的可能只有几个例如 ObservableFlowableSingle 等等,如果我们不去了解背后的原理,造成内存泄露是很正常的事,大家可以从 StackOverflow 上查看一下,有很多因为 RxJava 造成内存泄露的例子

  • RxJava 入门的门槛很高,学习过的朋友们,我相信能够体会到从入门到放弃是什么感觉

  • 解决回调地狱的问题

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

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

Kotlin Flow 如何在 MVVM 中使用

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

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

Kotlin Flow 在数据源中的使用

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

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<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 不能直接持有数据源,防止数据源的变化,影响上层的 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 发送的数据。

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> = _pokemon

viewModelScope.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>
        <variable
            name="viewModel"
            type="com.hi.dhl.pokemon.ui.detail.DetailViewModel" />

    </data>

    ......
    <androidx.appcompat.widget.AppCompatTextView
        android: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 GitHub 地址:https://github.com/hi-dhl/PokemonGo

PokemonGo

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,文章中有什么没有写明白的地方,或者有什么更好的建议欢迎留言,欢迎一起来学习,在技术的道路上一起前进。

链接:https://juejin.im/post/5f153adff265da22fb287e6e

本文由哈喽比特于4年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/WknyggP5F8mjRiUcOStn3g

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录