Kotlin Flow:LiveData 该退休了,Flow 才是数据流的正确打开方式

3 阅读8分钟

Kotlin Flow:LiveData 该退休了,Flow 才是数据流的正确打开方式

说实话,我从 LiveData 迁移到 Flow 的过程挺痛苦的。LiveData 用了三年,顺手得很,突然让我换成 Flow,心里一百个不情愿。但真用上之后才发现——回不去了。这篇文章就是我踩完坑之后的总结,帮你少走弯路。

1. 先搞清楚:Flow 到底是个啥

一句话:Flow 就是协程里的数据流,能持续、多次给你发送数据

类比一下:

  • suspend 函数 → 买一部电影,下载完看一次就结束,只返回一次结果
  • Flow → 看直播,主播持续推送画面,你能一直收到新内容,多次、持续发送数据

项目里的例子:

  • 用 suspend 函数:请求一次新闻列表,拿到结果就结束
  • 用 Flow:监听新闻列表的变化,数据库/接口有新数据就自动推送,UI 自动更新

2. 为什么一定要从 LiveData 迁移到 Flow

直接说痛点:

  1. LiveData 和协程配合很别扭:setValue 只能在主线程,postValue 又有丢数据的坑,协程里用起来各种扭捏
  2. LiveData 操作符少得可怜:就 map、switchMap 那几个,稍微复杂点的数据流处理就捉襟见肘
  3. LiveData 只能管 UI 状态:Repository 层用 LiveData?不合适,它天生就是给 UI 设计的
  4. Google 官方都推荐 Flow 了:Jetpack 全家桶全面支持 Flow,这是方向

表格

对比维度LiveDataFlow
底层本质数据持有者 + 观察者模式协程 + 数据流
生命周期感知✅ 原生自动支持需手动 repeatOnLifecycle
操作符数量极少(map、switchMap)超过100个
线程切换固定主线程flowOn 随意切
异常处理弱,直接崩溃catch 操作符捕获
背压支持✅ 原生支持
适用场景简单 UI 状态全场景

3. 冷流 vs 热流:最核心的概念

这是 Flow 里最容易搞混的,必须先搞懂。

冷流(默认的 Flow)

你不 collect,它就不干活;你 collect 一次,它就重新执行一次。

类比点外卖:你不点单(不 collect),商家不会做饭(不执行 emit);你下一次单,做一份饭;再下一次单,再做一份。

val newsFlow = flow {
    println("开始请求新闻")
    emit(ApiResponse.Loading)
    val data = api.getNewsList()
    emit(ApiResponse.Success(data))
}

// 第一次 collect:执行一次,打印"开始请求新闻"
newsFlow.collect { println("第一次收到:$it") }

// 第二次 collect:重新执行,再次打印"开始请求新闻"
newsFlow.collect { println("第二次收到:$it") }

热流(StateFlow / SharedFlow)

不管你订不订阅,它都在发数据;多个订阅者共享同一份数据。

类比看电视:不管你开不开电视,电视台都在播;你打开电视只能收到当前播的内容,收不到之前的。

// 热流:即使还没人 collect,也能存数据
val stateFlow = MutableStateFlow<ApiResponse<List<News>>>(ApiResponse.Loading)

// 先设置数据,此时还没有订阅者
stateFlow.value = ApiResponse.Success(listOf(News("新闻1")))

// 订阅:直接收到最新值
stateFlow.collect { println("收到:$it") }

一张表记住区别

表格

特性冷流(Flow)热流(StateFlow/SharedFlow)
不 collect 时不执行任何代码照常运行
多次 collect每次都重新执行共享同一份数据
类比点外卖看电视
用途网络请求、数据库查询UI 状态、事件通知

4. Flow 基础用法

4.1 创建 Flow(4种方式)

// 方式1:flow { } 构建器(最常用,90%场景用它)
fun getNewsFlow(): Flow<ApiResponse<List<News>>> = flow {
    emit(ApiResponse.Loading)
    val data = api.getNewsList()
    emit(ApiResponse.Success(data))
}

// 方式2:flowOf() 快速创建固定数据
val numberFlow = flowOf(1, 2, 3)

// 方式3:asFlow() 集合转 Flow
listOf(News("新闻1"), News("新闻2")).asFlow()

// 方式4:channelFlow 并发发送数据(进阶用)
channelFlow {
    send(1)
    send(2)
}

4.2 收集 Flow

class NewsViewModel : ViewModel() {
    fun loadNews() {
        viewModelScope.launch {
            getNewsFlow().collect { state ->
                when (state) {
                    is ApiResponse.Loading -> { /* 显示加载 */ }
                    is ApiResponse.Success -> { /* 显示列表 */ }
                    is ApiResponse.Error -> { /* 提示错误 */ }
                }
            }
        }
    }
}

三个注意点

  1. collect 是挂起函数,必须在协程里调用
  2. collect 会阻塞当前协程,后面的代码要等 Flow 结束才执行
  3. 冷流每次 collect 都会重新执行

