第一次用 Compose 我懵了:不写 XML,界面到底是怎么画出来的?
写了很多年 Android 的人第一次看到 Compose,往往会愣一下:怎么没有 XML 了?怎么界面是用 Kotlin 函数"写"出来的?这正是 Compose 带来的根本转变——从命令式 UI(找到控件,一步步改它)转向声明式 UI(描述"界面长什么样",由框架负责更新)。本文带你建立 Compose 的核心心智模型。
一、Compose 是什么:用函数描述界面
Jetpack Compose 是 Android 官方的现代声明式 UI 工具包。 你不再写 XML 布局、不再 findViewById,而是写一组带 @Composable 注解的 Kotlin 函数,每个函数描述"在当前数据下,界面应该长什么样"。
一个最简单的 Composable:
@Composable
fun Greeting(name: String) {
Text(text = "你好,$name")
}
@Composable 注解告诉编译器:这个函数不是用来"返回值"的,而是用来向界面树发射(emit)UI 元素的。它只能被另一个 Composable 调用。
命令式 vs 声明式:核心区别
传统 View 体系是命令式的——你拿到控件,亲手一步步修改它:
// 传统方式:找到控件,手动设置
val textView = findViewById<TextView>(R.id.title)
textView.text = "已登录"
textView.setTextColor(Color.GREEN)
Compose 是声明式的——你只描述"状态是这样时界面是什么样",状态一变,框架自动重绘:
@Composable
fun Title(isLoggedIn: Boolean) {
Text(
text = if (isLoggedIn) "已登录" else "未登录",
color = if (isLoggedIn) Color.Green else Color.Gray
)
}
心智转变: 在 Compose 里,你不去"修改"界面,而是改变数据,界面会自己跟着变。这就是声明式的精髓。
二、三个核心概念:Composable、State、重组
理解 Compose,绕不开这三件套。
2.1 Composable 函数
界面的基本"积木"。约定俗成:
- 用
@Composable注解; - 函数名首字母大写(像类名一样,因为它代表一个 UI 组件);
- 不返回 UI 对象,而是描述界面;
- 应当是幂等、无副作用的——同样的输入产生同样的界面,不要在里面直接做网络请求、写数据库这类副作用操作(那些有专门的机制,见第六节)。
2.2 State(状态):界面的"数据源"
界面显示什么,取决于状态。Compose 用 State 把数据和界面"挂钩":当被观察的 State 变化时,读取了它的 Composable 会自动重新执行。
@Composable
fun Counter() {
// remember 让状态在重组之间存活;mutableStateOf 创建可观察状态
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("点击了 $count 次")
}
}
这里 count 一变,Button(读取了 count 的部分)就会自动刷新。这就是声明式的威力:你没有手动去 setText,是状态驱动了界面。
2.3 重组(Recomposition)
当状态变化时,Compose 重新调用相关的 Composable 函数来更新界面,这个过程叫"重组"。
关键特性:
- 智能跳过: Compose 只重组真正读取了变化状态的部分,没受影响的不会重跑(这是性能基础);
- 可能频繁、可能乱序、可能并行: 所以 Composable 必须无副作用,不能依赖执行次数或顺序;
- 不保证执行次数: 别把"只想执行一次"的逻辑直接写在 Composable 体里。
常见误区: 在 Composable 函数体里直接
var count = 0是无效的——每次重组都会重新初始化为 0。必须用remember把它"记住",才能跨重组保留。
三、state 与 remember 的两个关键词
| 关键词 | 作用 | 没有它会怎样 |
|---|---|---|
mutableStateOf |
创建可被 Compose 观察的状态,值变了会触发重组 | 用普通变量,值变了界面不刷新 |
remember |
让对象在重组之间存活(缓存在组合里) | 每次重组都重新创建,状态丢失 |
rememberSaveable |
在 remember 基础上,还能在配置变更/进程重建后恢复 |
屏幕旋转后状态丢失 |
// 屏幕旋转也不丢的计数器
var count by rememberSaveable { mutableStateOf(0) }
记忆口诀:
mutableStateOf负责"值变了能通知界面",remember负责"重组时别把它弄丢",rememberSaveable负责"连旋转、被杀重建都不丢"。
四、状态提升:让组件可复用、可测试
一个 Composable 如果自己持有状态(像上面的 Counter),它就不好复用、不好测试,因为状态藏在内部。Compose 推崇 状态提升(State Hoisting):把状态移到调用者那里,组件只接收"当前值"和"变化回调"。
// 无状态组件:只负责显示和上报事件,自己不持有状态
@Composable
fun Counter(count: Int, onIncrement: () -> Unit) {
Button(onClick = onIncrement) {
Text("点击了 $count 次")
}
}
// 状态由上层持有
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Counter(count = count, onIncrement = { count++ })
}
这个模式叫 “状态下沉、事件上浮”(State down, events up),是 Compose 单向数据流的基础:
- 状态向下传递(参数);
- 事件向上回调(lambda)。
好处:
Counter现在是**无状态(stateless)**的,同样的count永远显示同样的界面,既方便预览、测试,也能在不同地方复用。
五、常用组件与布局
5.1 基础组件
| 组件 | 作用 | 对应 View |
|---|---|---|
Text |
显示文字 | TextView |
Button / TextButton / IconButton |
按钮 | Button |
Image |
显示图片 | ImageView |
TextField / OutlinedTextField |
输入框 | EditText |
Icon |
图标 | —— |
5.2 三大布局
| 布局 | 排列方式 | 对应 View |
|---|---|---|
Column |
垂直排列 | 垂直 LinearLayout |
Row |
水平排列 | 水平 LinearLayout |
Box |
层叠(一个盖一个) | FrameLayout |
@Composable
fun ProfileCard(name: String) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(16.dp)
) {
Icon(Icons.Default.Person, contentDescription = null)
Spacer(Modifier.width(8.dp))
Column {
Text(name, style = MaterialTheme.typography.titleMedium)
Text("在线", color = Color.Green)
}
}
}
5.3 列表:LazyColumn / LazyRow
对应 RecyclerView,但不需要写 Adapter——只按需创建可见的项,高效得多:
@Composable
fun NameList(names: List<String>) {
LazyColumn {
items(names) { name ->
Text(text = name, modifier = Modifier.padding(8.dp))
}
}
}
告别 RecyclerView 的样板代码: 不再有 Adapter、ViewHolder、
notifyDataSetChanged。数据变了,列表自动更新。
六、Modifier:装饰与布局的"链式语法"
Modifier 用来设置组件的尺寸、内边距、背景、点击、边框等。 它是链式调用,且顺序会影响结果。
Text(
"Hello",
modifier = Modifier
.padding(16.dp) // 先留白
.background(Color.Yellow) // 再上背景(背景不含外侧 padding 区域)
.clickable { /* 点击 */ }
)
顺序很关键:
padding在background之前 vs 之后,视觉效果完全不同(决定背景色是否覆盖内边距)。这是初学者最容易困惑的点——记住 Modifier 是从上到下依次应用的。
七、副作用:在 Compose 里"做事情"
Composable 应当无副作用,那网络请求、显示一次性提示这类"动作"放哪?Compose 提供了一组 副作用 API,让这些操作和组合的生命周期对齐:
| API | 用途 |
|---|---|
LaunchedEffect(key) |
进入组合时启动一个协程(key 变化会重启),适合加载数据、订阅 |
rememberCoroutineScope() |
拿到一个跟随组合生命周期的协程作用域,在事件回调里启动协程 |
DisposableEffect(key) |
需要"注册 + 注销"成对操作时(如注册监听器,离开时注销) |
derivedStateOf |
由其他状态派生出新状态,避免不必要的重组 |
@Composable
fun UserScreen(userId: Int, viewModel: UserViewModel) {
// 进入界面或 userId 变化时加载一次
LaunchedEffect(userId) {
viewModel.loadUser(userId)
}
// ...
}
为什么需要它们? 因为重组可能频繁发生,你不能把"加载数据"直接写在函数体里(会反复触发)。副作用 API 保证这些操作在正确的时机、正确的次数执行。
八、与传统 View 互操作
Compose 不是"全有或全无",可以和现有 View 项目共存,便于渐进式迁移:
- 在 Compose 里嵌入 View:
AndroidView { ... }(如嵌入地图、WebView); - 在 XML 里嵌入 Compose:放一个
ComposeView,在代码里setContent { ... }。
// 在 Activity 中整屏使用 Compose
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
CounterScreen()
}
}
}
}
注意整屏 Compose 的 Activity 通常继承
ComponentActivity(或AppCompatActivity),并用setContent { }替代setContentView。
九、@Preview:不开模拟器也能看效果
Compose 的一大生产力优势:用 @Preview 直接在 Android Studio 里预览界面,无需运行 App。
@Preview(showBackground = true)
@Composable
fun CounterScreenPreview() {
MaterialTheme {
Counter(count = 5, onIncrement = {})
}
}
这也是状态提升的回报之一: 因为
Counter是无状态的,你能轻松传入任意count来预览不同状态,无需真的去点击。
参考来源:
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)