第三部分:Flow 架构实战
第7章:UI 层安全实践:生命周期管理
7.1 为何 Flow 收集需要关注生命周期?
Android UI 组件具有明确的生命周期,错误的 Flow 收集会导致:
| 问题 | 后果 | 影响 |
|---|---|---|
| 内存泄漏 | Activity/Fragment 销毁后仍持有引用 | OOM、应用崩溃 |
| 资源浪费 | 后台不必要的计算和网络请求 | 电池耗尽、流量消耗 |
| UI 异常 | 视图销毁后尝试更新 | IllegalStateException、UI 闪烁 |
| 数据不一致 | 后台更新干扰前台显示 | 显示错误数据、状态混乱 |
错误示例:
kotlin
kotlin
class BadFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ❌ 危险:生命周期不安全的收集
lifecycleScope.launch {
viewModel.dataFlow.collect { data ->
// Fragment 进入后台后,这里仍会执行!
updateUI(data) // 可能引发 IllegalStateException
}
}
}
}
7.2 Google 的终极答案:repeatOnLifecycle 与 flowWithLifecycle
repeatOnLifecycle(推荐)
kotlin
kotlin
class SafeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ✅ 推荐方式 1:使用 repeatOnLifecycle
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 只有当 Fragment 至少处于 STARTED 状态时才收集
viewModel.dataFlow.collect { data ->
updateUI(data)
}
}
// 当生命周期低于 STARTED 时,协程自动取消
}
}
}
flowWithLifecycle(更简洁)
kotlin
kotlin
class SafeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ✅ 推荐方式 2:使用 flowWithLifecycle
viewLifecycleOwner.lifecycleScope.launch {
viewModel.dataFlow
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { data ->
updateUI(data)
}
}
}
}
工作原理:
repeatOnLifecycle:当生命周期进入目标状态时启动新协程,离开时取消flowWithLifecycle:内部使用repeatOnLifecycle,但更简洁的 API
7.3 告别 launchWhenX:新旧写法的风险对比
旧写法的问题(launchWhenX)
kotlin
kotlin
class OldFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ❌ 问题写法:launchWhenX 不会取消 Flow
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
// 问题 1:只是暂停协程,不取消 Flow 收集
// 问题 2:生产者可能继续生产数据,造成缓冲堆积
// 问题 3:恢复时可能错过中间状态
viewModel.dataFlow.collect { data ->
updateUI(data)
}
}
}
}
新旧对比表
| 特性 | launchWhenX | repeatOnLifecycle |
|---|---|---|
| 取消机制 | 暂停协程执行 | 取消整个协程 |
| 生产者状态 | 继续运行(可能导致背压) | 停止收集,生产者可能暂停 |
| 内存使用 | 可能缓冲积压数据 | 无缓冲积压风险 |
| 恢复行为 | 从暂停点继续 | 重新开始收集 |
| 推荐度 | ❌ 不推荐 | ✅ 推荐 |
7.4 最佳实践总结:不同场景下如何安全收集 Flow
📱 核心原则一句话总结:
不同的 Android 组件要用不同的方式收集 Flow,但都要跟随生命周期变化,避免内存泄漏。
7.4.1 Activity 中的安全收集实践
标准写法(最安全)
kotlin
kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ✅ 标准写法:使用 repeatOnLifecycle
lifecycleScope.launch {
// STARTED 状态表示 Activity 可见
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userData.collect { user ->
// 安全更新 UI,不会在后台执行
updateUserInfo(user)
}
}
}
}
}
❌ 避免写法
kotlin
kotlin
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ❌ 错误1:使用已弃用的 launchWhenX(不会取消 Flow)
lifecycleScope.launchWhenStarted {
viewModel.data.collect { data ->
// 进入后台后仍然执行,可能崩溃!
updateUI(data)
}
}
// ❌ 错误2:直接收集(不关注生命周期)
lifecycleScope.launch {
viewModel.data.collect { data ->
// 内存泄漏风险!
updateUI(data)
}
}
}
}
7.4.2 Fragment 中的安全收集实践 ⚠️(特别注意!)
为什么 Fragment 特殊?
重要概念:
- Fragment 有两个生命周期:Fragment 自身 和 视图
- 视图可能被销毁重建(屏幕旋转、返回栈)
- 必须用
viewLifecycleOwner,不能用lifecycleOwner
✅ 正确写法
kotlin
kotlin
class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ✅ 正确:必须用 viewLifecycleOwner
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userData.collect { user ->
// 安全更新 UI
binding.textName.text = user.name
}
}
}
}
}
❌ 常见错误写法
kotlin
kotlin
class WrongFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// ❌ 错误1:用 lifecycleOwner(应该用 viewLifecycleOwner)
lifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.data.collect {
// 视图销毁后这里还会执行!
binding.textView.text = it // 可能崩溃!
}
}
}
// ❌ 错误2:在 onCreate 中收集
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch { // 视图还没创建!
viewModel.data.collect { /* binding 可能为空! */ }
}
}
}
}
7.4.3 Compose 中的安全收集实践(最简单!)
方法1:推荐写法(一行代码搞定)
kotlin
kotlin
@Composable
fun UserProfileScreen(viewModel: UserViewModel = viewModel()) {
// ✅ 最简单:自动跟随生命周期
// 需要添加依赖:androidx.lifecycle:lifecycle-runtime-compose
val user by viewModel.userData.collectAsStateWithLifecycle()
Column {
if (user != null) {
Text(text = user!!.name)
Text(text = user!!.email)
}
}
}
方法2:处理一次性事件
kotlin
kotlin
@Composable
fun LoginScreen(viewModel: LoginViewModel = viewModel()) {
val state by viewModel.state.collectAsStateWithLifecycle()
// 处理一次性事件(导航、Toast等)
val sideEffects = viewModel.sideEffects
LaunchedEffect(sideEffects) {
sideEffects.collect { effect ->
when (effect) {
is SideEffect.NavigateToHome -> {
// 执行导航
navController.navigate("home")
}
is SideEffect.ShowToast -> {
// 显示 Toast
}
}
}
}
// 渲染 UI...
}
7.4.4 快速对比总结
各场景最佳实践对比表
| 场景 | 核心要点 | 推荐写法 | 避免写法 |
|---|---|---|---|
| Activity | 使用 lifecycleScope 关注整个 Activity 生命周期 |
✅ lifecycleScope.launch { repeatOnLifecycle(STARTED) { flow.collect() } } |
❌ launchWhenX ❌ 直接 launch { flow.collect() } |
| Fragment | 必须 用 viewLifecycleOwner 在 onViewCreated 中收集 |
✅ viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(STARTED) { flow.collect() } } |
❌ 用 lifecycleOwner ❌ 在 onCreate 中收集 |
| Compose | 使用专用扩展函数 自动管理生命周期 | ✅ val data by flow.collectAsStateWithLifecycle() |
❌ 直接 collectAsState() ❌ 手动管理复杂生命周期 |
7.4.5 实用工具类(复制即用)
kotlin
kotlin
// Activity 扩展函数
fun <T> ComponentActivity.collectWithLifecycle(
flow: Flow<T>,
minState: Lifecycle.State = Lifecycle.State.STARTED,
action: (T) -> Unit
) {
lifecycleScope.launch {
repeatOnLifecycle(minState) {
flow.collect(action)
}
}
}
// Fragment 扩展函数
fun <T> Fragment.collectWithLifecycle(
flow: Flow<T>,
minState: Lifecycle.State = Lifecycle.State.STARTED,
action: (T) -> Unit
) {
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(minState) {
flow.collect(action)
}
}
}
// 使用示例
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 一行代码搞定
collectWithLifecycle(viewModel.data) { data ->
binding.textView.text = data
}
}
}
第8章:MVI 架构模式
8.1 什么是 MVI?(一句话说清)
MVI = Model-View-Intent
就是一个让代码更有条理的 App 架构方法,核心思想:UI 完全由数据驱动
8.2 核心三要素(记住这三点就够了)
1. State(状态)
是什么 :当前页面所有的数据
特点:不可变的,唯一的,全部放在一起
kotlin
kotlin
// 一个页面所有的状态都放在这里
data class LoginState(
val email: String = "", // 邮箱输入框
val password: String = "", // 密码输入框
val isLoading: Boolean = false, // 加载状态
val error: String? = null, // 错误信息
val isLoginEnabled: Boolean = false // 登录按钮是否可用
)
2. Intent(意图)
是什么 :用户的操作
特点:每个按钮点击、输入变化都是一个 Intent
kotlin
kotlin
// 用户的所有操作都用这个表示
sealed class LoginIntent {
data class EmailChanged(val email: String) : LoginIntent()
data class PasswordChanged(val password: String) : LoginIntent()
object LoginClicked : LoginIntent()
}
3. View(视图)
是什么 :显示界面,监听用户操作
特点:只做两件事:
- 显示 State
- 发送 Intent
8.3 完整示例:登录页面
ViewModel 实现(处理所有逻辑)
kotlin
kotlin
class LoginViewModel : ViewModel() {
// 1. 定义 State(这是唯一的真相)
private val _state = MutableStateFlow(LoginState())
val state: StateFlow<LoginState> = _state
// 2. 定义一次性事件(Toast、导航等)
private val _events = MutableSharedFlow<LoginEvent>()
val events: SharedFlow<LoginEvent> = _events
// 3. 处理用户操作
fun processIntent(intent: LoginIntent) {
when (intent) {
is LoginIntent.EmailChanged -> {
// 更新邮箱
_state.value = _state.value.copy(
email = intent.email,
isLoginEnabled = intent.email.isNotEmpty() &&
_state.value.password.isNotEmpty()
)
}
is LoginIntent.PasswordChanged -> {
// 更新密码
_state.value = _state.value.copy(
password = intent.password,
isLoginEnabled = _state.value.email.isNotEmpty() &&
intent.password.isNotEmpty()
)
}
LoginIntent.LoginClicked -> {
// 点击登录按钮
login()
}
}
}
private fun login() {
viewModelScope.launch {
// 1. 显示加载
_state.value = _state.value.copy(isLoading = true)
delay(2000) // 模拟网络请求
// 2. 登录成功
_state.value = _state.value.copy(isLoading = false)
// 3. 发送导航事件(一次性)
_events.emit(LoginEvent.NavigateToHome)
// 4. 发送 Toast 事件
_events.emit(LoginEvent.ShowToast("登录成功!"))
}
}
}
// 一次性事件(不保存在 State 中)
sealed class LoginEvent {
object NavigateToHome : LoginEvent()
data class ShowToast(val message: String) : LoginEvent()
}
Activity/Fragment 实现(显示界面)
kotlin
kotlin
class LoginActivity : AppCompatActivity() {
private val viewModel: LoginViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 1. 监听 State 变化,更新 UI
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { state ->
// 更新所有 UI
updateUI(state)
}
}
}
// 2. 监听一次性事件(导航、Toast)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.events.collect { event ->
when (event) {
is LoginEvent.NavigateToHome -> {
// 跳转到首页
startActivity(Intent(this@LoginActivity, HomeActivity::class.java))
}
is LoginEvent.ShowToast -> {
Toast.makeText(this@LoginActivity, event.message, Toast.LENGTH_SHORT).show()
}
}
}
}
}
// 3. 设置点击监听,发送 Intent
binding.buttonLogin.setOnClickListener {
viewModel.processIntent(LoginIntent.LoginClicked)
}
binding.editEmail.addTextChangedListener {
viewModel.processIntent(LoginIntent.EmailChanged(it.toString()))
}
binding.editPassword.addTextChangedListener {
viewModel.processIntent(LoginIntent.PasswordChanged(it.toString()))
}
}
private fun updateUI(state: LoginState) {
// 根据 State 更新所有 UI
binding.editEmail.setText(state.email)
binding.editPassword.setText(state.password)
binding.buttonLogin.isEnabled = state.isLoginEnabled
binding.progressBar.isVisible = state.isLoading
if (state.error != null) {
Toast.makeText(this, state.error, Toast.LENGTH_SHORT).show()
}
}
}
8.4 MVI 的工作流程(超简单图解)
text
ini
用户点击按钮
↓
发送 LoginClicked Intent
↓
ViewModel 处理 Intent
↓
更新 State(isLoading = true)
↓
UI 自动更新(显示加载动画)
↓
网络请求完成
↓
更新 State(isLoading = false)
↓
发送 NavigateToHome Event
↓
UI 收到 Event,跳转页面
8.5 为什么用 MVI?(三大好处)
1. 代码清晰
- 所有状态在一个地方
- 所有操作在一个地方处理
- UI 只负责显示和发送事件
2. 容易调试
- 知道当前所有状态
- 知道每个操作如何改变状态
- 可以"时间旅行"(记录所有状态变化)
3. 避免 bug
- State 不可变,不会意外修改
- 单向数据流,不会循环更新
- 事件一次性,不会重复触发
8.6 简单版 BaseViewModel(复制直接用)
kotlin
kotlin
// 简单的 BaseViewModel,大部分场景够用了
open class SimpleViewModel<State, Event> : ViewModel() {
private val _state = MutableStateFlow(initialState)
val state: StateFlow<State> = _state
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events
protected open val initialState: State
get() = throw NotImplementedError("必须提供初始状态")
protected fun updateState(newState: State) {
_state.value = newState
}
protected fun updateState(updater: (State) -> State) {
_state.update(updater)
}
protected suspend fun sendEvent(event: Event) {
_events.emit(event)
}
protected fun postEvent(event: Event) {
viewModelScope.launch {
_events.emit(event)
}
}
}
// 使用示例
class CounterViewModel : SimpleViewModel<CounterState, CounterEvent>() {
override val initialState = CounterState()
fun increment() {
updateState { it.copy(count = it.count + 1) }
if (state.value.count == 10) {
postEvent(CounterEvent.ShowToast("达到10了!"))
}
}
}
data class CounterState(val count: Int = 0)
sealed class CounterEvent {
data class ShowToast(val message: String) : CounterEvent()
}
8.7 一句话总结 MVI
把所有状态放在一起,把所有操作放在一起处理,让 UI 只做显示和发送事件。
记住三句话:
- State 决定 UI 长什么样
- Intent 是用户做了什么
- ViewModel 处理 Intent,更新 State
最简单的 MVI 代码结构:
text
vbnet
页面
├── State(数据类,所有状态)
├── Intent(密封类,所有操作)
├── Event(密封类,一次性事件)
├── ViewModel(处理 Intent,更新 State)
└── UI(显示 State,发送 Intent,监听 Event)
第9章:数据层设计:Repository 模式革新
9.1 使用 Flow 封装网络请求
传统方式 vs Flow 方式
kotlin
kotlin
// ❌ 传统方式:回调地狱
class OldUserRepository {
fun getUser(userId: String, callback: (Result<User>) -> Unit) {
// 复杂的嵌套回调...
}
}
// ✅ Flow 方式:声明式、可组合
class UserRepository(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) {
fun getUser(userId: String): Flow<User> = flow {
// 1. 先发射本地数据(如果有)
localDataSource.getUser(userId).collect { cachedUser ->
if (cachedUser != null) {
emit(cachedUser)
}
}
// 2. 获取远程数据
val remoteUser = remoteDataSource.getUser(userId)
// 3. 保存到本地
localDataSource.saveUser(remoteUser)
// 4. 发射远程数据
emit(remoteUser)
}.catch { error ->
// 错误处理
if (error is IOException) {
emit(localDataSource.getLastKnownUser(userId))
} else {
throw error
}
}
}
9.2 实现 "单一数据源" 架构
kotlin
kotlin
class NewsRepository(
private val newsDao: NewsDao, // 本地数据库 - 唯一可信源
private val newsApi: NewsApi, // 网络数据源
) {
// ✅ 单一可信源:只从数据库暴露数据
fun getNews(): Flow<List<Article>> = newsDao.getAllArticles()
// 刷新数据(网络 -> 数据库)
suspend fun refreshNews(): Result<Unit> = withContext(Dispatchers.IO) {
return@withContext try {
val articles = newsApi.getLatestNews()
newsDao.clearAndInsertAll(articles)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
}
}
9.3 Flow 与 Retrofit、Room 的集成
Retrofit + Flow
kotlin
kotlin
interface NewsApi {
// ✅ 支持 suspend 函数
@GET("news/latest")
suspend fun getLatestNews(): List<ArticleDto>
}
// Repository 中使用
class NewsRepository @Inject constructor(
private val newsApi: NewsApi
) {
fun getLatestNews(): Flow<List<Article>> = flow {
val newsDto = newsApi.getLatestNews()
val news = newsDto.map { it.toDomain() }
emit(news)
}
}
Room + Flow
kotlin
kotlin
@Dao
interface ArticleDao {
// ✅ Room 原生支持 Flow
@Query("SELECT * FROM articles ORDER BY publish_date DESC")
fun getAllArticles(): Flow<List<ArticleEntity>>
}
9.4 缓存策略
kotlin
kotlin
class SmartCacheRepository(
private val memoryCache: MemoryCache,
private val diskCache: DiskCache,
private val remoteDataSource: RemoteDataSource
) {
fun getData(id: String): Flow<Data> = flow {
// 1. 检查内存缓存(最快)
memoryCache.get(id)?.let { cachedData ->
emit(cachedData)
return@flow
}
// 2. 检查磁盘缓存
diskCache.get(id)?.let { cachedData ->
// 存入内存缓存
memoryCache.put(id, cachedData)
emit(cachedData)
}
// 3. 从网络获取
try {
val remoteData = remoteDataSource.getData(id)
// 4. 更新两级缓存
memoryCache.put(id, remoteData)
diskCache.put(id, remoteData)
emit(remoteData)
} catch (e: Exception) {
// 网络失败,返回空数据
emit(Data.EMPTY)
}
}
}
第10章:Flow 与 Paging 3:无缝处理分页加载
10.1 基本使用
kotlin
kotlin
// 1. 定义 PagingSource
class ExamplePagingSource : PagingSource<Int, Item>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Item> {
return try {
val page = params.key ?: 1
val response = api.getItems(page, params.loadSize)
LoadResult.Page(
data = response.items,
prevKey = if (page == 1) null else page - 1,
nextKey = if (response.hasMore) page + 1 else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
// 2. 创建 Pager
val pager = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = { ExamplePagingSource() }
)
// 3. 获取 Flow<PagingData>
val pagingDataFlow: Flow<PagingData<Item>> = pager.flow
10.2 cachedIn(viewModelScope) 的魔力
kotlin
scss
class MyViewModel : ViewModel() {
val items: Flow<PagingData<Item>> = Pager(
config = PagingConfig(pageSize = 20),
pagingSourceFactory = { ItemPagingSource(api) }
)
.flow
.cachedIn(viewModelScope) // 缓存到 ViewModel 的作用域
}
为什么需要 cachedIn?
- 避免在配置更改时重新加载数据
- 在多个收集者之间共享同一分页数据流
- 确保数据在 ViewModel 生命周期内保持一致
10.3 在 UI 中使用
RecyclerView 中使用
kotlin
kotlin
class MainActivity : AppCompatActivity() {
private val viewModel: MyViewModel by viewModels()
private lateinit var adapter: ItemAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
adapter = ItemAdapter()
recyclerView.adapter = adapter
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.items.collectLatest { pagingData ->
adapter.submitData(pagingData)
}
}
}
}
}
Compose 中使用
kotlin
kotlin
@Composable
fun ItemListScreen(viewModel: MyViewModel = viewModel()) {
val items = viewModel.items.collectAsLazyPagingItems()
LazyColumn {
items(items) { item ->
if (item != null) {
ItemRow(item = item)
}
}
}
}
第11章:Flow 与 Jetpack DataStore:告别 SharedPreferences
11.1 基本使用
kotlin
kotlin
// 定义数据存储
val Context.settingsDataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings"
)
// 定义键
object PreferencesKeys {
val THEME = stringPreferencesKey("theme")
val NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled")
}
// 写入数据
suspend fun saveTheme(theme: String) {
context.settingsDataStore.edit { preferences ->
preferences[PreferencesKeys.THEME] = theme
}
}
// 读取数据(返回 Flow)
val themeFlow: Flow<String> = context.settingsDataStore.data
.map { preferences ->
preferences[PreferencesKeys.THEME] ?: "light"
}
11.2 在 ViewModel 中使用
kotlin
kotlin
class SettingsViewModel(
private val dataStore: DataStore<Preferences>
) : ViewModel() {
val theme: Flow<String> = dataStore.data
.map { preferences ->
preferences[PreferencesKeys.THEME] ?: "light"
}
suspend fun updateTheme(theme: String) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.THEME] = theme
}
}
}
第12章:Flow 与 WorkManager:监听后台任务状态(简化版)
12.1 基本使用
kotlin
kotlin
class DownloadViewModel(
private val workManager: WorkManager
) : ViewModel() {
// 监控特定 WorkRequest 的状态
fun monitorDownload(downloadId: UUID): Flow<WorkInfo> {
return workManager.getWorkInfoByIdFlow(downloadId)
}
}
12.2 在 UI 中监控
kotlin
kotlin
@Composable
fun DownloadScreen(viewModel: DownloadViewModel) {
val downloadState by viewModel.downloadState.collectAsState()
// 根据状态显示不同的 UI
when (downloadState) {
is WorkInfo.State.RUNNING -> {
Text("下载中...")
}
is WorkInfo.State.SUCCEEDED -> {
Text("下载完成")
}
is WorkInfo.State.FAILED -> {
Text("下载失败")
}
else -> {}
}
}