5. flowOn:线程切换的核心

这是 Flow 最灵活的地方,也是新手最容易迷糊的。

一句话记住:flowOn 只切换它「上面」的代码的线程,绝对不影响它「下面」的代码。

fun getNewsFlow(): Flow<ApiResponse<List<News>>> {
    return flow {
        // 【IO 子线程】受 flowOn 影响
        emit(ApiResponse.Loading)
        val data = api.getNewsList()   // 网络请求,在 IO 线程
        emit(ApiResponse.Success(data))
        
    // flowOn 切换上游到 IO 线程
    }.flowOn(Dispatchers.IO)
     .catch { e ->
         // 【主线程】捕获异常
         emit(ApiResponse.Error(e.message ?: "请求失败"))
     }
}

// ViewModel 中 collect —— 【主线程】
viewModelScope.launch {
    getNewsFlow().collect { state ->
        // 这里在主线程,可以直接更新 UI
        updateUI(state)
    }
}

和 LiveData 的对比:LiveData 观察者回调永远在主线程,写法简单但不灵活。Flow 的 flowOn 可以让你精确控制哪段代码跑在哪个线程,比 LiveData 的 setValue/postValue 靠谱多了。

6. Flow 操作符实战

Flow 有上百个操作符,这里只讲最常用的。

转换操作符

// map:转换数据
userFlow.map { user -> user.name }

// filter:过滤
userFlow.filter { it.age > 18 }

// take:取前 N 个
numberFlow.take(3)

// debounce:防抖(搜索场景必备)
searchQuery.debounce(300)

// distinctUntilChanged:去重,连续相同值只发一次
stateFlow.distinctUntilChanged()

// flatMapLatest:切换最新数据源(替代 switchMap)
categoryFlow.flatMapLatest { category ->
    repository.getNewsByCategory(category)
}

异常处理

fun getNewsFlow(): Flow<ApiResponse<List<News>>> = flow {
    emit(ApiResponse.Success(api.getNewsList()))
}.catch { e ->
    // 捕获上游所有异常,不会让 App 崩溃
    emit(ApiResponse.Error(e.message ?: "未知错误"))
}.onCompletion {
    // Flow 结束时回调(无论成功还是异常)
    hideLoading()
}

对比 LiveData:LiveData 没有内建的异常处理机制,异常直接抛出去可能崩 App。Flow 的 catch 操作符让你优雅地处理错误。

7. StateFlow vs SharedFlow

StateFlow:替代 LiveData 的 UI 状态管理

StateFlow 是热流,始终持有最新值,新订阅者立即收到当前状态——行为和 LiveData 非常像。

class NewsViewModel : ViewModel() {
    // 私有:可变
    private val _uiState = MutableStateFlow<ApiResponse<List<News>>>(ApiResponse.Loading)
    // 公开:只读
    val uiState: StateFlow<ApiResponse<List<News>>> = _uiState.asStateFlow()

    fun loadNews() {
        viewModelScope.launch {
            val result = api.getNewsList()
            _uiState.value = ApiResponse.Success(result)
        }
    }
}

和 LiveData 的写法几乎一模一样,只是 MutableLiveData 换成了 MutableStateFlowLiveData 换成了 StateFlow

SharedFlow:一次性事件的正确姿势

SharedFlow 专门用来发一次性事件(Toast、页面跳转等),这是 LiveData 搞不好的场景。

class NewsViewModel : ViewModel() {
    // replay = 0:新订阅者收不到之前的事件
    private val _eventFlow = MutableSharedFlow<String>(replay = 0)
    val eventFlow: SharedFlow<String> = _eventFlow.asSharedFlow()

    fun collectNews(news: News) {
        viewModelScope.launch {
            val result = dao.collectNews(news)
            if (result) {
                _eventFlow.emit("收藏成功")
            }
        }
    }
}

什么时候用哪个

表格

场景用什么为什么
UI 状态(列表数据、加载状态)StateFlow始终有值,新订阅者收到最新状态
一次性事件(Toast、导航)SharedFlowreplay=0,不会重复消费
网络请求/数据库查询冷流 Flow按需执行,不浪费资源

踩坑提醒:千万别用 StateFlow 发 Toast 事件!页面旋转重建时,StateFlow 会把最新值再推一次,Toast 就会重复弹。用 SharedFlow + replay=0 才对。

8. UI 层怎么收集 Flow

这是从 LiveData 迁移过来最大的变化——Flow 原生不感知 Android 生命周期,必须手动绑定。

写法对比

// LiveData:自动感知生命周期,一行搞定
viewModel.users.observe(this) { users ->
    updateUI(users)
}

// Flow:必须用 repeatOnLifecycle 绑定生命周期
viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            updateUI(state)
        }
    }
}

为什么必须用 repeatOnLifecycle

  • 页面 STARTED → 开始收集
  • 页面 STOPPED → 停止收集(省资源)
  • 页面 DESTROYED → 取消协程(防泄漏)

