Compose笔记(五十七)--snapshotFlow

这一节主要了解一下snapshotFlow,在Compose开发中,snapshotFlow是连接Compose状态与协程流的核心API,属于androidx.compose.runtime包,用于将Compose可观察状态转换为冷流,当状态变化时自动发射新值。

API:

Kotlin 复制代码
fun <T> snapshotFlow(
    block: () -> T
): Flow<T>

block:Lambda表达式,返回需要观察的Compose状态值(如mutableStateOf包装的值)。

返回值:Flow<T>,当block中的状态发生变化时,流会自动发射最新值。

特性

状态感知:仅当block中的状态值实际变化(且前后值不同)时,才会发射新值。

Compose上下文绑定:需在Compose的Composition上下文中使用(如LaunchedEffect、rememberCoroutineScope中)。

冷流特性:流在被收集(collect)时才会开始监听状态变化,取消收集则停止监听。

场景

1 监听非State对象的变化:如果有某些数据不是通过mutableStateOf创建的状态,但是希望在它们发生变化时能够触发重组或执行某些操作,可以使用snapshotFlow来监听这些数据的变化。

2 合并多个状态到一个流:有时候你可能需要根据多个State的值来决定何时执行某个操作。在这种情况下,可以使用snapshotFlow来监听多个状态,并且只有当这些状态中的任何一个发生改变时,才会触发相应的逻辑。

3 异步操作:当你需要基于某些状态执行异步操作,并且只在状态变化时才重新执行这些操作,这时snapshotFlow就能派上用场。可以通过collect或其他方式订阅这个流,并在状态变化时执行你的异步代码。

栗子:

Kotlin 复制代码
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Search
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*


val searchSuggestions = listOf(
    "Compose Animation", "Compose State", "Kotlin",
    "Android Jetpack", "Material Design", "Coroutines"
)

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchDemo() {
    var searchText by remember { mutableStateOf(TextFieldValue("")) }
    var suggestions by remember { mutableStateOf(emptyList<String>()) }

    
    LaunchedEffect(Unit) {
        snapshotFlow { searchText.text }
            .filter { it.isNotBlank() } 
            .debounce(1000L) 
            .distinctUntilChanged() 
            .map { query ->
              
                delay(300) 
                searchSuggestions.filter { it.contains(query, ignoreCase = true) }
            }
            .catch { e -> println("搜索出错:${e.message}") }
            .collect { result ->
                suggestions = result 
            }
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("实时搜索") },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.primaryContainer
                )
            )
        }
    ) { padding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
                .padding(16.dp)
        ) {
            // 搜索框
            OutlinedTextField(
                value = searchText,
                onValueChange = { searchText = it },
                leadingIcon = { Icon(Icons.Outlined.Search, contentDescription = "搜索") },
                placeholder = { Text("输入关键词搜索...") },
                shape = RoundedCornerShape(12.dp),
                modifier = Modifier.fillMaxWidth()
            )

            Spacer(modifier = Modifier.height(16.dp))

            
            if (searchText.text.isNotBlank()) {
                if (suggestions.isNotEmpty()) {
                    LazyColumn(
                        modifier = Modifier.fillMaxSize(),
                        verticalArrangement = Arrangement.spacedBy(8.dp)
                    ) {
                        items(suggestions) { suggestion ->
                            Card(
                                modifier = Modifier.fillMaxWidth(),
                                elevation = CardDefaults.cardElevation(2.dp),
                                shape = RoundedCornerShape(8.dp)
                            ) {
                                Text(
                                    text = suggestion,
                                    modifier = Modifier.padding(16.dp),
                                    style = MaterialTheme.typography.bodyLarge
                                )
                            }
                        }
                    }
                } else {
                    Box(
                        modifier = Modifier.fillMaxSize(),
                        contentAlignment = Alignment.Center
                    ) {
                        Text("暂无搜索结果", color = MaterialTheme.colorScheme.onSurfaceVariant)
                    }
                }
            }
        }
    }
}
Kotlin 复制代码
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.*


data class BmiCategory(val range: String, val color: Color, val description: String)

