写了很多年 Android 的人第一次看到 Compose往往会愣一下怎么没有 XML 了怎么界面是用 Kotlin 函数写出来的这正是 Compose 带来的根本转变——从命令式 UI找到控件一步步改它转向声明式 UI描述界面长什么样由框架负责更新。本文带你建立 Compose 的核心心智模型。一、Compose 是什么用函数描述界面Jetpack Compose 是 Android 官方的现代声明式 UI 工具包。你不再写 XML 布局、不再findViewById而是写一组带Composable注解的 Kotlin 函数每个函数描述在当前数据下界面应该长什么样。一个最简单的 ComposableComposablefunGreeting(name:String){Text(text你好$name)}Composable注解告诉编译器这个函数不是用来返回值的而是用来向界面树发射emitUI 元素的。它只能被另一个 Composable 调用。命令式 vs 声明式核心区别传统 View 体系是命令式的——你拿到控件亲手一步步修改它// 传统方式找到控件手动设置valtextViewfindViewByIdTextView(R.id.title)textView.text已登录textView.setTextColor(Color.GREEN)Compose 是声明式的——你只描述状态是这样时界面是什么样状态一变框架自动重绘ComposablefunTitle(isLoggedIn:Boolean){Text(textif(isLoggedIn)已登录else未登录,colorif(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(onClickonIncrement){Text(点击了$count次)}}// 状态由上层持有ComposablefunCounterScreen(){varcountbyremember{mutableStateOf(0)}Counter(countcount,onIncrement{count})}这个模式叫“状态下沉、事件上浮”State down, events up是 Compose 单向数据流的基础状态向下传递参数事件向上回调lambda。好处Counter现在是**无状态stateless**的同样的count永远显示同样的界面既方便预览、测试也能在不同地方复用。五、常用组件与布局5.1 基础组件组件作用对应 ViewText显示文字TextViewButton/TextButton/IconButton按钮ButtonImage显示图片ImageViewTextField/OutlinedTextField输入框EditTextIcon图标——5.2 三大布局布局排列方式对应 ViewColumn垂直排列垂直 LinearLayoutRow水平排列水平 LinearLayoutBox层叠一个盖一个FrameLayoutComposablefunProfileCard(name:String){Row(verticalAlignmentAlignment.CenterVertically,modifierModifier.padding(16.dp)){Icon(Icons.Default.Person,contentDescriptionnull)Spacer(Modifier.width(8.dp))Column{Text(name,styleMaterialTheme.typography.titleMedium)Text(在线,colorColor.Green)}}}5.3 列表LazyColumn / LazyRow对应 RecyclerView但不需要写 Adapter——只按需创建可见的项高效得多ComposablefunNameList(names:ListString){LazyColumn{items(names){name-Text(textname,modifierModifier.padding(8.dp))}}}告别 RecyclerView 的样板代码不再有 Adapter、ViewHolder、notifyDataSetChanged。数据变了列表自动更新。六、Modifier装饰与布局的链式语法Modifier用来设置组件的尺寸、内边距、背景、点击、边框等。它是链式调用且顺序会影响结果。Text(Hello,modifierModifier.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 里嵌入 ViewAndroidView { ... }如嵌入地图、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(showBackgroundtrue)ComposablefunCounterScreenPreview(){MaterialTheme{Counter(count5,onIncrement{})}}这也是状态提升的回报之一因为Counter是无状态的你能轻松传入任意count来预览不同状态无需真的去点击。参考来源Android 官方文档 - Jetpack ComposeCompose 思想Thinking in ComposeCompose 中的状态Compose 副作用
第一次用 Compose 我懵了:不写 XML,界面到底是怎么画出来的?
发布时间:2026/6/11 12:38:41
写了很多年 Android 的人第一次看到 Compose往往会愣一下怎么没有 XML 了怎么界面是用 Kotlin 函数写出来的这正是 Compose 带来的根本转变——从命令式 UI找到控件一步步改它转向声明式 UI描述界面长什么样由框架负责更新。本文带你建立 Compose 的核心心智模型。一、Compose 是什么用函数描述界面Jetpack Compose 是 Android 官方的现代声明式 UI 工具包。你不再写 XML 布局、不再findViewById而是写一组带Composable注解的 Kotlin 函数每个函数描述在当前数据下界面应该长什么样。一个最简单的 ComposableComposablefunGreeting(name:String){Text(text你好$name)}Composable注解告诉编译器这个函数不是用来返回值的而是用来向界面树发射emitUI 元素的。它只能被另一个 Composable 调用。命令式 vs 声明式核心区别传统 View 体系是命令式的——你拿到控件亲手一步步修改它// 传统方式找到控件手动设置valtextViewfindViewByIdTextView(R.id.title)textView.text已登录textView.setTextColor(Color.GREEN)Compose 是声明式的——你只描述状态是这样时界面是什么样状态一变框架自动重绘ComposablefunTitle(isLoggedIn:Boolean){Text(textif(isLoggedIn)已登录else未登录,colorif(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(onClickonIncrement){Text(点击了$count次)}}// 状态由上层持有ComposablefunCounterScreen(){varcountbyremember{mutableStateOf(0)}Counter(countcount,onIncrement{count})}这个模式叫“状态下沉、事件上浮”State down, events up是 Compose 单向数据流的基础状态向下传递参数事件向上回调lambda。好处Counter现在是**无状态stateless**的同样的count永远显示同样的界面既方便预览、测试也能在不同地方复用。五、常用组件与布局5.1 基础组件组件作用对应 ViewText显示文字TextViewButton/TextButton/IconButton按钮ButtonImage显示图片ImageViewTextField/OutlinedTextField输入框EditTextIcon图标——5.2 三大布局布局排列方式对应 ViewColumn垂直排列垂直 LinearLayoutRow水平排列水平 LinearLayoutBox层叠一个盖一个FrameLayoutComposablefunProfileCard(name:String){Row(verticalAlignmentAlignment.CenterVertically,modifierModifier.padding(16.dp)){Icon(Icons.Default.Person,contentDescriptionnull)Spacer(Modifier.width(8.dp))Column{Text(name,styleMaterialTheme.typography.titleMedium)Text(在线,colorColor.Green)}}}5.3 列表LazyColumn / LazyRow对应 RecyclerView但不需要写 Adapter——只按需创建可见的项高效得多ComposablefunNameList(names:ListString){LazyColumn{items(names){name-Text(textname,modifierModifier.padding(8.dp))}}}告别 RecyclerView 的样板代码不再有 Adapter、ViewHolder、notifyDataSetChanged。数据变了列表自动更新。六、Modifier装饰与布局的链式语法Modifier用来设置组件的尺寸、内边距、背景、点击、边框等。它是链式调用且顺序会影响结果。Text(Hello,modifierModifier.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 里嵌入 ViewAndroidView { ... }如嵌入地图、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(showBackgroundtrue)ComposablefunCounterScreenPreview(){MaterialTheme{Counter(count5,onIncrement{})}}这也是状态提升的回报之一因为Counter是无状态的你能轻松传入任意count来预览不同状态无需真的去点击。参考来源Android 官方文档 - Jetpack ComposeCompose 思想Thinking in ComposeCompose 中的状态Compose 副作用