当前位置: 首页 > news >正文

Kotlin Flow流

一 Kotlin Flow 中的 stateIn 和 shareIn

一、简单比喻理解

想象一个水龙头(数据源)和几个水杯(数据接收者):

  • 普通 Flow(冷流):每个水杯来接水时,都要重新打开水龙头从头放水
  • stateIn/shareIn(热流):水龙头一直开着,水存在一个水池里,任何水杯随时来接都能拿到水

二、stateIn 是什么?

就像手机的状态栏

  • 总是显示最新的一条信息(有当前值
  • 新用户打开手机时,立刻能看到最后一条消息
  • 适合用来表示"当前状态",比如:
    • 用户登录状态(已登录/未登录)
    • 页面加载状态(加载中/成功/失败)
    • 实时更新的数据(如股票价格)

代码示例:

// 创建一个永远知道当前温度的温度计
val currentTemperature = sensorFlow.stateIn(scope = viewModelScope,  // 在ViewModel生命周期内有效started = SharingStarted.WhileSubscribed(5000), // 5秒无订阅就暂停initialValue = 0 // 初始温度0度)// 在Activity中读取(总是能拿到当前温度)
textView.text = "${currentTemperature.value}°C"

三、shareIn 是什么?

就像广播电台

  • 不保存"当前值"(没有.value属性)
  • 新听众打开收音机时,可以选择:
    • 从最新的一条新闻开始听(replay=1)
    • 只听新新闻(replay=0)
  • 适合用来处理"事件",比如:
    • 显示Toast提示
    • 页面跳转指令
    • 一次性通知

代码示例:

// 创建一个消息广播站
val messages = notificationFlow.shareIn(scope = viewModelScope,started = SharingStarted.Lazily, // 有人收听时才启动replay = 1 // 新听众能听到最后1条消息)// 在Activity中收听广播
lifecycleScope.launch {messages.collect { msg ->Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()}
}

四、主要区别对比

特性stateIn (状态栏)shareIn (广播电台)
有无当前值有(.value 直接访问)无(必须通过collect接收)
新订阅者立即获得最新值可配置获得最近N条(replay)
典型用途持续更新的状态(如用户积分)一次性事件(如"购买成功"提示)
内存占用始终保存最新值按需缓存(可配置)
是否热流

五、为什么要用它们?

  1. 节省资源:避免重复计算(多个界面可以共享同一个数据源)

    • ❌ 不用时:每个界面都单独请求一次网络数据
    • ✅ 使用后:所有界面共享同一份网络数据
  2. 保持一致性:所有订阅者看到的数据完全相同

    • 比如用户头像更新后,所有界面立即同步
  3. 自动管理生命周期

    • 当Activity销毁时自动停止收集
    • 当配置变更(如屏幕旋转)时保持数据不丢失

六、生活场景类比

场景1:微信群(stateIn)

  • 群里最后一条消息就是当前状态(.value)
  • 新成员进群立刻能看到最后一条消息
  • 适合:工作群的状态同步

场景2:电台广播(shareIn)

  • 主播不断发送新消息
  • 听众打开收音机时:
    • 可以设置是否听之前的回放(replay)
    • 但无法直接问"刚才最后一首歌是什么"(无.value)
  • 适合:交通路况实时播报

七、什么时候用哪个?

用 stateIn 当:

  • 需要随时知道"当前值"
  • 数据会持续变化且需要被多个地方使用
  • 例如:
    • 用户登录状态
    • 购物车商品数量
    • 实时位置更新

用 shareIn 当:

  • 只关心新事件,不关心历史值
  • 事件可能被多个接收者处理
  • 例如:
    • "订单支付成功"通知
    • 错误提示消息
    • 页面跳转指令

八、超简单选择流程图

要管理持续变化的状态吗?是 → 需要直接访问当前值吗?是 → 用 stateIn否 → 用 shareIn(replay=1)否 → 这是一次性事件吗?是 → 用 shareIn(replay=0)

记住这个简单的口诀:
“状态用state,事件用share,想要回放加replay”

二 Kotlin Flow 的 shareInstateIn 操作符完全指南

在 Kotlin Flow 的使用中,shareInstateIn 是两个关键的操作符,用于优化流的共享和状态管理。本教程将深入解析这两个操作符的使用场景、区别和最佳实践。

一、核心概念解析

1. 冷流 vs 热流

  • 冷流 (Cold Flow):每个收集者都会触发独立的执行(如普通的 flow{} 构建器)
  • 热流 (Hot Flow):数据发射独立于收集者存在(如 StateFlowSharedFlow

2. 为什么需要 shareIn/stateIn?

  • 避免对上游冷流进行重复计算
  • 多个收集者共享同一个数据源
  • 将冷流转换为热流以提高效率

二、stateIn 操作符详解

基本用法

val sharedFlow: StateFlow<Int> = flow {// 模拟耗时操作emit(repository.fetchData())
}.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),initialValue = 0
)

参数说明:

  • scope:共享流的协程作用域(通常用 viewModelScope
  • started:共享启动策略(后文详细讲解)
  • initialValue:必须提供的初始值

特点:

  1. 总是有当前值(通过 value 属性访问)
  2. 新收集者立即获得最新值
  3. 适合表示 UI 状态

使用场景示例:

用户个人信息状态管理

class UserViewModel : ViewModel() {private val _userState = repository.userUpdates() // Flow<User>.map { it.toUiState() }.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = UserState.Loading)val userState: StateFlow<UserState> = _userState
}

三、shareIn 操作符详解

基本用法

val sharedFlow: SharedFlow<Int> = flow {emit(repository.fetchData())
}.shareIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),replay = 1
)

参数说明:

  • replay:新收集者接收的旧值数量
  • extraBufferCapacity:超出 replay 的缓冲大小
  • onBufferOverflow:缓冲策略(SUSPEND, DROP_OLDEST, DROP_LATEST

特点:

  1. 可以有多个订阅者
  2. 没有 value 属性,必须通过收集获取数据
  3. 适合事件处理(如 Toast、导航事件)

使用场景示例:

全局事件通知

class EventBus {private val _events = MutableSharedFlow<Event>()val events = _events.asSharedFlow()suspend fun postEvent(event: Event) {_events.emit(event)}// 使用 shareIn 转换外部流val externalEvents = someExternalFlow.shareIn(scope = CoroutineScope(Dispatchers.IO),started = SharingStarted.Eagerly,replay = 0)
}

四、started 参数深度解析

1. SharingStarted.Eagerly

  • 行为:立即启动,无视是否有收集者
  • 用例:需要预先缓存的数据
  • 风险:可能造成资源浪费
started = SharingStarted.Eagerly

2. SharingStarted.Lazily

  • 行为:在第一个收集者出现时启动,保持活跃直到 scope 结束
  • 用例:长期存在的共享数据
  • 注意:可能延迟首次数据获取
started = SharingStarted.Lazily

3. SharingStarted.WhileSubscribed()

  • 行为
    • 有收集者时活跃
    • 最后一个收集者消失后保持一段时间(默认 0ms)
    • 可配置 stopTimeoutMillisreplayExpirationMillis
  • 用例:大多数 UI 相关状态
// 保留5秒供可能的重新订阅
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000)

五、关键区别对比

特性stateInshareIn
返回类型StateFlowSharedFlow
初始值必须提供无要求
新收集者获取立即获得最新 value获取 replay 数量的旧值
值访问通过 .value 直接访问必须通过收集获取
典型用途UI 状态管理事件通知/数据广播
背压处理总是缓存最新值可配置缓冲策略

六、最佳实践指南

1. ViewModel 中的标准模式

class MyViewModel : ViewModel() {// 状态管理用 stateInval uiState = repository.data.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = null)// 事件处理用 shareInval events = repository.events.shareIn(scope = viewModelScope,started = SharingStarted.Lazily,replay = 1)
}

2. 合理选择 started 策略

  • UI 状态WhileSubscribed(stopTimeoutMillis = 5000)
  • 配置变更需保留Lazily
  • 全局常驻数据Eagerly

3. 避免常见错误

错误1:在每次调用时创建新流

// 错误!每次调用都创建新流
fun getUser() = repository.getUserFlow().stateIn(viewModelScope, SharingStarted.Eagerly, null)// 正确:共享同一个流
private val _user = repository.getUserFlow().stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
val user: StateFlow<User?> = _user

错误2:忽略 replay 配置

// 可能丢失事件
.shareIn(scope, SharingStarted.Lazily, replay = 0)// 更安全的配置
.shareIn(scope, SharingStarted.Lazily, replay = 1)

七、高级应用场景

1. 结合 Room 数据库

@Dao
interface UserDao {@Query("SELECT * FROM user")fun observeUsers(): Flow<List<User>>
}// ViewModel 中
val users = userDao.observeUsers().stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),initialValue = emptyList())

