Kotlin 中的 flow

105 阅读7分钟

Android 中的 flow

在协程中,flow 是一种可以依次发出多个值的类型,比如你可以使用 flow 来接收数据库的实时更新,而挂起函数只能返回一个值。

flow 是建立在协程的基础之上的,在概念上,一个 flow 就是一条可以异步计算的数据流,数据流的数据必须是同一个类型的。比如 Flow<Int> 就是发射整形数据的流。

同样是产生一个数据的序列,flow 和 Iterator 很像,但是 flow 使用挂起函数来异步生产和消费数据,这意味着 flow 可以安全地通过执行网络请求来产生新数据,而不会阻塞主线程。

这里有 3 个数据流相关的实体:

  • Producer(生产者),生产数据添加给流。
  • Intermediary(中介,可选的),它可以修改发给流的数据,或者修改流本身。
  • Consumer(消费者),消费来自流的数据。
image.png

创建一个 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 函数。