Compose出来有一段时间,很多人一直不愿意使用,一部分是之前的性能问题,还有一部分是很多项目其实是祖传的,谁也不敢冒然引入新技术,毕竟锅背起来很累。
写在文章开头:
目前这方面的文章很多,很多大佬写的很详细,分了很多章,确实非常精彩。但,很多时候我只是想先把技术用起来,并没有那么多时间让我慢慢品味,在使用中学习才是王道。同时也记录下这几天学习的成果,好记性不如烂笔头,那这篇文章就诞生。
如果你已经有了一定的Compose的基础,那这篇文章不看也罢。本文更像是知识点的流水账式的堆砌,主要让大家快速掌握一些基础概念,以防不时之需。
一、控件篇
- Text
kotlin
@Composable
fun SimpleText() {
//蓝色、24sp、粗体、居中的文字
Text(
text = "Hello, Compose!",
color = Color.Blue,
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Center
)
}
- Button
kotlin
@Composable
fun SimpleButton() {
Button(onClick = { /* 这里执行点击逻辑,比如 Toast */ }) {
Text("点击我")
}
}
按钮里面可以放任意 Composable,比如 Text 或 Icon。需要注意的是,Button本身不提供默认内容,需要手动填充 Text 或 Icon"
- Image
Image 用于加载图片,支持本地资源、网络 URL 或 Bitmap。Image不内置网络加载,但支持 painter。可以使用Coil和Glide实现网络加载,Compose里面我更支持Coil。
kotlin
import androidx.compose.foundation.Image
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import coil.compose.rememberAsyncImagePainter // 需要添加 Coil 依赖
@Composable
fun SimpleImage() {
// 本地图片
Image(
painter = painterResource(id = R.drawable.ic_launcher),
contentDescription = "App Icon"
)
// 网络图片(用 Coil 库)
Image(
painter = rememberAsyncImagePainter("https://example.com/image.jpg"),
contentDescription = "Network Image"
)
}
Coil的更多的用法不再阐述了,感兴趣的小伙伴自己去问AI。
- TextField / OutlinedTextField 描述:TextField是一个基本的文本输入框,允许用户输入文本,支持自定义标签、占位符等等。
代码示例:
kotlin
@Composable
fun TextFieldExample() {
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("请输入用户名") }, // 标签
placeholder = { Text("例如:张三") } // 占位符
)
}
@Preview
@Composable
fun TextFieldPreview() {
TextFieldExample()
}
OutlinedTextField用法是与TextField大致相同,它们主要的区别就是OutlinedTextField 是 TextField的带边框变体,样式更现代.
- Icon
描述:Icon 用于显示矢量图标,通常与 Icons 库(如 Icons.Filled)一起使用,适合在按钮或输入框中添加视觉元素。 代码示例:
kotlin
@Composable
fun IconExample() {
Icon(
imageVector = Icons.Filled.Favorite, // 使用内置的爱心图标
contentDescription = "收藏图标" // 无障碍描述
)
}
- Switch
描述:Switch 是一个开关控件,允许用户在两种状态(开/关)之间切换,适合设置选项
kotlin
@Composable
fun SwitchExample() {
var checked by remember { mutableStateOf(false) }
Switch(
checked = checked,
onCheckedChange = { checked = it } // 切换状态时更新
)
}
- Card
描述:Card 是一个带有阴影和圆角的容器,用于分组内容,适合展示卡片式布局。 代码示例:
kotlin
@Composable
fun CardExample() {
Card(
modifier = Modifier.padding(16.dp) // 外边距
) {
Text(
text = "这是一个卡片示例",
modifier = Modifier.padding(16.dp) // 内容内边距
)
}
}
- Divider
描述:Divider 是一个水平或垂直的分隔线,用于分隔内容区域。 代码示例:
kotlin
@Composable
fun DividerExample() {
Divider(
thickness = 2.dp, // 分隔线厚度
modifier = Modifier.padding(horizontal = 16.dp) // 水平边距
)
}
- Spacer
描述:Spacer 是一个不可见的占位控件,用于在布局中添加间距,控制组件之间的距离,可以在效果上代替xml的margin效果 代码示例:
kotlin
@Composable
fun SpacerExample() {
Column {
Text("第一行文本")
Spacer(modifier = Modifier.height(16.dp)) // 添加16dp的垂直间距
Text("第二行文本")
}
}
更多:
Image和Icon的区别:
| 特性 | Icon | Image |
|---|---|---|
| 用途 | 显示矢量图标或简单图像 | 显示位图、照片或复杂图像 |
| 颜色控制 | 支持tint改色 | 显示原始图像颜色,无tint属性 |
| 缩放裁剪 | 简单缩放,适合小图标 | 支持复杂缩放(如 ContentScale) |
| 性能 | 轻量,适合频繁使用的小图标 | 更适合高质量、大型图像,稍重 |
| 使用场景 | 按钮、导航、装饰性图标 | 头像、背景、产品图片等 |
二、布局篇
- Box
- Column
- Row
- LazyColumn
- LazyRow
- LazyVerticalGrid
- LazyHorizontalGrid
- ConstraintLayout
布局就不详细介绍了,太占篇幅,我学习了下,发现也比较好上手,就不浪费时间了。
三、Modifier究竟是个什么东西
Modifier 是 Compose 的一个核心概念,是 Compose 的灵魂,掌握它就能灵活布局。 Modifier 像链条一样链接:.padding().background().clickable() 等。 简单说,Modifier 是个接口,提供了尺寸、间距、点击、拖拽等功能,可以无限制串联API。
主要负责4个方面的功能:
- 修改Compose控件的外观(尺寸、布局、样式、行为),而不改变组件本身。
- 为Compose控件添加额外的信息,如适配盲人或弱视的无障碍标签。
- 处理用户的事件输入(
非TextField文本输入),如点击、滑动、拖动等等, - 让不可交互控件变得可交互(可点击、可滚动、可拖拽等),如Box控件变得可点击。
这正是不可变对象的设计:每次操作返回新实例,原对象不变,那举个例子,大家就明白了。
kotlin
// 虽然每次返回新实例,但由于不可变性,Compose 可以高效处理
val baseModifier = Modifier.fillMaxSize()
val paddedModifier = baseModifier.padding(16.dp) // 新实例,但不影响 baseModifier
val finalModifier = paddedModifier.background(Color.Red) // 又一新实例
新的实例修改,不会影响到其他使用相同Modifier的地方,状态隔离。
为什么用 Modifier?
- 复用:同一个控件用不同 Modifier 变身不同样式。
- 链式调用:易读。
kotlin
@Composable
fun ModifierExample() {
Text(
text = "Modified Text",
modifier = Modifier
.padding(16.dp) // 外边距
.background(Color.Yellow) // 背景色
.clickable { /* 点击事件 */ } // 可点击
)
}
这里,Text 有了 padding、黄背景和点击功能。
大重点:顺序重要!Modifier 按调用顺序应用,这一点跟XML不同。大家可以尝试一下,理解更深刻。
kotlin
Modifier.padding(16.dp).background(Color.Red) // 先 padding 再背景:背景在内层
Modifier.background(Color.Red).padding(16.dp) // 先背景再 padding:背景在外层
四、State,最难理解的概念之一
1、一句话概括:
UI 是由状态(State)驱动生成的,而不是命令式地修改 UI 。
定义一个 state 变量,当它变化时,Compose会找出哪些 Composable 读取了这个 state ,Compose会 自动重组(Recompose)这些UI,且只会执行这些函数。 举个最简单的例子:
kotlin
@Composable
fun Counter() {
// remember 保存状态,使其在重组后依然保留
var count by remember { mutableStateOf(0) }
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("当前计数:$count")
Button(onClick = { count++ }) {
Text("点击 +1")
}
}
}
解析:
count是一个状态(State<Int>)- 当
Button点击时,count变了。 - Compose 检测到
count改变后,会 自动重组(recompose) 相关 UI。 Text("当前计数:$count")会自动显示新值。
2、核心相关工具总结(重要):
| 工具 | 作用 | 适用场景 |
|---|---|---|
remember { mutableStateOf() } |
创建并记住可变状态 | 组件内部状态(如计数器) |
rememberSaveable { ... } |
状态可在重建时恢复(如旋转屏幕) | 屏幕旋转、进程重启 |
derivedStateOf { ... } |
派生状态,避免重复计算 | 从其他状态计算出新值 |
StateFlow / LiveData |
与 ViewModel 交互 | ViewModel 层统一状态管理 |
collectAsState() |
将 Flow 转为 Compose 的 State | 响应式数据流结合 Compose |
在 Compose 中,State 是 UI 的数据源,UI 是 State 的函数。State 变 → UI 自动更新。
当 State 改变时,Compose 自动重组 UI,开发者只需声明「UI 应该长什么样」。
3、State的简化
借助Kotlin 委托语法by可以进行一步简化
kotlin
//用等号时,count的类型是MutableState<Int>,而用by后,count的类型就变成了Int
//使用by关键字替代了之前的等号,用委托的方式来为count变量赋值
val count by remember { mutableIntStateOf(0) }
var count1 = remember { mutableIntStateOf(0) }
//赋值方式
Button(onClick = { count++ }) {
Text(text = "click me")
}
Button(onClick = { count1.intValue++ }) {
Text(text = "click me")
}
五、remember到底记住的是什么?
Compose 是声明式的,每次重组(recomposition)都会重新执行 Composable 函数。为了避免每次都重算昂贵的东西,我们用 remember "记住"值。
1、 用 remember { mutableStateOf(initialValue) } 管理 UI 状态。
代码实例:
kotlin
@Composable
fun Counter() {
val count = remember { mutableIntStateOf(0) } // 记住 count,不会每次重组重置
Column {
Text("Count: ${count.intValue}")
Button(onClick = { count.intValue++ }) { Text("Increment") }
}
}
没有 remember,count 每次点击都会重置为 0。remember和mutableStateOf在Composable函数中几乎是绑定出现的。
新手在学习State时,常犯的一些概念错误
| 误区 | 解释 |
|---|---|
| ❌ 把局部变量当状态 | 普通变量不会触发重组 |
| ❌ 在重组中创建状态 | 每次重组都会新建,状态丢失 |
✅ 正确:用 remember 包裹状态 |
状态在重组中被"记住" |
2、带键的 remember:
remember 在同一Composable 实例中记住值,重组时恢复。只有当键(key)变化时才重算。
kotlin
val result = remember(key1, key2) { expensiveCalculation() }
键是用户 ID 时,用户变了就重算。 remember 防止不必要的计算,确保状态持久。别忘了,状态提升(hoist)到父组件能更好地管理。
3、新手常犯的错误:
remember 的作用域是 "Composable 调用位置"
❌ 先看错误代码:
kotlin
// 错误!每次重组都会重置
@Composable fun Simple() {
Column {
var count by remember { mutableStateOf(0) }
Text("$count")
}
}
✅ 正确代码:
kotlin
@Composable fun Simple() {
//remember 必须在 此调用位置,不能在 Column 内
var count by remember { mutableStateOf(0) }
Column {
Text("$count")
}
}
六、ViewModel集成
ViewModel 是 Android 的生命周期感知组件,适合存 UI 状态和业务逻辑。Compose 完美集成 ViewModel,用 viewModel() 获取实例。
1、基本集成
build.gradle 添加依赖:
kotlin
dependencies {
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2"
implementation "androidx.compose.runtime:runtime-livedata:1.5.1"
}
2、实际使用
ViewModel 存状态,Composable 观察它。
代码实例:ViewModel 类,这是搭配Flow进行使用
kotlin
class MyViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count = _count.asStateFlow()
fun increment() {
_count.value++
}
}
这时候有小伙伴问了,我喜欢用LiveData咋办?别急,也有对应方案。入戏:
kotlin
class MyViewModel : ViewModel() {
private val _count = MutableLiveData(0)
val count = _count
fun increment() {
_count.value = _count.value!! + 1
}
}
Composable 使用:
kotlin
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun ViewModelCounter() {
val viewModel: MyViewModel = viewModel() // 获取 ViewModel
val count1 by viewModel.count.observeAsState(0) //livedata 相关
val count = viewModel.count.collectAsState().value // 观察状态
Button(onClick = { viewModel.increment() }) {
Text("Count: $count")
}
}
ViewModel 存活于配置变化(如旋转屏幕),状态不丢。
高级:用 Hilt 或 Koin 注入 ViewModel,实现依赖注入。
这让 Compose 和 MVVM 架构无缝结合。
七、单向数据流:
Compose 推崇 单向数据流(Unidirectional Data Flow, UDF) :
vbnet
State(数据源)
↓
Composable(UI渲染)
↓
Event(用户交互)
↓
更新 State
比如:
kotlin
@Composable
fun CountScreen(viewModel: CountViewModel = viewModel()) {
val count by viewModel.count.collectAsState()
Count(
count = count,
onIncrement = { viewModel.increment() }
)
}
这样可以保证:
- 数据变化只从上往下流;
- 没有"UI → UI"之间的状态混乱;
- 方便调试、测试和状态恢复。

