Kotlin Flow 深度探索与实践指南——中部:实战与应用篇

第三部分: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 必须viewLifecycleOwneronViewCreated 中收集 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(视图)

是什么 :显示界面,监听用户操作
特点:只做两件事:

  1. 显示 State
  2. 发送 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 只做显示和发送事件。

记住三句话:

  1. State 决定 UI 长什么样
  2. Intent 是用户做了什么
  3. 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 -> {}
    }
}
相关推荐
cindershade2 小时前
事件委托(Event Delegation)的原理
前端
开发者小天2 小时前
React中useMemo的使用
前端·javascript·react.js
1024肥宅2 小时前
JS复杂去重一定要先排序吗?深度解析与性能对比
前端·javascript·面试
im_AMBER3 小时前
weather-app开发手记 04 AntDesign组件库使用解析 | 项目设计困惑
开发语言·前端·javascript·笔记·学习·react.js
用泥种荷花3 小时前
VueCropper加载OBS图片跨域问题
前端
董世昌413 小时前
什么是事件冒泡?如何阻止事件冒泡和浏览器默认事件?
java·前端
Bigger3 小时前
在 React 里优雅地 “隐藏 iframe 滚动条”
前端·css·react.js
小沐°3 小时前
vue3-ElementPlus出现Uncaught (in promise) cancel 报错
前端·javascript·vue.js
四瓣纸鹤3 小时前
F2图表在Vue3中的使用方法
前端·javascript·vue.js·antv/f2