Android 中的 flow
在协程中,flow 是一种可以依次发出多个值的类型,比如你可以使用 flow 来接收数据库的实时更新,而挂起函数只能返回一个值。
flow 是建立在协程的基础之上的,在概念上,一个 flow 就是一条可以异步计算的数据流,数据流的数据必须是同一个类型的。比如 Flow<Int> 就是发射整形数据的流。
同样是产生一个数据的序列,flow 和 Iterator 很像,但是 flow 使用挂起函数来异步生产和消费数据,这意味着 flow 可以安全地通过执行网络请求来产生新数据,而不会阻塞主线程。
这里有 3 个数据流相关的实体:
- Producer(生产者),生产数据添加给流。
- Intermediary(中介,可选的),它可以修改发给流的数据,或者修改流本身。
- Consumer(消费者),消费来自流的数据。
创建一个 flow
在下面的示例中,数据源以一个固定的频率自动获取最新的新闻,由于挂起函数不能返回多个连续的数据,数据源创建并返回了 flow 来实现该需求:
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val refreshIntervalMs: Long = 5000
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
while(true) {
// 获取新闻
val latestNews = newsApi.fetchLatestNews()
emit(latestNews) // 把结果发送给 flow
delay(refreshIntervalMs) // 延迟 5 秒
}
}
}
// 使用挂起函数进行网络请求的接口
interface NewsApi {
suspend fun fetchLatestNews(): List<ArticleHeadline>
}
flow 构建器在协程中执行,因此,flow 构建器中的代码是异步的,但是也有一些限制:
- 数据流是有序的。由于生产者是一个协程,当调用挂起函数的时候,生产者会挂起直到挂起函数返回。在这个例子当中,生产者会挂起直到 fetchLatestNews() 这个挂起函数执行完成,然后才会把数据发送给流。
- 使用 flow 构建器,生产者不能从另外一个 CoroutineContext 发射值,因此你不能使用 withContext 创建一个新的协程从而在一个不同的 CoroutineContext 中发射值。
Jetpack 中的 flow
Flow 被集成到了许多 Jetpack 库中,并且在 Android 第三方库中很流行,Flow 很适合实时数据更新和无限数据流的场景。
你可以使用 Flow with Room 接收数据库更改的通知. 当使用 data access objects (DAO)时,返回一个 Flow 类型来获取实时更新。
@Dao
abstract class ExampleDao {
@Query("SELECT * FROM Example")
abstract fun getExamples(): Flow<List<Example>>
}
每次当 Example 表有更新的时候,一个新的 List<Example> 就被发射出来了。
StateFlow 和 SharedFlow
StateFlow 和 SharedFlow 可以让流以最优的方式发射 状态更新 和发射 值 给多个消费者。
StateFlow
StateFlow 是一个状态容器(state-holder),它会观察发射 当前 和 新 状态更新给它的收集者的流。当前状态值可以通过它的 value 属性读取到,想要更新状态并发送给这个 flow,可以给 MutableStateFlow 类的 value 属性赋一个新值。
在 Android 中,StateFlow 适合用于需要维护 可观察可变状态(observable mutable state)的类。
接着前面的 Kotlin flows 的例子, StateFlow 可以通过 LatestNewsViewModel 暴露出来,这样 View 就可以监听 UI 状态的更新,同时保证屏幕的状态不受 configuration change 的影响。
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
// Backing property to avoid state updates from other classes
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// The UI collects from this StateFlow to get its state updates
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// Update View with the latest favorite news
// Writes to the value property of MutableStateFlow,
// adding a new element to the flow and updating all
// of its collectors
.collect { favoriteNews ->
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(val exception: Throwable): LatestNewsUiState()
}
负责更新 MutableStateFlow 的类是生产者,所有收集 StateFlow 的类是消费者。与通过 flow 构建器构建的冷流不同,冷流只有在有消费者消费时才会生产数据,StateFlow 是热流:收集热流不会触发生产者的代码的执行。StateFlow 会一直活跃在内存中,只有当垃圾回收根没有引用它的时候(garbage collection root),才会回收它。
当一个新的消费者开始收集 StateFlow ,它会收到此后的所有状态,你会在其他的可观察类中发现这个行为,比如 LiveData。
View 监听 StateFlow 的代码如下:
class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
...
// Start a coroutine in the lifecycle scope
lifecycleScope.launch {
// repeatOnLifecycle launches the block in a new coroutine every time the
// lifecycle is in the STARTED state (or above) and cancels it when it's STOPPED.
repeatOnLifecycle(Lifecycle.State.STARTED) {
// Trigger the flow and start listening for values.
// Note that this happens when lifecycle is STARTED and stops
// collecting when the lifecycle is STOPPED
latestNewsViewModel.uiState.collect { uiState ->
// New value received
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
}
如果需要更新 UI,切勿在 launch 或者 launchIn 函数中直接收集流,因为这样做,即使视图不可见了,这些函数还会处理事件。这种行为可能会导致 App 崩溃。为了避免这种情况,请使用上例中的 repeatOnLifecycle API。
如需将任何 flow 转换为 StateFlow,请使用 stateIn 中间操作符。
StateFlow, Flow, 与 LiveData
StateFlow 与 LiveData 有一些相似点,都是可观察数据的容器类,并且在 App 架构中遵循相似的模式,但是需要注意的是,StateFlow 与 LiveData 有下面这些不一样的表现:
- StateFlow 需要给构造函数赋一个初始状态,而 LiveData 不需要;
- 当 UI 组件进入 STOPPED 状态时, LiveData.observe() 会自动取消消费者的注册,而收集 StateFlow 或其他别的流则不会自动停止收集。要实现相同的行为,你需要在 Lifecycle.repeatOnLifecycle 代码块中收集流。
使用 shareIn 操作符让冷流变成热流
StateFlow 是热流,它在被收集之前,或者 garbage collection root 持有它的引用时,会一直存在于内存当中。你可以使用 shareIn
操作符把冷流变成热流。
拿 Kotlin flows 中的 callbackFlow 举个例子,您可以使用 shareIn 在收集器之间共享从 Firestore 获取到的数据,而不是让每个收集器创建一个新流。你需要传递以下参数:
- 一个 CoroutinScope 用于共享流,这个 scope 应该比任何消费者都存活得更久,以便在需要的时候保持共享流的存活。
- 要重播给每个新收集器的 item 的数量。
- 启动的行为策略。
class NewsRemoteDataSource(...,
private val externalScope: CoroutineScope,
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
...
}.shareIn(
externalScope,
replay = 1,
started = SharingStarted.WhileSubscribed()
)
}
在这个例子中,latestNews 这个流重播最近发射的 item 给一个新的收集器,并且会保持活跃,只要 externalScope 还存活,并且还有其他活跃的收集器。SharingStarted.WhileSubscribed() 启动策略会让上游的生产者保持活跃,只要还有活跃的订阅者。还有其他的启动策略,比如 SharingStarted.Eagerly 会让生产者马上启动,或者 SharingStarted.Lazily 会在第一个订阅者出现后开始共享,并且会让流永远保持活跃。
SharedFlow
shareIn 函数返回的是一个 SharedFlow,它也是热流,它会发射值给所有收集它的消费者,SharedFlow 是 StateFlow 高度可配置的泛化。
不使用 shareIn 你也可以创建 SharedFlow,举个例子,你可以用 SharedFlow 来给 App 里面的代码发送滴答(ticks),这样所有的内容可以同时定期刷新。除了获取最新的新闻,您可能还想用最喜欢的主题集合刷新用户信息栏。在下面的代码片段中,TickHandler 中暴露了一个 SharedFlow,以便其他类知道何时刷新内容。与 “StateFlow” 一样,需要在类中使用 “MutableSharedFlow” 将 items 发送给流。
// Class that centralizes when the content of the app needs to be refreshed
class TickHandler(
private val externalScope: CoroutineScope,
private val tickIntervalMs: Long = 5000
) {
// Backing property to avoid flow emissions from other classes
private val _tickFlow = MutableSharedFlow<Unit>(replay = 0)
val tickFlow: SharedFlow<Event<String>> = _tickFlow
init {
externalScope.launch {
while(true) {
_tickFlow.emit(Unit)
delay(tickIntervalMs)
}
}
}
}
class NewsRepository(
...,
private val tickHandler: TickHandler,
private val externalScope: CoroutineScope
) {
init {
externalScope.launch {
// Listen for tick updates
tickHandler.tickFlow.collect {
refreshLatestNews()
}
}
}
suspend fun refreshLatestNews() { ... }
...
}
您可以通过以下方式自定义 SharedFlow 的行为:
- replay,重发以前发射的值给新的订阅者的次数。
- onBufferOverflow,指定等待发送数据的缓冲区满了的时候的执行策略,默认值是 BufferOverflow.SUSPEND,会让调用者挂起,其他值包括 DROP_LATEST 和 DROP_OLDEST 。
MutableSharedFlow 还有一个 subscriptionCount 属性,其中包含活跃收集器的数量,以便你可以相应地优化业务逻辑。如果您不想重播发送给这个 flow 的最新信息,MutableSharedFlow 还包含一个resetReplayCache 函数。