2. 实现自动刷新功能

val autoRefreshData = flow {while(true) {emit(repository.fetchLatest())delay(30_000) // 每30秒刷新}
}.shareIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),replay = 1
)

3. 多源数据合并

val combinedData = combine(repo1.data.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1),repo2.data.shareIn(viewModelScope, SharingStarted.WhileSubscribed(), 1)
) { data1, data2 ->data1 + data2
}.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(),initialValue = emptyList()
)

八、性能优化技巧

  1. 合理设置 replay

    • UI 状态:replay = 1(确保新订阅者立即获得状态)
    • 事件通知:replay = 0(避免重复处理旧事件)
  2. 使用 WhileSubscribed 的过期策略

    started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000,replayExpirationMillis = 60_000 // 1分钟后丢弃缓存
    )
    
  3. 避免过度缓冲

    .shareIn(scope = ...,replay = 1,extraBufferCapacity = 1, // 总共缓冲2个值onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    

九、测试策略

1. 测试 StateFlow

@Test
fun testStateFlow() = runTest {val testScope = TestScope()val flow = flowOf(1, 2, 3)val stateFlow = flow.stateIn(scope = testScope,started = SharingStarted.Eagerly,initialValue = 0)assertEquals(0, stateFlow.value) // 初始值testScope.advanceUntilIdle()assertEquals(3, stateFlow.value) // 最后发射的值
}

2. 测试 SharedFlow

@Test
fun testSharedFlow() = runTest {val testScope = TestScope()val flow = flowOf("A", "B", "C")val sharedFlow = flow.shareIn(scope = testScope,started = SharingStarted.Eagerly,replay = 1)val results = mutableListOf<String>()val job = launch {sharedFlow.collect { results.add(it) }}testScope.advanceUntilIdle()assertEquals(listOf("A", "B", "C"), results)job.cancel()
}

十、总结决策树

何时使用 stateIn

  • 需要表示当前状态(有 .value 属性)
  • UI 需要立即访问最新值
  • 适合:页面状态、表单数据、加载状态

何时使用 shareIn

  • 处理一次性事件
  • 需要自定义缓冲策略
  • 适合:Toast 消息、导航事件、广播通知

选择哪种 started 策略?

  • WhileSubscribed():大多数 UI 场景
  • Lazily:配置变更需保留数据
  • Eagerly:需要预加载的全局数据

通过本教程,应该已经掌握了 shareInstateIn 的核心用法和高级技巧。正确使用这两个操作符可以显著提升应用的性能和资源利用率。

三 从 LiveData 迁移到 Kotlin Flow 完整教程

LiveData 长期以来是 Android 架构组件中状态管理的核心,但随着 Kotlin Flow 的成熟,Google 官方推荐将现有 LiveData 迁移到 Flow。本教程基于官方文章并扩展实践细节,完成平滑迁移。

一、为什么要从 LiveData 迁移到 Flow?

LiveData 的局限性

  1. 有限的运算符:只有简单的 map/switchMap 转换
  2. 线程限制:只能在主线程观察
  3. 一次性操作:不适合处理事件流
  4. 生命周期耦合:虽然方便但也限制了灵活性

Flow 的优势

  1. 丰富的操作符:filter, transform, combine 等 100+ 操作符
  2. 灵活的线程控制:通过 Dispatchers 指定执行线程
  3. 响应式编程:完整的事件流处理能力
  4. 协程集成:与 Kotlin 协程完美配合

二、基础迁移方案

1. 简单替换:LiveData → StateFlow

原 LiveData 代码

class MyViewModel : ViewModel() {private val _userName = MutableLiveData("")val userName: LiveData<String> = _userNamefun updateName(name: String) {_userName.value = name}
}

迁移为 StateFlow

class MyViewModel : ViewModel() {private val _userName = MutableStateFlow("")val userName: StateFlow<String> = _userName.asStateFlow()fun updateName(name: String) {_userName.value = name}
}

2. 在 UI 层收集 Flow

Activity/Fragment 中收集

lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.userName.collect { name ->binding.textView.text = name}}
}

