kotlin flow的两种SharingStarted策略的区别

一 两种 SharingStarted 策略的区别:

  1. SharingStarted.Eagerly:
  • 立即开始收集上游流,即使没有下游订阅者
  • 持续保持活跃状态,直到 ViewModel 被清除
  • 优点:响应更快,数据始终保持最新
  • 缺点:消耗更多资源,因为始终在收集数据
  1. SharingStarted.WhileSubscribed(5000):
  • 仅在有下游订阅者时才开始收集
  • 停止收集后等待 5000 毫秒才真正停止上游流
  • 优点:更节省资源
  • 缺点:首次订阅时可能有短暂延迟

选择建议:

kotlin 复制代码
// 适合使用 Eagerly 的场景:
// 1. 需要立即获取和保持数据最新状态
// 2. 数据更新频繁且重要的场景
val fragranceChannel = FragranceRepository.observeSelectedChannel()
    .stateIn(viewModelScope, SharingStarted.Eagerly, DEFAULT_CHANNEL)

// 适合使用 WhileSubscribed 的场景:
// 1. 数据不需要实时更新
// 2. 想要节省资源的场景
val acStatus = ACStatusRepository.acSwitchStatus
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)

两种策略说明

这是一个典型的Flow订阅场景。解释上下游关系:

  1. 上游(Upstream)
kotlin 复制代码
// 在 ViewModel 中
val fragranceChannelMaterialCardView = ToggleButtonState(
    state = FragranceRepository.observeSelectedChannel()  // 这是上游数据源
        .stateIn(viewModelScope, SharingStarted.Eagerly, DEFAULT_CHANNEL),
    ...
)
  1. 下游(Downstream)
kotlin 复制代码
// 在 Fragment 中
viewModel.fragranceChannelMaterialCardView.state.collect { pos ->  // 这是下游订阅者
    binding.fragranceSelectedChannel = pos
}

流程说明:

  1. FragranceRepository.observeSelectedChannel() 产生数据
  2. .stateIn() 将Flow转换为StateFlow
  3. Fragment中的 .collect 订阅这个StateFlow
  4. 当上游数据变化时,下游会收到通知并更新UI

这就像一个管道:

复制代码
数据源(Repository) -> StateFlow(ViewModel) -> 订阅者(Fragment)
[上游]              [中转站]               [下游]

使用 SharingStarted.Eagerly 意味着即使没有下游订阅,上游也会一直产生数据

如果改用 WhileSubscribed,只有在Fragment 订阅时才会开始收集数据

二 SharingStarted.Eagerly示例

SharingStarted.Eagerly 的收集机制:

kotlin 复制代码
class WeatherViewModel : ViewModel() {
    // 上游数据源 - 模拟温度传感器
    private val temperatureSource = flow {
        var temp = 20
        while(true) {
            emit(temp++)
            delay(1000)
            println("上游发射温度: $temp") // 日志观察发射
        }
    }

    // 使用 Eagerly 立即开始收集
    val temperature = temperatureSource.stateIn(
        scope = viewModelScope,
        started = SharingStarted.Eagerly, // 立即开始收集
        initialValue = 0
    )

    init {
        // 可以在这里观察收集到的值
        viewModelScope.launch {
            temperature.collect { temp ->
                println("ViewModel 内部收到温度: $temp")
            }
        }
    }
}

// UI层使用
class WeatherFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 即使这里暂时还没有收集,上游也在发射数据
        // 延迟5秒后再开始收集
        lifecycleScope.launch {
            delay(5000)
            viewModel.temperature.collect { temp ->
                binding.tempText.text = "$temp°C"
            }
        }
    }
}

关键点解释:

  1. 立即收集的意义

    • 即使没有下游订阅者,StateFlow 也会保持最新值
    • 当下游开始订阅时,可以立即获得最新状态
    • 适合需要持续监控或后台处理的场景
  2. 收集过程

    上游发射温度: 20 (立即开始)
    上游发射温度: 21
    上游发射温度: 22
    上游发射温度: 23
    上游发射温度: 24
    [5秒后 Fragment 开始收集]
    Fragment收到温度: 24 (立即获得最新值)
    上游发射温度: 25
    Fragment收到温度: 25

  3. 适用场景

kotlin 复制代码
class LocationViewModel : ViewModel() {
    // 位置追踪需要持续进行,即使UI暂时不可见
    val location = locationManager.locationUpdates()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.Eagerly,
            initialValue = defaultLocation
        )
}
  1. 与 WhileSubscribed 的对比
