jetpack compose 副作用 snapshotFlow

snapshotFlow 把 Snapshot 状态系统桥接到 Kotlin Flow,让你能用 Flow 操作符处理 Compose 状态变化。produceState把 LiveData 拉进 Compose,snapshotFlow 把 Compose State 推出去给 Flow。 作用:snapshotFlow 不受compose重组机制影响,值变了才读取state值并且emit,转为 Flow流处理。

例子 搜索防抖

kotlin 复制代码
// 场景1:监听 Compose State 变化做异步操作
@Composable
fun SearchScreen() {
    var query by remember { mutableStateOf("") }

    LaunchedEffect(Unit) {
        snapshotFlow { query }        // 监听 query 的变化
            .debounce(300)            // 防抖 300ms
            .filter { it.length > 2 } // 至少3个字符
            .mapLatest { api.search(it) }  // 自动取消上次请求
            .collect { results ->it }
    }
}

// 场景2:将 Snapshot State 转为 StateFlow
val state = MutableStateFlow(0)
LaunchedEffect(Unit) {
    snapshotFlow { scrollState.value }
        .collect { state.value = it }
}

多状态组合 → 单一 Flow

scss 复制代码
@Composable
fun FormScreen() {
    var name by remember { mutableStateOf("") }
    var email by remember { mutableStateOf("") }
    var age by remember { mutableStateOf("") }
    var isFormValid by remember { mutableStateOf(false) } //这里可以使用derivedStateOf 

    LaunchedEffect(Unit) {
        snapshotFlow {
            name.isNotBlank() && email.contains("@") && (age.toIntOrNull() ?: 0) > 0
        }.collect { isFormValid = it }
    }

    Column {
        TextField(value = name, onValueChange = { name = it })
        TextField(value = email, onValueChange = { email = it })
        TextField(value = age, onValueChange = { age = it })
        Button(enabled = isFormValid, onClick = { /* submit */ }) {
            Text("提交")
        }
    }
}

滚动位置监听

scss 复制代码
@Composable
fun CollapsingToolbar() {
    val scrollState = rememberScrollState()
    var isCollapsed by remember { mutableStateOf(false) }

    LaunchedEffect(Unit) {
        snapshotFlow { scrollState.value > 200 }
            .distinctUntilChanged()
            .collect { isCollapsed = it }
    }

    Box {
        LazyColumn(state = scrollState) { /* ... */ }
        TopAppBar(
            modifier = Modifier.animateContentSize(),
            title = if (isCollapsed) Text("简洁模式") else Text("完整标题")
        )
    }
}

写入 SharedPreferences / DataStore

kotlin 复制代码
@Composable
fun ThemeToggle(dataStore: DataStore<Preferences>) {
    var isDark by remember { mutableStateOf(false) }

    LaunchedEffect(Unit) {
        snapshotFlow { isDark }
            .drop(1)                    // 跳过初始值,避免启动时就写入
            .collect { dark ->
                dataStore.edit { prefs ->
                    prefs[THEME_KEY] = dark
                }
            }
    }

    Switch(checked = isDark, onCheckedChange = { isDark = it })
}

地图中心点跟随 + 节流(GPS 上报类场景)

kotlin 复制代码
@Composable
fun MapScreen() {
    var cameraPosition by remember { mutableStateOf(CameraPosition(0.0, 0.0, 12f)) }

    LaunchedEffect(Unit) {
        snapshotFlow { cameraPosition }
            .sample(1000)               // 每秒最多 emit 一次
            .collect { pos ->
                analytics.track("map_move", "lat" to pos.lat, "lng" to pos.lng)
            }
    }
}

原理:

kotlin 复制代码
fun <T> snapshotFlow(block: () -> T): Flow<T> = flow {
    val manager = SnapshotFlowManager()
    val needToRerunBlock = Channel<Unit>(1)

    try {
        var lastValue = manager.runAndWatch(needToRerunBlock, block)
        emit(lastValue)                              // ① 首值

        while (true) {
            needToRerunBlock.receive()               // ② 等待变化信号
            val newValue = manager.runAndWatch(needToRerunBlock, block)
            if (newValue != lastValue) {             // ③ 去重
                lastValue = newValue
                emit(newValue)
            }
        }
    } finally {
        manager.reportSnapshotFlowCancellation(...)  // ④ 取消时清理
        manager.dispose()
    }
}

四步循环:执行 block → 记录读取的 state → 等待 ApplyObserver 信号 → 值变化则 emit → 循环。协程取消时 receive() 抛 CancellationException,finally 清理订阅。

runAndWatch --- 如何"记录读取"

kotlin 复制代码
// ★ 字段初始化时注册,Manager 创建即注册
private val unregisterApplyObserver =
    Snapshot.registerApplyObserver { changed, _ ->
        var toNotify: SendChannel<Unit>? = null
        synchronized(lock) {
            val watchSet = watchSet
            if (watchSet == null) {
                // 只观察了一个对象,直接 contains
                if (changed.contains(soleWatchedObject)) {
                    toNotify = subscribedChannel    // ★ needToRerunBlock 赋值
                }
            } else {
                // 观察了多个对象,遍历检查
                if (watchSet.any { changed.contains(it) }) {
                    toNotify = subscribedChannel
                }
            }
        }
        toNotify?.trySend(Unit)    // ★ 命中 → 发信号
    }

override fun dispose() {
    unregisterApplyObserver.dispose()   // ★ 销毁时注销
    // ...
}


internal fun <T> runAndWatch(channel: SendChannel<Unit>, block: () -> T): T {
    val result = Snapshot.takeSnapshot(readObserverFor(channel))  // 创建可读快照
        .run {
            clearWatchSet(channel)          // 清空上一次的观察列表
            try {
                enter(block)                // 在快照中执行 block
            } finally {
                dispose()
            }
        }
    commitSubscriptionChanges()             // 将新观察列表提交生效
    return result
}

关键在于 Snapshot.takeSnapshot(readObserver). 它创建了一个"可追踪读取"的快照------block 中每读取一个 mutableStateOf 对象,都会回调 readObserver,从而在 Manager 中记录 "这个 state 对象被订阅了"。

相关推荐
古法安卓2 小时前
Android-epoll原理详解
android·java·android studio
字节暗面4 小时前
四款 APP 加固强度测评:让 AI 拆了四个包,结果有点出乎意料
android·逆向工程·加固
网安蟹佬霸4 小时前
蓝队防御实战:SIEM与SOAR构建安全运营中心(万字详解+实战配置)
android·安全·web安全·网络安全·网安
敢敢のwings5 小时前
世界模型与FlashWorld:从“在梦中学习“到秒级3D场景生成
android·学习·3d
Dovis(誓平步青云)6 小时前
牛奶不能减成负数:冰箱库存的筛选、去重与不可变更新
android·java·开发语言
Awna7 小时前
MySQL 存储空间与索引运维实战:从空间排查到在线扩容
android·运维·mysql
智购科技无人售货机工厂7 小时前
2026自动售货机触摸屏驱动开发:从I2C协议到多点触控校准的工程实践~YH
android·驱动开发·python·单片机·云原生·django
又见情义7 小时前
RK3568 Android 13驱动适配-WiFi&BT模块(RTL8822CS)
android·arm开发·驱动开发
美狐美颜SDK开放平台7 小时前
直播APP源码如何实现美颜功能?视频美颜SDK开发方案详解
android·大数据·人工智能·计算机视觉·直播美颜sdk
pengyu8 小时前
【Kotlin 协程修仙录 · 化神境 · 后阶】 | 异常天劫:Flow 异常处理的终极奥义与重试之道
android·kotlin