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 对象被订阅了"。

相关推荐
爱笑鱼1 小时前
Binder 实战(九):一次调用变慢,到底卡在客户端、驱动、Binder 线程还是 Handler?
android
恋猫de小郭2 小时前
Flutter hit,一个可以灵活控制溢出点击的第三方包
android·前端·flutter
我命由我123452 小时前
Kotlin 面向对象 - 枚举排序
android·java·开发语言·java-ee·kotlin·android studio·android-studio
fengci.19 小时前
Microweber CMS 未授权路径穿越漏洞(CVE-2026-65694)
android·开发语言·前端·学习·php
stevenzqzq20 小时前
Android 矢量图(Vector Drawable)核心坐标与尺寸机制技术文档
android
plainGeekDev21 小时前
运行时获取依赖 → 编译时注入
android·java·kotlin
Wiktok21 小时前
Vue3(Vite)打包安卓APP 完整最佳实践
android·vue
略略略咯咯21 小时前
LocalDateTime
android
Co_Hui21 小时前
Android 系统服务的添加
android