写了很多年 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 { /* 点击 */ }
)

顺序很关键: paddingbackground 之前 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 来预览不同状态,无需真的去点击。


参考来源:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