第一次用 Compose 我懵了:不写 XML,界面到底是怎么画出来的?
2026/6/11 12:38:40 网站建设 项目流程

写了很多年 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让对象在重组之间存活(缓存在组合里)每次重组都重新创建,状态丢失
rememberSaveableremember基础上,还能在配置变更/进程重建后恢复屏幕旋转后状态丢失
// 屏幕旋转也不丢的计数器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{/* 点击 */})

顺序很关键:paddingbackground之前 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 副作用

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询