关键点:使用 repeatOnLifecycle 确保只在 UI 可见时收集,避免资源浪费

三、高级迁移模式

1. 数据流转换(替代 Transformations)

LiveData 方式

val userName: LiveData<String> = Transformations.map(_userName) { "Hello, $it!" 
}

Flow 方式

val userName: Flow<String> = _userName.map { "Hello, $it!" 
}

2. 多数据源合并(替代 MediatorLiveData)

LiveData 方式

val result = MediatorLiveData<String>().apply {addSource(liveData1) { value = "$it + ${liveData2.value}" }addSource(liveData2) { value = "${liveData1.value} + $it" }
}

Flow 方式

val result = combine(flow1, flow2) { data1, data2 ->"$data1 + $data2"
}

四、处理一次性事件(替代 SingleLiveEvent)

LiveData 常被滥用处理一次性事件(如 Toast、导航),Flow 提供了更专业的解决方案:

使用 SharedFlow

class EventViewModel : ViewModel() {private val _events = MutableSharedFlow<Event>()val events = _events.asSharedFlow()sealed class Event {data class ShowToast(val message: String) : Event()object NavigateToNext : Event()}fun triggerToast() {viewModelScope.launch {_events.emit(Event.ShowToast("Hello Flow!"))}}
}

UI 层收集

lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.events.collect { event ->when (event) {is EventViewModel.Event.ShowToast -> Toast.makeText(context, event.message, Toast.LENGTH_SHORT).show()EventViewModel.Event.NavigateToNext -> startActivity(Intent(this, NextActivity::class.java))}}}
}

五、Room 数据库迁移

LiveData 查询:

@Dao
interface UserDao {@Query("SELECT * FROM user")fun getUsers(): LiveData<List<User>>
}

迁移到 Flow:

@Dao
interface UserDao {@Query("SELECT * FROM user")fun getUsers(): Flow<List<User>>
}

ViewModel 中使用

val users: StateFlow<List<User>> = userDao.getUsers().stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = emptyList())

六、兼容现有代码的渐进式迁移

1. 使用 asLiveData() 临时兼容

val userFlow: Flow<User> = repository.getUserFlow()// 临时保持 LiveData 接口
val userLiveData: LiveData<User> = userFlow.asLiveData()

2. 混合使用策略

class HybridViewModel : ViewModel() {// 新功能使用 Flowprivate val _newFeatureState = MutableStateFlow("")val newFeatureState: StateFlow<String> = _newFeatureState.asStateFlow()// 旧功能暂保持 LiveDataprivate val _oldFeatureData = MutableLiveData(0)val oldFeatureData: LiveData<Int> = _oldFeatureData
}

七、测试策略调整

LiveData 测试:

@Test
fun testLiveData() {val liveData = MutableLiveData("test")assertEquals("test", liveData.value)
}

Flow 测试:

@Test
fun testFlow() = runTest {val flow = MutableStateFlow("test")val results = mutableListOf<String>()val job = launch {flow.collect { results.add(it) }}flow.value = "new value"assertEquals(listOf("test", "new value"), results)job.cancel()
}