val bmiCategories = listOf(
    BmiCategory("< 18.5", Color(0xFF64B5F6), "偏瘦"),
    BmiCategory("18.5 - 23.9", Color(0xFF81C784), "正常"),
    BmiCategory("24.0 - 27.9", Color(0xFFFFB74D), "超重"),
    BmiCategory("≥ 28.0", Color(0xFFE57373), "肥胖")
)

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BmiCalculatorDemo() {
    var heightCm by remember { mutableStateOf("") } 
    var weightKg by remember { mutableStateOf("") } 
    var bmiValue by remember { mutableStateOf(0.0) }
    var currentCategory by remember { mutableStateOf<BmiCategory?>(null) }

    
    LaunchedEffect(Unit) {
        snapshotFlow {
            
            val height = heightCm.toDoubleOrNull() ?: 0.0
            val weight = weightKg.toDoubleOrNull() ?: 0.0
            Pair(height, weight)
        }
            .map { (height, weight) ->
                
                if (height > 0 && weight > 0) {
                    weight / ((height / 100) * (height / 100))
                } else 0.0
            }
            .distinctUntilChanged() 
            .collect { bmi ->
                bmiValue = bmi
               
                currentCategory = when {
                    bmi < 18.5 -> bmiCategories[0]
                    bmi < 24.0 -> bmiCategories[1]
                    bmi < 28.0 -> bmiCategories[2]
                    bmi >= 28.0 -> bmiCategories[3]
                    else -> null
                }
            }
    }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("BMI计算器") },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.primary
                )
            )
        }
    ) { padding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding)
                .padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Top
        ) {
            
            Card(
                modifier = Modifier.fillMaxWidth(),
                elevation = CardDefaults.cardElevation(4.dp),
                shape = RoundedCornerShape(16.dp)
            ) {
                Column(
                    modifier = Modifier.padding(24.dp),
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.spacedBy(16.dp)
                ) {
                    OutlinedTextField(
                        value = heightCm,
                        onValueChange = { heightCm = it },
                        label = { Text("身高(厘米)") },
                        placeholder = { Text("请输入身高") },
                        singleLine = true,
                        modifier = Modifier.fillMaxWidth(),
                        keyboardOptions = KeyboardOptions(
                            keyboardType = androidx.compose.ui.text.input.KeyboardType.Number
                        )
                    )

                    OutlinedTextField(
                        value = weightKg,
                        onValueChange = { weightKg = it },
                        label = { Text("体重(公斤)") },
                        placeholder = { Text("请输入体重") },
                        singleLine = true,
                        modifier = Modifier.fillMaxWidth(),
                        keyboardOptions = KeyboardOptions(
                            keyboardType = KeyboardType.Number
                        )
                    )
                }
            }

            Spacer(modifier = Modifier.height(32.dp))

            
            if (bmiValue > 0) {
                Column(
                    horizontalAlignment = Alignment.CenterHorizontally,
                    verticalArrangement = Arrangement.spacedBy(16.dp)
                ) {
                    Text(
                        text = "你的BMI值",
                        style = MaterialTheme.typography.titleMedium,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )

                   
                    Card(
                        modifier = Modifier.size(180.dp),
                        shape = CircleShape,
                        colors = CardDefaults.cardColors(
                            containerColor = currentCategory?.color ?: MaterialTheme.colorScheme.surfaceVariant
                        ),
                        elevation = CardDefaults.cardElevation(8.dp)
                    ) {
                        Box(
                            modifier = Modifier.fillMaxSize(),
                            contentAlignment = Alignment.Center
                        ) {
                            Text(
                                text = String.format("%.1f", bmiValue),
                                style = MaterialTheme.typography.displaySmall.copy(fontWeight = FontWeight.Bold),
                                color = Color.White
                            )
                        }
                    }

                   
                    currentCategory?.let { category ->
                        Row(
                            verticalAlignment = Alignment.CenterVertically,
                            horizontalArrangement = Arrangement.Center,
                            modifier = Modifier.padding(8.dp)
                        ) {
                            Icon(
                                Icons.Outlined.Info,
                                contentDescription = "信息",
                                tint = category.color,
                                modifier = Modifier.size(20.dp)
                            )
                            Spacer(modifier = Modifier.width(8.dp))
                            Text(
                                text = "BMI ${category.range}:${category.description}",
                                style = MaterialTheme.typography.bodyLarge,
                                color = category.color
                            )
                        }
                    }
                }
            } else {
                
                Box(
                    modifier = Modifier.fillMaxSize(),
                    contentAlignment = Alignment.Center
                ) {
                    Text(
                        text = "请输入有效的身高和体重",
                        style = MaterialTheme.typography.bodyLarge,
                        color = MaterialTheme.colorScheme.onSurfaceVariant
                    )
                }
            }
        }
    }
}