效果和 LiveData 完全一致,只是写法多几行。但这个"多几行"换来了更强大的数据流处理能力,值得。

9. 全流程实战:Flow 处理网络请求

从 Repository 到 ViewModel 到 UI,一套完整的 Flow 网络请求方案。

Repository 层

class NewsRepository @Inject constructor(
    private val api: NewsApi,
    private val dao: NewsDao
) {
    fun loadNewsFlow(category: String, page: Int): Flow<ApiResponse<List<News>>> {
        return flow {
            emit(ApiResponse.Loading)

            // 先读缓存,快速显示
            val cacheList = dao.getNewsByCategory(category)
            if (cacheList.isNotEmpty()) {
                emit(ApiResponse.Success(cacheList))
            }

            // 再请求网络
            val netList = api.getNewsList(category, page, 20)
            dao.insertNewsList(netList)
            emit(ApiResponse.Success(netList))

        }.flowOn(Dispatchers.IO)  // 耗时操作全在 IO 线程
         .catch { e ->
             emit(ApiResponse.Error(e.message ?: "网络请求失败"))
         }
    }
}

ViewModel 层

@HiltViewModel
class NewsViewModel @Inject constructor(
    private val repository: NewsRepository
) : ViewModel() {
    // UI 状态用 StateFlow
    private val _uiState = MutableStateFlow<ApiResponse<List<News>>>(ApiResponse.Loading)
    val uiState: StateFlow<ApiResponse<List<News>>> = _uiState.asStateFlow()

    // 一次性事件用 SharedFlow
    private val _eventFlow = MutableSharedFlow<String>()
    val eventFlow: SharedFlow<String> = _eventFlow.asSharedFlow()

    fun loadNews(category: String = "推荐") {
        viewModelScope.launch {
            repository.loadNewsFlow(category, 1).collect { state ->
                _uiState.value = state
            }
        }
    }

    fun collectNews(news: News) {
        viewModelScope.launch {
            repository.toggleCollect(news)
            _eventFlow.emit("收藏成功")
        }
    }
}

UI 层

@AndroidEntryPoint
class NewsFragment : Fragment() {
    private val viewModel: NewsViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // 观察 UI 状态
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect { state ->
                    when (state) {
                        is ApiResponse.Loading -> showLoading()
                        is ApiResponse.Success -> showList(state.data)
                        is ApiResponse.Error -> showError(state.message)
                    }
                }
            }
        }

        // 观察一次性事件
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.eventFlow.collect { message ->
                    showToast(message)
                }
            }
        }
    }
}

10. 新手必踩的 5 个坑

坑1:忘记加 flowOn,主线程网络请求直接 ANR

// ❌ 错误:flow 里的网络请求没有 flowOn
flow { emit(api.getNewsList()) }  // 主线程执行网络请求,ANR!

// ✅ 正确:加 flowOn
flow { emit(api.getNewsList()) }.flowOn(Dispatchers.IO)

坑2:同一个 launch 里写两个 collect,第二个永远执行不到

// ❌ 错误:collect 是挂起函数,会阻塞
viewModelScope.launch {
    flow1.collect { }   // 阻塞在这里
    flow2.collect { }   // 永远执行不到
}

// ✅ 正确:每个 collect 单独开协程
viewModelScope.launch {
    launch { flow1.collect { } }
    launch { flow2.collect { } }
}

坑3:用 StateFlow 发一次性事件,页面旋转重复弹 Toast

// ❌ 错误
private val _toast = MutableStateFlow("")  // 页面重建会重新收到
// ✅ 正确
private val _toast = MutableSharedFlow<String>(replay = 0)  // 只消费一次

坑4:在 GlobalScope 里收集 Flow,页面销毁后还在跑

// ❌ 错误
GlobalScope.launch { flow.collect { } }  // 内存泄漏
// ✅ 正确
viewModelScope.launch { flow.collect { } }  // 页面销毁自动取消

坑5:冷流每次 collect 都重复请求网络

kotlin

// ❌ 问题:按钮点击多次 = 请求多次
button.setOnClickListener {
    lifecycleScope.launch { newsFlow.collect { } }  // 每次都重新执行
}

// ✅ 方案:用 shareIn 把冷流转成热流,多订阅者共享
val sharedNewsFlow = newsFlow.shareIn(
    viewModelScope,
    SharingStarted.WhileSubscribed(5000),
    replay = 1
)

总结

表格

概念一句话
Flow 冷流你不 collect 它就不干活,每次 collect 重新执行
StateFlow 热流替代 LiveData 管 UI 状态,始终有值
SharedFlow 热流发一次性事件,replay=0 不重复消费
flowOn只切上游线程,下游不管
catch捕获上游异常,不会崩 App
repeatOnLifecycle给 Flow 加上和 LiveData 一样的生命周期安全

迁移建议:新项目直接上 Flow,老项目 Repository 层先迁 Flow,ViewModel 层的 LiveData 可以逐步替换成 StateFlow,不用一次性全改。