八、性能优化技巧

  1. 使用 stateIn 共享流

    val sharedFlow = someFlow.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000),initialValue = null)
    
  2. 避免重复创建 Flow

    // 错误方式 - 每次调用都创建新流
    fun getUser() = userDao.getUserFlow()// 正确方式 - 共享同一个流
    private val _userFlow = userDao.getUserFlow()
    val userFlow = _userFlow
    
  3. 合理选择背压策略

    // 缓冲最新值
    val events = MutableSharedFlow<Event>(replay = 0,extraBufferCapacity = 1,onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    

九、常见问题解决方案

Q1: 如何确保 Flow 收集不会泄漏?

lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {// 只有在此块内会激活收集viewModel.data.collect { ... }}
}

Q2: 为什么我的 Flow 不发射数据?

检查:

  1. Flow 是否被正确触发(冷流需要收集才会开始)
  2. 是否在正确的协程作用域内收集
  3. 是否有异常导致流终止

Q3: 如何处理 Java 代码调用?

// 暴露 LiveData 给 Java 模块
val javaCompatData: LiveData<String> = flow.asLiveData()

十、完整迁移示例

迁移前 ViewModel

class OldViewModel : ViewModel() {private val _data = MutableLiveData("")val data: LiveData<String> = _dataprivate val _event = SingleLiveEvent<Event>()val event: LiveData<Event> = _eventfun loadData() {viewModelScope.launch {_data.value = repository.fetchData()_event.value = Event.ShowToast("Loaded")}}
}

迁移后 ViewModel

class NewViewModel : ViewModel() {private val _data = MutableStateFlow("")val data: StateFlow<String> = _data.asStateFlow()private val _event = MutableSharedFlow<Event>()val event: SharedFlow<Event> = _event.asSharedFlow()fun loadData() {viewModelScope.launch {_data.value = repository.fetchData()_event.emit(Event.ShowToast("Loaded"))}}
}

通过本教程,可以系统性地将现有 LiveData 代码迁移到 Kotlin Flow。建议采用渐进式迁移策略,优先在新功能中使用 Flow,逐步改造旧代码。

四 Android StateFlow 完整教程

Android StateFlow 完整教程:从入门到实战

StateFlow 是 Kotlin 协程库中用于状态管理的响应式流,特别适合在 Android 应用开发中管理 UI 状态。本教程将带你全面了解 StateFlow 的使用方法。

1. StateFlow 基础概念

1.1 什么是 StateFlow?

StateFlow 是 Kotlin 协程提供的一种热流(Hot Flow),它具有以下特点:

  • 总是有当前值(初始值必须提供)
  • 只保留最新值
  • 支持多个观察者
  • 与 LiveData 类似但基于协程

1.2 StateFlow vs LiveData

特性StateFlowLiveData
生命周期感知否(需配合 lifecycleScope)
需要初始值
基于协程观察者模式
线程控制通过 Dispatcher主线程
背压处理自动处理自动处理

2. 基本使用

2.1 添加依赖

在 build.gradle 中添加:

dependencies {implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4"implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4"implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.5.1"
}

2.2 创建 StateFlow

class MyViewModel : ViewModel() {// 私有可变的StateFlowprivate val _uiState = MutableStateFlow<UiState>(UiState.Loading)// 公开不可变的StateFlowval uiState: StateFlow<UiState> = _uiState.asStateFlow()sealed class UiState {object Loading : UiState()data class Success(val data: String) : UiState()data class Error(val message: String) : UiState()}fun loadData() {viewModelScope.launch {_uiState.value = UiState.Loadingtry {val result = repository.fetchData()_uiState.value = UiState.Success(result)} catch (e: Exception) {_uiState.value = UiState.Error(e.message ?: "Unknown error")}}}
}

2.3 在 Activity/Fragment 中收集 StateFlow

class MyActivity : AppCompatActivity() {private val viewModel: MyViewModel by viewModels()override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.uiState.collect { state ->when (state) {is MyViewModel.UiState.Loading -> showLoading()is MyViewModel.UiState.Success -> showData(state.data)is MyViewModel.UiState.Error -> showError(state.message)}}}}}private fun showLoading() { /*...*/ }private fun showData(data: String) { /*...*/ }private fun showError(message: String) { /*...*/ }
}

3. 高级用法

3.1 结合 SharedFlow 处理一次性事件

class EventViewModel : ViewModel() {private val _events = MutableSharedFlow<Event>()val events = _events.asSharedFlow()sealed class Event {data class ShowToast(val message: String) : Event()object NavigateToNextScreen : Event()}fun triggerEvent() {viewModelScope.launch {_events.emit(Event.ShowToast("Hello World!"))}}
}// 在Activity中收集
lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {viewModel.events.collect { event ->when (event) {is EventViewModel.Event.ShowToast -> showToast(event.message)EventViewModel.Event.NavigateToNextScreen -> navigateToNext()}}}
}

3.2 状态合并 (combine)

val userName = MutableStateFlow("")
val userAge = MutableStateFlow(0)val userInfo = combine(userName, userAge) { name, age ->"Name: $name, Age: $age"
}// 收集合并后的流
userInfo.collect { info ->println(info)
}

3.3 状态转换 (map, filter, etc.)

val numbers = MutableStateFlow(0)val evenNumbers = numbers.filter { it % 2 == 0 }.map { "Even: $it" }evenNumbers.collect { println(it) }

4. 性能优化

4.1 使用 stateIn 缓存 StateFlow

val networkFlow = flow {// 模拟网络请求emit(repository.fetchData())
}val cachedState = networkFlow.stateIn(scope = viewModelScope,started = SharingStarted.WhileSubscribed(5000), // 5秒无订阅者停止initialValue = "Loading..."
)

4.2 避免重复收集

// 错误方式 - 每次重组都会创建新的收集器
@Composable
fun MyComposable(viewModel: MyViewModel) {val state by viewModel.state.collectAsState()// ...
}// 正确方式 - 使用 derivedStateOf 或 remember
@Composable
fun MyComposable(viewModel: MyViewModel) {val state by remember { viewModel.state }.collectAsState()// ...
}

5. 测试 StateFlow

5.1 单元测试

@Test
fun `test state flow`() = runTest {val viewModel = MyViewModel()val results = mutableListOf<MyViewModel.UiState>()val job = launch {viewModel.uiState.collect { results.add(it) }}viewModel.loadData()advanceUntilIdle()assertEquals(3, results.size) // Loading, Success/ErrorassertTrue(results[0] is MyViewModel.UiState.Loading)job.cancel()
}

5.2 使用 Turbine 测试库

dependencies {testImplementation "app.cash.turbine:turbine:0.12.1"
}@Test
fun `test with turbine`() = runTest {val viewModel = MyViewModel()viewModel.uiState.test {viewModel.loadData()assertEquals(MyViewModel.UiState.Loading, awaitItem())val success = awaitItem()assertTrue(success is MyViewModel.UiState.Success)cancelAndIgnoreRemainingEvents()}
}

6. 常见问题解答

Q1: StateFlow 和 LiveData 哪个更好?

StateFlow 更适合协程环境,LiveData 更简单但功能较少。新项目推荐 StateFlow。

Q2: 如何处理背压(Backpressure)?

StateFlow 自动处理背压,只保留最新值。

