第 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(),让加载状态与搜索结果来自同一条数据流。