kotlin 复制代码
// WhileSubscribed - 有订阅者才收集
val temperature1 = temperatureSource.stateIn(
    scope = viewModelScope,
    started = SharingStarted.WhileSubscribed(5000),
    initialValue = 0
)

// Eagerly - 立即开始收集
val temperature2 = temperatureSource.stateIn(
    scope = viewModelScope,
    started = SharingStarted.Eagerly,
    initialValue = 0
)
  1. 使用建议
  • 如果数据源消耗资源较大,建议使用 WhileSubscribed
  • 如果需要后台持续处理或保持最新状态,使用 Eagerly
  • 大多数UI状态场景,推荐使用 WhileSubscribed
kotlin 复制代码
class CarViewModel : ViewModel() {
    // 车辆状态需要持续监控 - 使用 Eagerly
    val carStatus = carMonitor.status.stateIn(
        scope = viewModelScope,
        started = SharingStarted.Eagerly,
        initialValue = CarStatus.Unknown
    )

    // UI展示数据 - 使用 WhileSubscribed
    val uiState = dataSource.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = UiState()
    )
}

所以,Eagerly 的立即收集主要用于:

  • 需要持续后台处理的场景
  • 状态不能中断的监控场景
  • 需要立即响应的关键数据流

三 SharingStarted.WhileSubscribed(5000) 示例

下面通过一个具体示例来说明下游停止收集的情况:

kotlin 复制代码
class TemperatureViewModel : ViewModel() {
    // 上游数据源
    private val temperatureSource = flow {
        var temp = 20
        while(true) {
            emit(temp++)
            delay(1000) 
        }
    }

    // 使用 WhileSubscribed(5000) 转换为 StateFlow
    val temperature = temperatureSource.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = 0
    )
}

// Fragment/Activity 中使用
class TemperatureFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 开始收集 - 这时上游 flow 会开始发射数据
        viewLifecycleOwner.lifecycleScope.launch {
            viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.temperature.collect { temp ->
                    binding.tempText.text = "$temp°C"
                }
            }
        }
    }
}

以下情况会导致下游停止收集:

  1. Fragment 进入 STOPPED 状态(如切到后台):
kotlin 复制代码
// repeatOnLifecycle 会在 STOPPED 时自动取消收集
// 但会等待 5000ms 后才真正停止上游 flow
onStop() {
    // 此时下游停止收集,但上游继续运行 5000ms
}
  1. 显式取消协程:
kotlin 复制代码
val job = lifecycleScope.launch {
    viewModel.temperature.collect { }
}

// 取消协程会停止收集
job.cancel() // 上游会在 5000ms 后停止
  1. Fragment/Activity 销毁:
kotlin 复制代码
onDestroy() {
    // lifecycleScope 取消导致收集停止
    // 上游会在 5000ms 后停止
}

WhileSubscribed(5000) 的好处是:

  • 短时间内重新订阅时(如快速切换页面)无需重启上游 flow
  • 避免频繁启停上游带来的开销
  • 5000ms 后才真正停止,可以平衡资源使用和响应性

所以它特别适合:

  • 需要共享的开销较大的数据流
  • 页面快速切换的场景
  • 需要缓存最新值的场景
相关推荐
2501_916008895 小时前
Web 前端开发常用工具推荐与团队实践分享
android·前端·ios·小程序·uni-app·iphone·webview
我科绝伦(Huanhuan Zhou)5 小时前
MySQL一键升级脚本(5.7-8.0)
android·mysql·adb
怪兽20147 小时前
Android View, SurfaceView, GLSurfaceView 的区别
android·面试
龚礼鹏7 小时前
android 图像显示框架二——流程分析
android
消失的旧时光-19437 小时前
kmp需要技能
android·设计模式·kotlin
帅得不敢出门8 小时前
Linux服务器编译android报no space left on device导致失败的定位解决
android·linux·服务器
雨白9 小时前
协程间的通信管道 —— Kotlin Channel 详解
android·kotlin
TimeFine11 小时前
kotlin协程 容易被忽视的CompletableDeferred
android
czhc114007566312 小时前
Linux1023 mysql 修改密码等
android·mysql·adb
GOATLong13 小时前
MySQL内置函数
android·数据库·c++·vscode·mysql