Q3: 为什么我的收集器没有收到更新?

检查:

  1. 是否在正确的生命周期范围内收集
  2. Flow 是否有发射新值
  3. 是否在正确的协程上下文中

Q4: 如何避免内存泄漏?

使用 repeatOnLifecycleflowWithLifecycle 确保只在活跃生命周期收集。

7. 完整示例项目

以下是一个完整的 ViewModel 示例:

class UserViewModel(private val userRepository: UserRepository) : ViewModel() {private val _userState = MutableStateFlow<UserState>(UserState.Loading)val userState: StateFlow<UserState> = _userState.asStateFlow()private val _events = MutableSharedFlow<UserEvent>()val events: SharedFlow<UserEvent> = _events.asSharedFlow()init {loadUser()}fun loadUser() {viewModelScope.launch {_userState.value = UserState.Loadingtry {val user = userRepository.getUser()_userState.value = UserState.Success(user)} catch (e: Exception) {_userState.value = UserState.Error(e.message ?: "Unknown error")_events.emit(UserEvent.ShowErrorToast("Failed to load user"))}}}fun updateUserName(name: String) {viewModelScope.launch {val currentUser = (_userState.value as? UserState.Success)?.user ?: return@launchval updatedUser = currentUser.copy(name = name)_userState.value = UserState.Success(updatedUser)userRepository.updateUser(updatedUser)}}sealed class UserState {object Loading : UserState()data class Success(val user: User) : UserState()data class Error(val message: String) : UserState()}sealed class UserEvent {data class ShowErrorToast(val message: String) : UserEvent()}
}

通过本教程,你应该已经掌握了 StateFlow 的核心用法。StateFlow 是构建响应式 Android 应用的强大工具,结合协程可以提供更简洁、更安全的状态管理方案。

相关文章:

Kotlin Flow流

一 Kotlin Flow 中的 stateIn 和 shareIn 一、简单比喻理解 想象一个水龙头&#xff08;数据源&#xff09;和几个水杯&#xff08;数据接收者&#xff09;&#xff1a; 普通 Flow&#xff08;冷流&#xff09;&#xff1a;每个水杯来接水时&#xff0c;都要重新打开水龙头从…...

虚拟局域网(VLAN)实验(Cisco Packet Tracer)-路由器、交换机的基本配置

好的&#xff0c;我们来根据你提供的文档&#xff0c;一步步地在 Cisco Packet Tracer 中完成这个跨交换机划分 VLAN 的实验。 实验目标: 配置两台交换机 SW1 和 SW2&#xff0c;划分 VLAN 10 和 VLAN 20&#xff0c;配置 Trunk 链路&#xff0c;并测试同 VLAN 和跨 VLAN 的连…...

【论文速递】2025年09周 (Robotics/Embodied AI/LLM)

目录 LLM-Microscope&#xff1a;揭示标点符号在Transformers的上下文中的隐藏作用英文摘要中文摘要 SurveyX&#xff1a;通过大型语言模型实现学术调查自动化英文摘要中文摘要 数学推理的自我奖励校正英文摘要中文摘要 VideoGrain&#xff1a;调整时空关注以进行多元透明视频编…...

自主机器人模拟系统

一、系统概述 本代码实现了一个基于Pygame的2D自主机器人模拟系统&#xff0c;具备以下核心功能&#xff1a; 双模式控制&#xff1a;支持手动控制&#xff08;WASD键&#xff09;和自动导航模式&#xff08;鼠标左键设定目标&#xff09; 智能路径规划&#xff1a;采用改进型…...

DeepSeek构建非农预测模型:量化关税滞后效应与非线性经济冲击传导

AI分析&#xff1a;非农数据前瞻与关税影响的滞后性 根据AI模型对多维度经济指标的交叉验证&#xff0c;4月非农就业报告或呈现“增速放缓但未失速”的特征。当前市场共识预期为新增就业13.3万人&#xff08;前值22.8万&#xff09;&#xff0c;失业率维持4.2%&#xff0c;时薪…...

前端面经-VUE3篇--vue3基础知识(一)插值表达式、ref、reactive

一、计算属性(computed) 计算属性&#xff08;Computed Properties&#xff09;是 Vue 中一种特殊的响应式数据&#xff0c;它能基于已有的响应式数据动态计算出新的数据。 计算属性有以下特性&#xff1a; 自动缓存&#xff1a;只有当它依赖的响应式数据发生变化时&#xff…...

云原生后端架构的优势与最佳实践

📝个人主页🌹:慌ZHANG-CSDN博客 🌹🌹期待您的关注 🌹🌹 在过去的几年里,随着云计算和容器化技术的迅猛发展,云原生架构逐渐成为现代企业和开发团队构建和运维应用系统的首选方式。云原生架构通过高度的自动化、弹性伸缩、微服务化等特点,使得企业能够在不断变化…...

力扣838.推多米诺随笔

“生活就像海洋&#xff0c;只有意志坚强的人&#xff0c;才能到达彼岸。”—— 马克思 题目 n 张多米诺骨牌排成一行&#xff0c;将每张多米诺骨牌垂直竖立。在开始时&#xff0c;同时把一些多米诺骨牌向左或向右推。 每过一秒&#xff0c;倒向左边的多米诺骨牌会推动其左侧…...

aab转apk

一、 android34升级&#xff1a; 1、升级到安卓34&#xff08;蓝牙、图片&#xff09; 再蓝牙广播的地方加入Context.RECEIVER_EXPORTED 2、废弃了 BluetoothAdapter#enable() 和 BluetoothAdapter#disable()&#xff0c;需要修改 // 以前的蓝牙操作BluetoothManager bluetoo…...

LeetCode 560. 和为 K 的子数组 | 前缀和与哈希表的巧妙应用

