写了很多年 Android 的人第一次看到 Compose,往往会愣一下:怎么没有 XML 了?怎么界面是用 Kotlin 函数"写"出来的?这正是 Compose 带来的根本转变——从命令式 UI(找到控件,一步步改它)转向声明式 UI(描述"界面长什么样",由框架负责更新)。本文带你建立 Compose 的核心心智模型。
一、Compose 是什么:用函数描述界面
Jetpack Compose 是 Android 官方的现代声明式 UI 工具包。你不再写 XML 布局、不再findViewById,而是写一组带@Composable注解的 Kotlin 函数,每个函数描述"在当前数据下,界面应该长什么样"。
一个最简单的 Composable:
@ComposablefunGreeting(name:String){Text(text="你好,$name")}@Composable注解告诉编译器:这个函数不是用来"返回值"的,而是用来向界面树发射(emit)UI 元素的。它只能被另一个 Composable 调用。
命令式 vs 声明式:核心区别
传统 View 体系是命令式的——你拿到控件,亲手一步步修改它:
// 传统方式:找到控件,手动设置valtextView=findViewById<TextView>(R.id.title)textView.text="已登录"textView.setTextColor(Color.GREEN)Compose 是声明式的——你只描述"状态是这样时界面是什么样",状态一变,框架自动重绘:
@ComposablefunTitle(isLoggedIn:Boolean){Text(text=if(isLoggedIn)"已登录"else"未登录",color=if(isLoggedIn)Color.GreenelseColor.Gray)}心智转变:在 Compose 里,你不去"修改"界面,而是改变数据,界面会自己跟着变。这就是声明式的精髓。
二、三个核心概念:Composable、State、重组
理解 Compose,绕不开这三件套。
2.1 Composable 函数
界面的基本"积木"。约定俗成:
- 用
@Composable注解; - 函数名首字母大写(像类名一样,因为它代表一个 UI 组件);
- 不返回 UI 对象,而是描述界面;
- 应当是幂等、无副作用的——同样的输入产生同样的界面,不要在里面直接做网络请求、写数据库这类副作用操作(那些有专门的机制,见第六节)。
2.2 State(状态):界面的"数据源"
界面显示什么,取决于状态。Compose 用State把数据和界面"挂钩":当被观察的 State 变化时,读取了它的 Composable 会自动重新执行。
@ComposablefunCounter(){// remember 让状态在重组之间存活;mutableStateOf 创建可观察状态varcountbyremember{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基础上,还能在配置变更/进程重建后恢复 | 屏幕旋转后状态丢失 |
// 屏幕旋转也不丢的计数器varcountbyrememberSaveable{mutableStateOf(0)}记忆口诀:
mutableStateOf负责"值变了能通知界面",remember负责"重组时别把它弄丢",rememberSaveable负责"连旋转、被杀重建都不丢"。
四、状态提升:让组件可复用、可测试
一个 Composable 如果自己持有状态(像上面的Counter),它就不好复用、不好测试,因为状态藏在内部。Compose 推崇状态提升(State Hoisting):把状态移到调用者那里,组件只接收"当前值"和"变化回调"。
// 无状态组件:只负责显示和上报事件,自己不持有状态@ComposablefunCounter(count:Int,onIncrement:()->Unit){Button(onClick=onIncrement){Text("点击了$count次")}}// 状态由上层持有@ComposablefunCounterScreen(){varcountbyremember{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 |
@ComposablefunProfileCard(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——只按需创建可见的项,高效得多:
@ComposablefunNameList(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 | 由其他状态派生出新状态,避免不必要的重组 |
@ComposablefunUserScreen(userId:Int,viewModel:UserViewModel){// 进入界面或 userId 变化时加载一次LaunchedEffect(userId){viewModel.loadUser(userId)}// ...}为什么需要它们?因为重组可能频繁发生,你不能把"加载数据"直接写在函数体里(会反复触发)。副作用 API 保证这些操作在正确的时机、正确的次数执行。
八、与传统 View 互操作
Compose 不是"全有或全无",可以和现有 View 项目共存,便于渐进式迁移:
- 在 Compose 里嵌入 View:
AndroidView { ... }(如嵌入地图、WebView); - 在 XML 里嵌入 Compose:放一个
ComposeView,在代码里setContent { ... }。
// 在 Activity 中整屏使用 ComposeclassMainActivity:ComponentActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContent{MaterialTheme{CounterScreen()}}}}注意整屏 Compose 的 Activity 通常继承
ComponentActivity(或AppCompatActivity),并用setContent { }替代setContentView。
九、@Preview:不开模拟器也能看效果
Compose 的一大生产力优势:用@Preview直接在 Android Studio 里预览界面,无需运行 App。
@Preview(showBackground=true)@ComposablefunCounterScreenPreview(){MaterialTheme{Counter(count=5,onIncrement={})}}这也是状态提升的回报之一:因为
Counter是无状态的,你能轻松传入任意count来预览不同状态,无需真的去点击。
参考来源:
- Android 官方文档 - Jetpack Compose
- Compose 思想(Thinking in Compose)
- Compose 中的状态
- Compose 副作用