注意:

1.不要在snapshotFlow中捕获外部可变状态

2.避免在snapshotFlow内部执行副作用或耗时操作

3.注意snapshotFlow的初始发射行为

4.避免在snapshotFlow中监听非稳定(unstable)对象

源码:

Kotlin 复制代码
val readSet = MutableScatterSet<Any>()
val readObserver: (Any) -> Unit = {
    if (it is StateObjectImpl) {
        it.recordReadIn(ReaderKind.SnapshotFlow)
    }
    readSet.add(it)
}

说明:记录block执行时读取的所有状态对象

StateObjectImpl:Compose内部所有mutableStateOf创建的状态都实现此接口。

recordReadIn(ReaderKind.SnapshotFlow):标记该状态"被 snapshotFlow 读过",用于后续筛选。

readSet:保存所有被读取的对象引用(包括非State对象,但只有State才会触发更新)。

Kotlin 复制代码
val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
    val maybeObserved = changed.fastAny {
        it !is StateObjectImpl || it.isReadIn(ReaderKind.SnapshotFlow)
    }
    if (maybeObserved) {
        appliedChanges.trySend(changed)
    }
}

说明:状态变更监听

registerApplyObserver注册一个全局监听器,当任何Snapshot状态被修改并提交(apply)时触发。

changed:本次事务中所有被修改的状态集合。

Kotlin 复制代码
var lastValue = Snapshot.takeSnapshot(readObserver).run {
    try {
        enter(block)
    } finally {
        dispose()
    }
}
emit(lastValue)

说明:首次执行与初始值发射

Snapshot.takeSnapshot(readObserver):创建一个带readObserver的快照上下文;

enter(block):在该上下文中执行block,此时所有状态读取都会被readObserver捕获;

执行完后立即emit初始值。

Kotlin 复制代码
while (true) {
    var found = false
    var changedObjects = appliedChanges.receive()

    // Poll for any other changes before running block
    while (true) {
        found = found || readSet.intersects(changedObjects)
        changedObjects = appliedChanges.tryReceive().getOrNull() ?: break
    }

    if (found) {
        readSet.clear()
        val newValue = Snapshot.takeSnapshot(readObserver).run { ... }
        if (newValue != lastValue) {
            emit(newValue)
            lastValue = newValue
        }
    }
}

说明:循环监听变化+防抖优化

批量处理变更(防抖):

使用while(tryReceive())循环拉取所有pending的appliedChanges;

避免因高频状态更新导致block被反复执行(例如连续快速点击按钮)。

精确依赖判断:

readSet.intersects(changedObjects):只有当本次变更的状态与上次读取的状态有交集,才重新计算。

值比较去重:

即使状态变了,如果block返回值未变(如filter后结果相同),不发射,避免无效更新。

重置readSet:

每次重新执行block前清空readSet,确保只记录最新一轮的依赖。

简单总结:

Kotlin 复制代码
1.注册apply observer(监听全局状态变更)
2.首次执行block → 记录依赖(readSet)→ emit 初始值
3.进入无限循环:
   3.1.等待appliedChanges(状态变更事件)
   3.2.批量拉取所有pending变更
   3.3.检查是否有变更影响当前依赖(intersects)
   3.4.若有 → 重新执行 block → 比较值 → emit(若不同)
相关推荐
花阴偷移2 小时前
kotlin语法(上)
android·java·开发语言·kotlin
Smart-佀2 小时前
Android初学必备:选Kotlin 还是Java ?
android·android studio·安卓
普通网友2 小时前
Android kotlin Jetpack mvvm 项目
android·开发语言·kotlin
大耳猫2 小时前
Android Kotlin 协程详解
android·kotlin·协程
Crogin2 小时前
快速简单入门Kotlin——基础语法(第一天)
android·开发语言·kotlin
Ziegler Han2 小时前
《升维》阅读笔记:在不确定的世界里,如何做出高确定性的决策
笔记·《升维》
工程师平哥2 小时前
APE-01 新建工程
笔记·嵌入式硬件
泓博2 小时前
Android摇一摇
笔记
国服第二切图仔2 小时前
Electron for鸿蒙PC实战项目之简易绘图板应用
android·electron·开源鸿蒙·鸿蒙pc