文章目录 方法思路&#xff1a;前缀和 哈希表核心思想关键步骤 代码实现复杂度分析示例解析总结 题目描述 给定一个整数数组 nums 和一个整数 k&#xff0c;请统计并返回该数组中和为 k 的子数组的数量。 子数组是数组中连续的非空元素序列。 示例 输入&#xff1a;nums …...

【Hive入门】Hive性能调优:小文件问题与动态分区合并策略详解

目录 引言 1 Hive小文件问题概述 1.1 什么是小文件问题 1.2 小文件产生的原因 2 Hive小文件合并机制 2.1 hive.merge.smallfiles参数详解 2.2 小文件合并流程 2.3 合并策略选择 3 动态分区与小文件问题 3.1 动态分区原理 3.2 动态分区合并策略 3.3 动态分区合并流程…...

基于Springboot+Vue3.0的前后端分离的个人旅游足迹可视化平台

文章目录 0、前言1、前端开发1.1 登录注册页面1.2 首页1.3 足迹管理1.3.1 足迹列表1.3.2 添加足迹1.4 个人中心1.4.1 足迹成就1.4.2 个人信息1.4.3 我的计划2、后端开发2.1 用户接口开发2.2 足迹点接口2.3 旅游计划接口3、完整代码资料下载0、前言 项目亮点: 前端用户权限动态…...

安妮推广导航系统开心版多款主题网址推广赚钱软件推广变现一键统计免授权源码Annie

一、源码描述 这是一套推广导航源码&#xff08;Annie&#xff09;&#xff0c;基于Funadmin框架&#xff08;ThinkPHP8Layui &#xff09;&#xff0c;内置多款主题&#xff0c;可以用于网址推广&#xff0c;或者用于软件推广&#xff0c;PC端软件手机端软件&#xff0c;后台…...

单片机-STM32部分:1、STM32介绍

飞书文档https://x509p6c8to.feishu.cn/wiki/CmpZwTgHhiQSHZkvzjdc6c4Yn1g STM32单片机不是一款芯片&#xff0c;而是一个系列的芯片&#xff1f; STM32系列单片机是ST&#xff08;意法半导体&#xff09;公司开发的一套32位微控制器基于Arm Cortex()-M处理器&#xff0c;它包…...

PHP-session

PHP中&#xff0c;session&#xff08;会话&#xff09;是一种在服务器上存储用户数据的方法&#xff0c;这些数据可以在多个页面请求或访问之间保持。Session提供了一种方式来跟踪用户状态&#xff0c;比如登录信息、购物车内容等。当用户首次访问网站时&#xff0c;服务器会创…...

php artisan resetPass 执行密码重置失败的原因?php artisan resetPass是什么 如何使用?-优雅草卓伊凡

php artisan resetPass 执行密码重置失败的原因&#xff1f;php artisan resetPass是什么 如何使用&#xff1f;-优雅草卓伊凡 可能的原因 命令不存在&#xff1a;如果你没有正确定义这个命令&#xff0c;Laravel 会报错而不是提示”重置密码失败”用户不存在&#xff1a;’a…...

AI大模型-微调和RAG方案选项

在搭建知识库的方向上&#xff0c;有两个落地方案&#xff1a;微调、RAG。两个方案的比对&#xff1a; 方案选型 微调 让大模型&#xff08;LLM&#xff09;去学习现有知识&#xff08;调整大模型的参数&#xff0c;让它学习新的知识&#xff09;&#xff0c;最终生成一个新的…...

MySQL 第一讲---基础篇 安装

前言&#xff1a; 在当今数据驱动的时代&#xff0c;掌握数据库技术已成为开发者必备的核心技能。作为全球最受欢迎的开源关系型数据库&#xff0c;MySQL承载着淘宝双十一每秒50万次的交易请求&#xff0c;支撑着Facebook百亿级的数据存储&#xff0c;更是无数互联网企业的数据…...

【JavaScript-Day 1】从零开始:全面了解 JavaScript 是什么、为什么学以及它与 Java 的区别

Langchain系列文章目录 01-玩转LangChain&#xff1a;从模型调用到Prompt模板与输出解析的完整指南 02-玩转 LangChain Memory 模块&#xff1a;四种记忆类型详解及应用场景全覆盖 03-全面掌握 LangChain&#xff1a;从核心链条构建到动态任务分配的实战指南 04-玩转 LangChai…...

C++ 复习

VS 修改 C 语言标准 右键项目-属性 输入输出 //引用头文件&#xff0c;用<>包裹起来的一般是系统提供的写好的代码 编译器会在专门的系统路径中去进行查找 #include <iostream> //自己写的代码文件一般都用""包裹起来 编译器会在当前文件所在的目录中査…...

数字智慧方案5877丨智慧交通项目方案(122页PPT)(文末有下载方式)

篇幅所限&#xff0c;本文只提供部分资料内容&#xff0c;完整资料请看下面链接 https://download.csdn.net/download/2301_78256053/89575494 资料解读&#xff1a;智慧交通项目方案 详细资料请看本解读文章的最后内容。 智慧交通项目方案是一个全面的设计框架&#xff0c;…...

如何封装一个线程安全、可复用的 HBase 查询模板

目录 一、前言&#xff1a;原生 HBase 查询的痛点 &#xff08;一&#xff09;连接管理混乱&#xff0c;容易造成资源泄露 &#xff08;二&#xff09;查询逻辑重复&#xff0c;缺乏统一的模板 &#xff08;三&#xff09;多线程/高并发下的线程安全性隐患 &#xff08;四…...

VLM Qwen2.5VL GRPO训练微调 EasyR1 多机多卡训练(2)

在之前博客进行了简单的训练尝试:https://www.dong-blog.fun/post/2060 在本博客,将会深入进行多机多卡训练,以及调整训练奖励函数。 之前构建了镜像: docker build . -t kevinchina/deeplearning:r1 FROM hiyouga/verl:ngc-th2.6.0-cu126-vllm0.8.4-flashinfer0.2.2-cx…...

