ow 是一种基于流的编程模型,本文我们将向大家介绍响应式编程以及其在 Android 开发中的实践,您将了解到如何将生命周期、旋转及切换到后台等状态绑定到 Flow 中,并且测试它们是否能按照预。..
Flow 是一种基于流的编程模型,本文我们将向大家介绍响应式编程以及其在 Android 开发中的实践,您将了解到如何将生命周期、旋转及切换到后台等状态绑定到 Flow 中,并且测试它们是否能按照预期执行。
如果您更喜欢通过视频了解此内容,请 点击此处 查看。
一、Kotlin Flow实战
1.1 单向数据流
△ 加载数据流的过程
每款 Android 应用都需要以某种方式收发数据,比如从数据库获取用户名、从服务器加载文档,以及对用户进行身份验证等。接下来,我们将介绍如何将数据加载到 Flow,然后经过转换后暴露给视图进行展示。
1 | 为了大家更方便地理解 Flow,我们以 Pancho (潘乔) 的故事来展开。当住在山上的 Pancho 想从湖中获取淡水时,会像大多数新手一开始一样,拿个水桶走到湖边取水,然后再走回来。 |
△ 山上的 Pancho
但有时 Pahcho 不走运,走到湖边时发现湖水已经干涸,于是就不得不再去别处寻找水源。发生了几次这种情况后,Pancho 意识到,搭建一些基础设施可以解决这个问题。于是他在湖边安装了一些管道,当湖中有水时,只用拧开水龙头就能取到水。知道了如何安装管道,就能很自然地想到从多个水源地把管道组合,这样一来 Pancho 就不必再检查湖水是否已经干涸。
△ 铺设管道
在 Android 应用中您可以简单地在每次需要时请求数据,例如我们可以使用挂起函数来实现在每次视图启动时向 ViewModel 请求数据,而后 ViewModel 又向数据层请求数据,接下来这一切又在相反的方向上发生。不过这样过了一段时间之后,像 Pancho 这样的开发者们往往会想到,其实有必要投入一些成本来构建一些基础设施,我们就可以不再请求数据而改为观察数据。观察数据就像安装取水管道一样,部署完成后对数据源的任何更新都将自动向下流动到视图中,Pancho 再也不用走到湖边去了。
△ 传统的请求数据与单向数据流
1.2 响应式编程
我们将这类观察者会自动对被观察者对象的变化而作出反应的系统称之为响应式编程,它的另一个设计要点是保持数据只在一个方向上流动,因为这样更容易管理且不易出错。
某个示例应用界面的 “数据流动” 如下图所示,身份认证管理器会告诉数据库用户已登录,而数据库又必须告诉远程数据源来加载一组不同的数据;与此同时这些操作在获取新数据时都会告诉视图显示一个转圈的加载图标。对此我想说这虽然是可行的,但容易出现错误。
△ 错综复杂的 “数据流动”
1 | 更好的方式则是让数据只在一个方向上流动,并创建一些基础设施 (像 Pancho 铺设管道那样) 来组合和转换这些数据流,这些管道可以随着状态的变化而修改,比如在用户退出登录时重新安装管道。 |
△ 单向数据绑定
1.3 使用 Flow
可以想象对于这些组合和转换来说,我们需要一个成熟的工具来完成这些操作。在本文中我们将使用 Kotlin Flow 来实现。Flow 并不是唯一的数据流构建器,不过得益于它是协程的一部分并且得到了很好的支持。我们刚才一直用作比喻的水流,在协程库里称之为 Flow 类型,我们用泛形 T 来指代数据流承载的用户数据或者页面状态等任何类型。
△ 生产者和消费者
1 | 生产者会将数据 emit (发送) 到数据流中,而消费者则从数据流中 collect (收集) 这些数据。在 Android 中数据源或存储区通常是应用数据的生产者;消费者则是视图,它会把数据显示在屏幕上。 |
大多数情况下您都无需自行创建数据流,因为数据源中依赖的库,例如 DataStore、Retrofit、Room 或 WorkManager 等常见的库都已经与协程及 Flow 集成在一起了。这些库就像是水坝,它们使用 Flow 来提供数据,您无需了解数据是如何生成的,只需 “接入管道” 即可。
△ 提供 Flow 支持的库
我们来看一个 Room 的例子。您可以通过导出指定类型的数据流来获取数据库中发生变更的通知。在本例中,Room 库是生产者,它会在每次查询后发现有更新时发送内容。
1 |
|
创建 Flow
如果您要自己创建数据流,有一些方案可供选择,比如数据流构建器。假设我们处于UserMessagesDataSource
1 | 中,当您希望频繁地在应用内检查新消息时,可以将用户消息暴露为消息列表类型的数据流。我们使用数据流构建器来创建数据流,因为 Flow 是在协程上下文环境中运行的,它以挂起代码块作为参数,这也意味着它能够调用挂起函数,我们可以在代码块中使用 while(true)来循环执行我们的逻辑。 |
1 | class UserMessagesDataSource( |
}
1 |
|
收集 Flow
现在我们已经了解过如何生成和修改数据流,接下来了解一下如何收集数据流。收集数据流通常发生在视图层,因为这是我们想要在屏幕上显示数据的地方。
在本例中,我们希望列表中能够显示最新消息以便 Pancho 能够了解最新动态。我们可以使用终端运算符collect
来监听数据流发送的所有值,
接收一个函数作为参数,每个新值都会调用该参数,并且由于它是一个挂起函数,因此需要在协程中执行。
1 | userMessages.collect { messages -> |
在 Flow 中使用终端运算符将按需创建数据流并开始发送值,而相反的是中间操作符只是设置了一个操作链,其会在数据被发送到数据流时延迟执行。每次对
1 | userMessages |
1 | 时都会创建一个新的数据流,其生产者代码块将根据自己的时间间隔开始刷新来自 API 的息。在协程中我们将这种按需创建并且只有在被观察时才会发送数据的数据流称之为 冷流 (Cold Stream)。 |
1.4 在 Android 视图上收集数据流
在 Android 的视图中收集数据流要注意两点,第一是在后台运行时不应浪费资源,第二是配置变更。
安全收集
假设我们在MessagesActivity
中,如果希望在屏幕上显示消息列表,则应该当界面没有显示在屏幕上时停止收集,就像是 Pancho 在刷牙或者睡觉时应该关上水龙头一样。我们有多种具有生命周期感知能力的方案,来实现当信息不在屏幕上展示就不从数据流中收集信息的功能,比如androidx.lifecycle:lifecycle-runtime-ktx
1 | 包中的 `Lifecycle.repeatOnLifecycle(state)` 和 `Flow<T>.flowWithLifecycle(lifecycle, state)` 。您还可以在 ViewModel 中使用 |
androidx.lifecycle:lifecycle-livedata-ktx
1 | 包里的 `Flow<T>.asLiveData(): LiveData` 将数据流转换为 LiveData,这样就可以像往常一样使用 LiveData 来实现这件事情。不过为了简单起见,这里推荐使用 |
repeatOnLifecycle
从界面层收集数据流。
是一个接收Lifecycle.State
作为参数的挂起函数,该 API 具有生命周期感知能力,所以能够在当生命周期进入响应状态时自动使用传递给它的代码块启动新的协程,并且在生命周期离开该状态时取消该协程。在上面的例子中,我们使用了 Activity 的lifecycleScope
来启动协程,由于
是挂起函数,所以它需要在协程中被调用。最佳实践是在生命周期初始化时调用该函数,就像上面的例子中我们在 Activity 的onCreate
中调用一样:
1 | import androidx.lifecycle.repeatOnLifecycle |
1 | class MessagesActivity : AppCompatActivity() { |
1 | val viewModel: MessagesViewModel by viewModels() |
1 | override fun onCreate(savedInstanceState: Bundle?) { |
1 | lifecycleScope.launch { |
}
1 | // 协程将会在 lifecycle 进入 DESTROYED 后被恢复 |
}
1 | 的可重启行为充分考虑了界面的生命周期,不过需要注意的是,直到生命周期进入 |
为了能够直观地展示具体的运作过程,我们来探索一下此 Activity 的生命周期,首先是创建完成并向用户可见;接下来用户按下了主屏幕按钮将应用退到后台,此时 Activity 会收到
onStop信号;当重新打开应用时又调用```text onStart。如果您调用text repeatOnLifecycle `并传入text
STARTED
1 | 状态,界面就只会在屏幕上显示时收集数据流发出的信号,并且在应用转到后台时取消收集。 |
配置变更
当您向视图暴露数据流时,必须要考虑到您正在尝试在具有不同生命周期的两个元素之间传递数据,并不是所有生命周期都会出现问题,但在 Activity 和 Fragment 的生命周期里会比较棘手。当设备旋转或者接收到配置变更时,所有的 Activity 都可能会重启但 ViewModel 却能被保留,因此您不能把任意数据流都简单地从 ViewModel 中暴露出来。
△ 旋转屏幕会重建 Activity 但能够保留 ViewModel
1 | 以如下代码中的冷流为例,由于每次收集冷流时它都会重启,所以在设备旋转之后会再次调用 `repository.fetchItem()` 。我们需要某种缓冲区机制来保障无论重新收集多少次都可以保持数据,并在多个收集器之间共享数据,而 |
StateFlow
正是为了此用途而设计的。在我们的湖泊比喻中,
就好比水箱,即使没有收集器它也能持有数据。因为它可以多次被收集,所以能够放心地将其与 Activity 或 Fragment 一起使用。
1 | val result: Flow<Result<UiState>> = flow { |
将接收来自上游数据流的所有更新并存储最新的值,并且收集器的数量可以是 0 至任意多个,因此非常适合与
ViewModel
`一起使用。当然,除此之外还有一些其他类型的 Flow,但推荐您使用```text
1 | ,因为我们可以对它进行非常精确的优化。 |
1 | 我们来看看这两个场景: 第一种场景是旋转,在该场景中 Activity (也就是数据流收集器) 在短时间内被销毁然后重建;第二个场景是回到主屏幕,这将会使我们的应用进入后台。在旋转场景中我们不希望重启任何数据流以便尽可能快地完成过渡,而在回到主屏幕的场景中我们则希望停止所有数据流以便节省电量和其他资源。 |
会经过我们设置的超时时间之后才会停止其上游数据流,如果用户再次打开应用则会自动重启上游数据流。而在旋转场景中视图只停止了很短的时间,无论如何都不会超过 5 秒钟,因此 StateFlow 并不会重启,所有的上游数据流都将会保持在活跃状态,就像什么都没有发生一样可以做到即时向用户呈现旋转后的屏幕。
△ 设置超时时间来应对不同的场景
总的来说,建议您使用
来通过ViewModel
暴露数据流,或者使用asLiveData
来实现同样的目的,关于
或其父类SharedFlow
的更多详细信息,请参阅: StateFlow 和 SharedFlow。
1.5 测试数据流
测试数据流可能会比较复杂,因为要处理的对象是流式数据,这里介绍在两个不同的场景中有用的小技巧:
首先是第一个场景,被测单元依赖了数据流,那对此类场景进行测试最简单的方法就是用模拟生产者替代依赖项。在本例中,您可以对这个模拟源进行编程以对不同的测试用例发送其所需要的内容。您可以像上面的例子一样实现一个简单的冷流,测试本身会对受测对象的输出进行断言,输出的内容可以是数据流或其他任何类型。
△ 测单元依赖数据流的测试技巧
模拟被测单元所依赖的数据流:
1 | class MyFakeRepository : MyRepository { |
}
1 |
|
1.6 回顾
感谢阅读本文,希望您通过本文内容已经了解到为什么响应式架构值得投资,以及如何使用 Kotlin Flow 构建您的基础设施。文末提供了有关这方面的资料,包括涵盖基础知识的指南以及深入探讨某些主题的文章。另外您还可以通过 Google I/O 应用了解这些内容的详细信息,我们在早些时候为其更新了很多有关数据流的内容。
指南: Android 上的 Kotlin 数据流
使用更为安全的方式收集 Android UI 数据流
从 LiveData 迁移到 Kotlin 数据流
Flow 操作符 shareIn 和 stateIn 使用须知
设计 repeatOnLifecycle API 背后的故事
示例代码: Google I/O 应用
欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!
本文标题: 实战使用KotlinFlow构建
发布时间: 2019年09月01日 00:00
最后更新: 2025年12月30日 08:54
原始链接: https://haoxiang.eu.org/76893bb7/
版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0许可协议,转载请注明出处!