六、列表控件性能优化
列表用 LazyColumn 或 LazyRow,只渲染可见项,适合长列表。优化关键:用 key 标识项,避免重组;用 remember 缓存子项状态。
1、基本 LazyColumn
代码实例:
kotlin
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@Composable fun SimpleList(items: List<String>) {
LazyColumn {
items(items) {
item -> Text(item)
}
}
}
2、优化:加上key
如果列表项可变,用 key 告诉 Compose 项的身份。
javascript
//假设 item 有 id 属性。
items(items, key = { it.id }) { item -> /* ... */ }
原因:
- 没有 key → 插入/删除项时,Compose 无法识别"哪项变了" → 所有项重绘
- 有 key → 只重绘变化的项 → 性能提升 + 动画流畅
3、更多优化
- 用 contentType 分组项类型,提高回收效率。
- 避免在项里用 remember,如果需要,用 remember(item.id) {}。
- 对于复杂项,用 LazyRow 嵌套实现网格。
代码实例:带 key 的列表
kotlin
data class Item(val id: Int, val name: String)
@Composable
fun OptimizedList(items: List<Item>) {
LazyColumn {
items(
items = items,
key = { it.id },
contentType = { "text" } // 所有项同类型
) { item ->
Text(item.name)
}
}
}
这在插入/删除项时性能更好。 记住:Lazy 组件是性能王牌,别用普通 Column 做长列表。 更加真实和复杂的列表的用法,请参见我这篇文章juejin.cn/post/756423...
八、Side Effect(世界不光只有UI,还有。。。)
这个概念更抽象,篇幅有限,我尽量用最少的语言说明白,这边只简单入门讲讲。\
1、什么是副作用
先得搞明白概念,首先声明不是吃药那种副作用哈。在 Composable 函数之外,会对外部系统或全局状态产生影响的操作即为副作用。非"UI声明"本身,但又跟Compose的生命周期息息相关。例如:
- 网络请求(加载数据)。
- 数据库操作(保存或读取数据)。
- 动画或定时器启动。
- 注册监听及回调。
- 启动协程收集Flow
- 日志记录或输出。
2、为什么需要它
Compose 的核心设计是声明式和响应式的:UI 是通过可组合函数(Composables)描述的纯函数,状态变化时会多次进行重组,而副作用往往是一次性或带状态行为,且在Compose函数不能直接调用副作用代码,否则会带来如下问题:
-
每次重组都执行;
-
重复注册;
-
内存泄漏;
-
状态混乱。
3、常用Side Effect API 总览
| 函数 | 执行时机 | 典型用途 |
|---|---|---|
SideEffect |
每次成功重组后执行 | 调试、日志、同步非Compose状态 |
LaunchedEffect |
第一次进入或 key 改变时执行 | 取消旧协程,启动新协程、副作用逻辑(网络、动画) |
DisposableEffect |
进入时执行 + 离开时清理 | 注册监听器、释放资源 |
rememberCoroutineScope |
获取当前作用域启动协程 | 手动控制协程生命周期 |
produceState |
从非 Compose 数据源生成状态 | 将 Flow / 回调转为 State |
derivedStateOf |
从其他 State 派生计算值 | 避免重复计算 |
4、使用范例
SideEffect:
- 调试
- 同步 Compose 状态到外部系统(例如 ViewModel、SharedPreference)
kotlin
@Composable
fun LogExample(count: Int) {
SideEffect {
println("当前计数:$count") // 每次 count 变化并重组后执行
}
Text("Count = $count")
}
LaunchedEffect:最常用的副作用函数。
在 Composable 第一次进入 Composition 时启动协程,或当 key 变化时重新执行
key变化会重启副作用;- 离开 Composition 时会自动取消协程;
- 不会因为重组重复执行。
LaunchedEffect(true)是错误用法,每次重组都会执行
koltin
@Composable
fun TimerExample() {
var seconds by remember { mutableStateOf(0) }
// 启动协程副作用,使用Unit当key,只会执行一次,因为Key不会变化,适用于初始化操作
LaunchedEffect(Unit) {
while (true) {
delay(1000)
seconds++
}
}
Text("已过去 $seconds 秒")
}
DisposableEffect:用于需要清理的副作用(进入时执行 + 离开时清理),典型场景如下:
- 广播接收器注册;
- 注册监听;
- 观察者模式;
- 清理资源。
kotlin
@Composable
fun SensorListenerExample(sensorManager: SensorManager) {
DisposableEffect(Unit) {
val listener = SensorEventListener { /* ... */ }
sensorManager.registerListener(listener)
//离开时自动执行
onDispose {
//取消传感器的监听
sensorManager.unregisterListener(listener)
}
}
}
rememberCoroutineScope: 获取当前 Composable 作用域,手动启动协程(不会自动重启或取消)
- 注意与LaunchedEffect的区别,
LaunchedEffect自动控制生命周期
kotlin
@Composable
fun ButtonExample() {
val scope = rememberCoroutineScope()
Button(onClick = {
scope.launch {
delay(1000)
println("按钮点击1秒后执行")
}
}) {
Text("点击")
}
}
produceState:将 非 Compose 数据源(如 Flow、回调) 转换为 Compose State,让 UI 响应变化。
- 异步请求;
- Flow、LiveData 转换为 State;
- 与非 Compose 数据交互。
kotlin
@Composable
fun UserProfile(userId: String) {
val user by produceState<User?>(initialValue = null, userId) {
value = fetchUser(userId) // 在协程中执行网络请求
}
if (user == null) {
ircularProgressIndicator()
}
else {
Text("用户名:${user!!.name}")
}
}
derivedStateOf:从现有的 State 计算派生出新值,避免重复重组
- 缓存计算;
- 避免每次重组都重新计算。
kotlin
val list = remember { mutableStateOf(listOf(1, 2, 3)) }
val sum by derivedStateOf { list.value.sum() }
这个派生是不是一脸懵。别急,我将大白话介绍下。 某个状态A是由多个State计算出来的,多数情况下这个状态A不会变化。如果直接监听这些State将会导致Compose频繁重组,如果通过derivedStateOf生成派生状态A,只有这个状态A发生变化才会进行重组,这样就提高了性能。
| 概念 | 含义 |
|---|---|
| 来源 | 从一个或多个 State 推导出新值 |
| 核心功能 | 缓存计算结果,减少不必要重组 |
| 触发时机 | 依赖的 State 变化时自动更新 |
| 常见用途 | 过滤、排序、滚动状态判断、计算属性 |
| 关键点 | 一定要结合 remember 使用 |
Side Effect错误写法
| 错误写法 | 问题 |
|---|---|
在 Composable 顶层直接 launch {} |
❌ 每次重组都会重新启动 |
在 remember 外创建协程 |
❌ 无法管理生命周期 |
忘记 onDispose 清理监听 |
❌ 内存泄漏风险 |
不使用 key 导致 LaunchedEffect 永远不更新 |
❌ 副作用不响应新参数 |
一句话概括:Compose 的 Side Effect 是用来安全地执行非 UI 逻辑的机制。
它确保副作用与 Composable 的生命周期绑定,不会因为重组而重复执行或泄漏。
九、Navigation 跳转
Compose Navigation 管理屏幕跳转,默认情况下是在同一个Activity内进行跳转,像单页应用。它本质上是Compose函数的替换和重组。本文使用的是Material2,Material3变化较大,且在alpha阶段,感兴趣同学自己去尝试吧。
基本集成:
Groovy
implementation "androidx.navigation:navigation-compose:2.x.x"
设置 NavHost
用 NavController 导航。 代码实例:基本设置 在 Activity:
kotlin
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("detail/{id}") { backStackEntry ->
DetailScreen(id = backStackEntry.arguments?.getString("id"))
}
}
}
}
}
HomeScreen:
kotlin
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.navigation.NavController
@Composable
fun HomeScreen(navController: NavController) {
Button(onClick = { navController.navigate("detail/123") }) {
Text("Go to Detail")
}
}
DetailScreen:
kotlin
@Composable
fun DetailScreen(id: String?) {
Text("Detail for ID: $id")
}
支持参数、深链、动画过渡(用 AnimatedNavHost)。 Navigation 让 app 路由简单明了。 详细说明:\
- Compose Navigation 的工作原理
- NavController 管理的是 Compose UI 层面的导航
- 页面切换实际上是 Composable 函数的替换和重组
- 所有操作都在当前 ComponentActivity 的 setContent 范围内进行
- 优势
- 性能更好,避免了 Activity 启动的开销
- 状态管理更方便
- 转场动画更流畅
- 如果确实需要启动新的 Activity,需要使用传统的 Intent 方式.
kotlin
val intent = Intent(context, NewActivity::class.java)
context.startActivity(intent)
十、动画
Compose 的动画 API 简单强大,用 animate*AsState 动画值变化,或 AnimatedVisibility 控制可见性。
1、基本动画:值变化
代码示例:按钮大小动画,点击时按钮平滑变大/小。
kotlin
@Composable
fun SizeAnimation() {
val expanded = remember { mutableStateOf(false) }
Button(
onClick = { expanded.value = !expanded.value },
modifier = Modifier
.size(if (expanded.value) 200.dp else 100.dp)
.animateContentSize(animationSpec = tween(500)) // 500ms 动画
) {
Text("Toggle Size")
}
}
2、可见性动画
AnimatedVisibility:淡入淡出等。
代码示例:
scss
@Composable
fun VisibilityAnimation() {
val visible = remember { mutableStateOf(true) }
Column {
Button(onClick = { visible.value = !visible.value }) { Text("Toggle") }
AnimatedVisibility(
visible = visible.value,
enter = fadeIn(),
exit = fadeOut()
) {
Text("I appear/disappear!")
}
}
}
十一、额外知识点:
1、状态提升
在 Jetpack Compose 中,更推荐无状态 (stateless) 的 Compose 函数。让 UI 组件专注于展示逻辑,而状态管理交给上层组件处理。
kotlin
// 推荐:无状态组件
@Composable
fun Counter(
count: Int,
onIncrement: () -> Unit,
onDecrement: () -> Unit
) {
Row {
Button(onClick = onDecrement) { Text("-") }
Text("$count")
Button(onClick = onIncrement) { Text("+") }
}
}
2、State和Compose的相互转化
State → Compose
我们通常不把状态(State)放在Composable里,更多的是放在Viewmodel中。 具体相关请参照本文Viewmodel标题相关部分,此处不再阐述。 反向:Compose State → Flow / LiveData
有时候你想把 Compose 的状态暴露给其他层(例如逻辑层、存储层)
scss
val text by remember { mutableStateOf("") }
LaunchedEffect(Unit) {
snapshotFlow { text } // State → Flow
.filter { it.isNotEmpty() }
.collect { println("用户输入:$it") }
}
SnapshotFlow 会在 text 改变时发出新的流值。
十二、写在最后
Compose 不是取代XML,而是进化。学会 State + Modifier + Side Effect,你就掌握了 80% 的精髓。剩下的,边用边学吧!