基于建造者模式的信号量与理解建造者模式

信号量是什么&#xff1f; AI解释&#xff1a;信号量&#xff08;Semaphore&#xff09;是操作系统中用于 进程同步与互斥 的经典工具&#xff0c;由荷兰计算机科学家 Edsger Dijkstra 在 1965 年提出。它本质上是一个 非负整数变量&#xff0c;通过原子操作&#xff08;P 操作…...

笔试专题(十四)

文章目录 mari和shiny题解代码 体操队形题解代码 二叉树中的最大路径和题解代码 mari和shiny 题目链接 题解 1. 可以用多状态的线性dp 2. 细节处理&#xff1a;使用long long 存储个数 3. 空间优化&#xff1a;只需要考虑等于’s’&#xff0c;‘sh’&#xff0c;shy’的情况…...

2025年五一数学建模A题【支路车流量推测】原创论文讲解

大家好呀&#xff0c;从发布赛题一直到现在&#xff0c;总算完成了2025年五一数学建模A题【支路车流量推测】完整的成品论文。 给大家看一下目录吧&#xff1a; 摘 要&#xff1a; 一、问题重述 二&#xff0e;问题分析 2.1问题一 2.2问题二 2.3问题三 2.4问题四 2.5 …...

Linux系统:进程程序替换以及相关exec接口

本节重点 理解进程替换的相关概念与原理掌握相关程序替换接口程序替换与进程创建的区别程序替换的注意事项 一、概念与原理 进程程序替换是操作系统中实现多任务和资源复用的关键机制&#xff0c;允许进程在运行时动态加载并执行新程序。 1.1 定义 进程程序替换是指用新程…...

STM32复盘总结——芯片简介

1、stm32介绍 STM32是ST公司基于ARM Cortex-M内核开发的32位微控制器 STM32常应用在嵌入式领域&#xff0c;如智能车、无人机、机器人、无线通信、物联网、工业控制、娱乐电子产品等 STM32功能强大、性能优异、片上资源丰富、功耗低&#xff0c;是一款经典的嵌入式微控制器 目…...

安装深度环境anaconda+cuda+cudnn+pycharm+qt+MVS

下载anaconda,链接:link 默认电脑有显卡驱动,没有的话直接进NVIDIA官网:https://www.nvidia.cn/geforce/drivers/ 下载。 下载cuda 链接:https://developer.nvidia.com/cuda-toolkit-archive 下载cudnn安装包,链接:https://developer.nvidia.com/rdp/cudnn-archive 备注:…...

泰迪杯特等奖案例学习资料:基于多模态特征融合的图像文本检索系统设计

(第十二届泰迪杯数据挖掘挑战赛B题特等奖案例解析) 一、案例背景与核心挑战 1.1 应用场景与行业痛点 随着智能终端与社交媒体的普及,图像与文本数据呈现爆炸式增长,跨模态检索需求日益迫切。传统方法面临以下问题: 语义鸿沟:图像与文本的异构特征分布差异显著,导致跨模…...

进程与线程:05 内核级线程实现

内核级线程代码实现概述 这节课我们要讲内核级线程到底是怎么做出来的&#xff0c;实际上就是要深入探讨内核级线程的代码实现。 在前两节课中&#xff0c;我们学习了用户级线程和内核级线程是如何进行切换的&#xff0c;以及实现切换的核心要点。那两节课讲述的内容&#xf…...

Laravel 12 实现 API 登录令牌认证

Laravel 12 实现 API 登录令牌认证 在 Laravel 12 中实现基于令牌(Token)的 API 认证&#xff0c;可以使用 Laravel Sanctum 或 Laravel Passport。以下是两种方式的实现方法&#xff1a; 方法一&#xff1a;使用 Laravel Sanctum (轻量级 API 认证) 1. 安装 Sanctum compo…...

【Git】万字详解 Git 的原理与使用(上)

&#x1f970;&#x1f970;&#x1f970;来都来了&#xff0c;不妨点个关注叭&#xff01; &#x1f449;博客主页&#xff1a;欢迎各位大佬!&#x1f448; 文章目录 1. 初识 Git1.1 Git 是什么&#xff1f;1.2 为什么要有 Git 2. 安装 Git2.1 Linux-Ubuntu 安装 Git2.2 Windo…...

Python高级爬虫之JS逆向+安卓逆向1.7节: 面向对象

目录 引言: 1.7.1 先理解面向过程 1.7.2 再理解面向对象 1.7.3 面向对象的三大特征 1.7.4 类属性,类方法,静态方法 1.7.5 构造函数,对象属性,对象方法 1.7.6 爬虫接单实现了雪糕自由 引言: 大神薯条老师的高级爬虫+安卓逆向教程: 这套爬虫教程会系统讲解爬虫的初…...

SpringBoot基础(原理、项目搭建、yaml)

SpringBoot&#xff1a;javaweb的一个框架&#xff0c;基于Spring开发&#xff0c;SpringBoot本身并不提供Spring框架的核心特性以及扩展功能&#xff0c;只是用于快速、敏捷的开发新一代基于Spring框架的应用程序&#xff0c;它与Spring框架紧密结合用于提升Spring开发者体验的…...

MTV-SCA:基于多试向量的正弦余弦算法

3 正弦余弦算法 (SCA) 正弦余弦算法&#xff08;SCA&#xff09;是为全局优化而开发的&#xff0c;并受到两个函数&#xff0c;正弦和余弦的启发。与其他基于启发式种群的算法一样&#xff0c;SCA在问题的预设最小值和最大值边界内随机生成候选解。然后&#xff0c;通过应用方…...

STL之vector容器

