渐进式编程课
KMP 目录
第 41 课2026.08.27

搜索请求发出后、结果返回前,页面需要显示正在加载。把 Loading 也放进 Flow,可以让界面只渲染统一的 SearchUiState,不必额外维护加载开关。

第 41 课:onStart 发送加载状态

知识点

使用 onStart() 在上游 Flow 开始收集前,先发送一个加载状态。

用途

搜索请求发出后、结果返回前,页面需要显示正在加载。把 Loading 也放进 Flow,可以让界面只渲染统一的 SearchUiState,不必额外维护加载开关。

核心概念

  • onStart 的代码会在上游 Flow 开始收集前执行。
  • onStart 的接收者是 FlowCollector,因此可以调用 emit()。
  • 操作符位置决定触发范围:放在每个搜索 Flow 内部,才能让每次新搜索都发送 Loading。
  • 随后的搜索结果会继续从同一条 Flow 发出。
  • 如果新搜索取消旧搜索,新的搜索 Flow 仍会执行自己的 onStart。

示例代码

假设 normalizedQuery 是昨天已经规范化并过滤重复值的搜索词 Flow:

kotlin
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart

data class SearchItem(val id: Long, val title: String)

sealed interface SearchUiState {
    data object Idle : SearchUiState
    data object Loading : SearchUiState
    data class Content(val items: List<SearchItem>) : SearchUiState
    data class Error(val message: String) : SearchUiState
}

interface SearchRepository {
    fun search(query: String): Flow<List<SearchItem>>
}

@OptIn(ExperimentalCoroutinesApi::class)
fun observeSearch(
    normalizedQuery: Flow<String>,
    repository: SearchRepository,
): Flow<SearchUiState> = normalizedQuery
    .flatMapLatest { value ->
        if (value.isBlank()) {
            flowOf(SearchUiState.Idle)
        } else {
            repository.search(value)
                .map<List<SearchItem>, SearchUiState> { items ->
                    SearchUiState.Content(items)
                }
                .onStart {
                    emit(SearchUiState.Loading)
                }
                .catch { error ->
                    emit(SearchUiState.Error(error.message ?: "搜索失败"))
                }
        }
    }

每个非空搜索词都会先发送 Loading,随后发送 Content;发生异常时,则由之前学过的 catch() 发送 Error。

5 分钟练习

补全下面的代码,让商品 Flow 在开始加载前先发送 ProductUiState.Loading:

kotlin
val state: Flow<ProductUiState> = repository.observeProducts()
    .map<List<Product>, ProductUiState> { products ->
        ProductUiState.Content(products)
    }
    ._________ {
        _________(ProductUiState.Loading)
    }

参考答案

kotlin
val state: Flow<ProductUiState> = repository.observeProducts()
    .map<List<Product>, ProductUiState> { products ->
        ProductUiState.Content(products)
    }
    .onStart {
        emit(ProductUiState.Loading)
    }

Loading 会先发出,然后才开始收集 observeProducts() 的结果。

与上一课的联系

上一课用 distinctUntilChanged() 阻止重复搜索词继续向下游传递;今天为每个真正开始的新搜索加入 onStart(),让加载状态与搜索结果来自同一条数据流。

参考资料