vector的介绍 1.vector是可变大小数组的容器 2.像数组一样&#xff0c;采用连续的空间存储&#xff0c;也就意味着可以通过下标去访问&#xff0c;但它的大小可以动态改变 3.每次的插入都要开空间吗&#xff1f;开空间就要意味着先开临时空间&#xff0c;然后在拷贝旧的到新…...

Android学习总结之jetpack组件间的联系

在传统安卓开发中&#xff0c;UI 组件&#xff08;Activity/Fragment&#xff09;常面临三个核心问题&#xff1a; 生命周期混乱&#xff1a;手动管理 UI 与数据的绑定 / 解绑&#xff0c;易导致内存泄漏&#xff08;如 Activity 销毁后回调仍在触发&#xff09;。数据断层&am…...

linux的信号量初识

Linux下的信号量(Semaphore)深度解析 在多线程或多进程并发编程的领域中&#xff0c;确保对共享资源的安全访问和协调不同执行单元的同步至关重要。信号量&#xff08;Semaphore&#xff09;作为经典的同步原语之一&#xff0c;在 Linux 系统中扮演着核心角色。本文将深入探讨…...

【安装指南】Centos7 在 Docker 上安装 RabbitMQ4.0.x

目录 前置知识:RabbitMQ 的介绍 一、单机安装 RabbitMQ 4.0.7版本 1.1 在线拉取镜像 二、延迟插件的安装 2.1 安装延迟插件 步骤一:下载延迟插件 步骤二:将延迟插件放到插件目录 步骤三:启动延迟插件 步骤四:重启 RabbitMQ 服务 步骤五:验收成果 步骤六:手动…...

Android和iOS测试的区别有哪些?

作为移动端测试工程师,Android 和 iOS 的测试差异直接影响测试策略设计。本文从测试环境、工具链、兼容性、发布流程等维度全面解析,并附实战建议。 1. 测试环境差异 维度AndroidiOS设备碎片化高(厂商/分辨率/系统版本多样)低(仅苹果设备,版本集中)系统开放性开放(可Ro…...

spring中的@PostConstruct注解详解

基本概念 PostConstruct 是 Java EE 规范的一部分&#xff0c;后来也被纳入到 Spring 框架中。它是一个标记注解&#xff0c;用于指示一个方法应该在依赖注入完成后被自动调用。 主要特点 生命周期回调&#xff1a;PostConstruct 标记的方法会在对象初始化完成、依赖注入完成…...

大模型开发学习笔记

文章目录 大模型基础大模型的使用大模型训练的阶段大模型的特点及分类大模型的工作流程分词化(tokenization)与词表映射 大模型的应用 进阶agent的组成和概念planning规划子任务分解ReAct框架 memory记忆Tools工具\工具集的使用langchain认知框架ReAct框架plan-and-Execute计划…...

【android Framework 探究】pixel 5 内核编译

相关文章&#xff1a; 【android Framework 探究】android 13 aosp编译全记录 【android Framework 探究】android 13 aosp 全记录 - 烧录 一&#xff0c;环境 主机 -> Ubuntu 18.04.6 LTS 内存 -> 16GB 手机 -> pixel 5 代号redfin。kernel代号redbull 二&#xf…...

PowerBI实现点击空白处隐藏弹窗(详细教程)

PowerBI点击空白处隐藏弹窗 第五届PowerBI可视化大赛中亚军作品:金融企业智慧经营分析看板 有个功能挺好玩的&#xff1a;点击空白处隐藏弹窗&#xff0c;gif动图如下&#xff1a; 我们以一个案例分享下实现步骤&#xff1a; 第一步&#xff0c; 先添加一个显示按钮&#xff…...

【git】获取特定分支和所有分支

1 特定分支 1.1 克隆指定分支&#xff08;默认只下载该分支&#xff09; git clone -b <分支名> --single-branch <仓库URL> 示例&#xff08;克隆 某一个 分支&#xff09;&#xff1a; git clone -b xxxxxx --single-branch xxxxxxx -b &#xff1a;指定分支…...

Windows配置grpc

Windows配置grpc 方法一1. 使用git下载grph下载速度慢可以使用国内镜像1.1 更新子模块 2. 使用Cmake进行编译2.1 GUI编译2.2 命令行直接编译 3. 使用Visual Studio 生成解决方法 方法二1. 安装 vcpkg3.配置vckg的环境变量2. 使用 vcpkg 安装 gRPC3. 安装 Protobuf4. 配置 CMake…...

【学习笔记】深入理解Java虚拟机学习笔记——第2章 Java内存区域与内存溢出异常

第2章 Java内存区域与内存溢出异常 2.1 概述 略 2.2 运行时数据区域 2.2.1 程序计数器 线程私有&#xff0c;记录执行的字节码位置 2.2.2 Java 虚拟机栈 线程私有&#xff0c;存储一个一个的栈帧&#xff0c;通过栈帧的出入栈来控制方法执行。 -栈帧&#xff1a;对应一个…...

数字智慧方案6189丨智慧应急综合解决方案(46页PPT)(文末有下载方式)

资料解读&#xff1a;智慧应急综合解决方案 详细资料请看本解读文章的最后内容。 在当前社会环境下&#xff0c;应急管理的重要性愈发凸显。国务院发布的《“十四五” 国家应急体系规划》以及 “十四五” 智慧应急专项规划&#xff0c;明确了应急管理体系建设的方向和重点&…...

解决 3D Gaussian Splatting 中 SIBR 可视化组件报错 uv_mesh.vert 缺失问题【2025最新版!】

一、&#x1f4cc; 引言 在使用 3D Gaussian Splatting&#xff08;3DGS&#xff09;进行三维重建和可视化的过程&#xff0c;SIBR_gaussianViewer_app 是一款官方推荐的本地可视化工具&#xff0c;允许我们在 GPU 上实时浏览重建结果。然而&#xff0c;许多用户在启动该工具时…...