1 简介
1.1 背景
- 2019 年 5 月(首次亮相)在 Google I/O 大会上,Google 正式宣布 Jetpack Compose 项目,定位为 "用于构建原生 Android UI 的现代工具包",核心是采用声明式语法,基于 Kotlin 语言设计。此时处于早期预览阶段,API 不稳定。
- 2020 - 2021 年(迭代完善)
a、发布多个 alpha/beta 版本,逐步完善核心功能(布局系统、状态管理、动画、主题等)。
b、重点解决性能问题(如重组优化、渲染效率),并适配 Android 12 等新系统特性。
c、推出 Compose 与传统 View 系统的互操作方案(如 AndroidView
、ComposeView
),降低迁移成本。
- 2021 年 7 月(稳定版发布):Jetpack Compose 1.0 正式发布,标志着其进入生产可用阶段。Google 强调 "Compose 是 Android UI 开发的未来",并开始在官方文档和示例中主推 Compose。
- 2022 年至今(生态扩张)
a、持续迭代版本(截至 2023 年已到 1.4+),完善功能(如更好的列表性能、手势处理、 accessibility 支持)。
b、推动第三方库适配(如 Retrofit、Room、Coil 等主流库均支持 Compose)。
c、推出 Compose Multiplatform(跨平台扩展),允许用 Compose 语法开发 Android、iOS、桌面应用,进一步扩大应用范围。
Google团队从2019首次亮相到2021首个稳定版本发布,花费两年时间打造开发。所以Compose不是为提升开发效率高级控件,例如2014推出的RecycleView高级控件,旨在替代传统的 ListView 和 GridView。那Compose是什么呢?
1.2 Compose 是什么?
Jetpack Compose 是用于构建原生界面的最新的 Android 工具包,采用声明式 UI 的设计,拥有更简单的自定义和实时的交互预览功能,由 Android 官方团队全新打造的 UI 框架。
关键词:声明式UI开发框架(核心:不需要手动更新)
1.3 Composable 是什么?
Composeable(可组合函数),专门用来描述UI样子的特殊函数。
主要特点:
- @Composable注解,每个可组合函数必须用Composable标记
- 首字母大写,每个可组合函数首字母大写用于区分普通函数(虽然没强制限制,但一般默认强制)
- 调用限制,可组合函数只能由其它可组合调用,不能由普通函数直接调用
- 无返回值,可组合函数作用是描述UI样子,无返回值。
ini
@Composable
fun Greeting(modifier: Modifier = Modifier) {
Column {
MyText(
string = "Hello world!",
modifier = modifier,
isShow = isShowName
)
MyText(
string = "name is Chery ",
modifier = modifier,
isShow = isShowAge
)
}
}
1.4 什么是声明式UI? 什么又是命令式UI?
前面我们一直在说Compose使用声明式UI代替传统的命令式UI,那到底什么是声明式UI? 什么又是命令式UI呢?
本质区别:命令式Ui关注的是"过程",声明式UI关注是"结果"。
- 命令式UI:
需要开发者一步一步编写逻辑代码告诉系统,怎么构建和修改UI。
如下:在xml中创建TextView,然后需要在代码中主动修改TextView内容。所以命令式UI需要我们手动"命令"系统怎么去做。

- 声明式UI:
开发者只需要描述"在某种状态下UI应该是什么样子",至于"如何从当前状态过渡到目标状态"则由框架自动完成。
如下:我们声明UI在isShow = true时添加Text,当我们需要隐藏Text时只需要修改isShow = false即可,不需要传统式主动调用visibility 去"命令"Text隐藏。所以声明式UI不需要我们手动"命令"系统怎么去做。

遗留技术点:Compose框架自动从当前状态过渡到目标状态技术及原理
1.5 为什么要使用Compose?
为什么采用 Compose | Jetpack Compose | Android Developers
官方给出精简代码、直观易读 、加速开发、功能强大 等优点,我在使用Compose后几点感悟:
- Google官方主推Compose,意味着未来必然成为主流,同时官方也必然继续打造开发更加方便、更加优秀的控件或技术。
- 代码确实更加精简
相对传统的命令式UI,代码量是显著减少的。

- 功能强大 --动画方面
在动画实现方面,让我爱上了Compose。
示例:实现ImageView点击后缩放动效
传统UI:
typescript
private void startScaleAnimation() {
isAnimating = true;
// 创建缩放动画:从1倍缩放到0.8倍
ScaleAnimation scaleDown = new ScaleAnimation(
1f, 0.8f, // X轴缩放从1到0.8
1f, 0.8f, // Y轴缩放从1到0.8
Animation.RELATIVE_TO_SELF, 0.5f, // X轴缩放中心为自身50%处
Animation.RELATIVE_TO_SELF, 0.5f // Y轴缩放中心为自身50%处
);
scaleDown.setDuration(200); // 动画持续时间
scaleDown.setFillAfter(true); // 保持动画结束状态
// 创建缩放动画:从0.8倍缩放到1倍
ScaleAnimation scaleUp = new ScaleAnimation(
0.8f, 1f, // X轴缩放从0.8到1
0.8f, 1f, // Y轴缩放从0.8到1
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f
);
scaleUp.setDuration(200);
scaleUp.setFillAfter(true);
// 设置动画监听,用于在第一个动画结束后启动第二个动画
scaleDown.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
// 第一个动画结束后开始第二个动画
scaleImage.startAnimation(scaleUp);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
// 第二个动画结束后重置状态
scaleUp.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
isAnimating = false;
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
// 开始第一个动画
scaleImage.startAnimation(scaleDown);
}
Compose :
ini
@Composable
fun ScaleAnimationDemo() {
// 记录当前缩放值,初始为1f
val scale = remember { Animatable(1f) }
// 标记是否正在执行动画
var isAnimating by remember { mutableStateOf(false) }
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.mipmap.ic_launcher),
contentDescription = "可缩放的图片",
modifier = Modifier
.size(200.dp)
.scale(scale.value) // 应用缩放
.clickable {
// 只有不在动画中时才响应点击
if (!isAnimating) {
isAnimating = true
}
}
)
}
// 当isAnimating变为true时启动动画
LaunchedEffect(isAnimating) {
if (isAnimating) {
// 从1f缩放到0.8f
scale.animateTo(
targetValue = 0.8f,
animationSpec = tween(durationMillis = 200)
)
// 从0.8f缩放回1f
scale.animateTo(
targetValue = 1f,
animationSpec = tween(durationMillis = 200)
)
// 动画结束,重置状态
isAnimating = false
}
}
传统方式实现整个startScaleAnimation()方法都是动画实现逻辑,而Compose实现在25行之前的部分是UI,真正实现动效只需28~41行短短13行代码,相比之下在实现简单和代码量方面都有显著的优势。
遗留技术点:Compose 常见动效实现
2 编程思想
compose编程思想与传统的思想差异其实还是蛮大的,开始多少会存在不适应。个人感受是
这是什么玩意? -> 这个有那么好用? -> 还是不错的!->真香!!
2.1 声明式UI
声明式编程是compose基石,其核心在于"描述UI应该是什么样子,而不是描述如何构建它,同样需要更新时也不要我们手动去命令它"。只需要关注"结果",不需要关注"过程"。
这里我之前也一直不理解或者说一直转不过弯,举个例子:如下
简单的Text,现在想修改显示,只需要修改name,但肯定的是需要点击操作或者登录成功等一系列复杂逻辑后才会去修改name,这些逻辑不属于"过程"吗?
是的,不属于。因为这里说的"结果"和"过程"只针对UI,再直白点就是在使用compose 编写UI我完全没有写更新代码,而传统命令式UI则需要setText()更新。
这里有人会想到DataBinding,虽然DataBinding可以实现数据发生变化时UI同步更新,用过DataBinding的都知道我们在set的时候会调用notifiyChanged()方法发出通知,然后DataBinding框架内部会根据code更新指定控件。
Compose怎么知道更新对应Text?--"状态追踪机制",后面也会讲到)
kotlin
@Composable
fun Greeting() {
var name by remember { mutableStateOf("world") }
Text(
text = "Hello $name!",
modifier = modifier
)
}
2.2 函数式组合
Compose采用函数式编程思想,将UI分解为独立的、可组合函数,并通过这些组合函数构建复杂的UI,而非传统的继承方式。
- 高内聚低耦合,每个可组合函数专注于单一功能。
- 高度可复用,可组合函数复用性极高。
- 灵活组合,通过不同的组合函数构建复杂的UI。
kotlin
@Composable
fun Greeting(modifier: Modifier = Modifier) {
Log.d("chery","Greeting----start---")
Column {
NameComponent(modifier)
AgeComponent(modifier)
}
Log.d("chery","Greeting----end---")
}
@Composable
private fun NameComponent(modifier: Modifier = Modifier) {
var isShowName by remember { mutableStateOf(true) }
MyText(
string = "Hello world!",
modifier = modifier,
isShow = isShowName
)
LaunchedEffect(Unit) {
delay(2000)
isShowName = false
}
}
@Composable
private fun AgeComponent(modifier: Modifier = Modifier) {
var isShowAge by remember { mutableStateOf(true) }
MyText(
string = "age :18!",
modifier = modifier,
isShow = isShowAge
)
LaunchedEffect(Unit) {
delay(1000)
isShowAge = false
}
}
//MyText 可组合函数
@Composable
fun MyText(string: String, isShow: Boolean, modifier: Modifier = Modifier) {
Log.d("chery","MyText----start---string:$string,isShow:$isShow")
if (isShow){
Text(
text = string,
modifier = modifier
)
}
Log.d("chery","MyText----end---")
}
2.3 状态驱动UI
compose采用"状态驱动UI"的概念,UI的任何变化都应该由状态变化引发,当状态发生变化时compose自动更新,即状态是驱动一切的核心。
示例:
a、isShowAge状态变化后会触发依赖的该状态的Greeting()和 Age ->MyText()函数重组,且不会导致Name ->MyText()函数重组。
b、remeber保留了isShowAge 变化后的状态,Greeting()函数重组时isShowAge = false。
c、那怎么继续优化呢?--思路:减少不必要的重组(2.7 章节重组优化)
kotlin
@Composable
fun Greeting(modifier: Modifier = Modifier) {
Log.d("chery","Greeting----start---")
//状态
var isShowName by remember { mutableStateOf(true) }
var isShowAge by remember { mutableStateOf(true) }
Column {
MyText(
string = "Hello world!",
modifier = modifier,
isShow = isShowName
)
MyText(
string = "age :18!",
modifier = modifier,
isShow = isShowAge
)
}
//协程 LaunchedEffect( Unit ){ delay( 2000 ) isShowAge= false }
Log.d("chery","Greeting----end---")
}
//MyText 可组合函数
@Composable
fun MyText(string: String, isShow: Boolean, modifier: Modifier = Modifier) {
Log.d("chery","MyText----start---string:$string,isShow:$isShow")
if (isShow){
Text(
text = string,
modifier = modifier
)
}
Log.d("chery","MyText----end---")
}
//日志
com.example.test D Greeting----start---
com.example.test D Greeting----isShowName:true---isShowAge:true--
com.example.test D MyText----start---string:Hello com.example.test world!,isShow:true
com.example.test D MyText----end---
com.example.test D MyText----start---string:age :18!,isShow:true
com.example.test D MyText----end---
com.example.test D Greeting----end---
// 2s后
com.example.test D Greeting----start---
com.example.test D Greeting----isShowName:true---isShowAge:false--
com.example.test D MyText----start---string:age :18!,isShow:false
com.example.test D MyText----end---
com.example.test D Greeting----end---
- 可观察状态,使用mutableStateOf创建的状态对象能够被compose观察。
- 自动重组,当状态改变时,依赖该状态的所有可组合函数会被重新执行(重组)。
- 单向数据流,用户交互导致状态变化,状态变化又驱动UI更新,形成完美闭环。
- 状态缓存,使用remeber保留状态,确保重组时状态不丢失。
2.4 状态基础
什么是状态? ->状态怎么驱动UI? -> 什么是状态容器? -> 状态提升 -> 怎么保存界面状态?
2.4.1 什么是状态?
前面一直说compose中"状态驱动UI",UI的任何变化都应该由状态变化引发,那什么是状态呢?
-
定义:状态就是指"影响/更新UI的数据"
-
示例澄清:
- "var count = 0"方式创建,count 可以叫做非可观察状态,更严谨的说count就是一个普通变量,不能称之为状态。
- "var count = mutableStateOf(0)"和"var count by remember { mutableStateOf(0) }"两种方式创建的count都可以叫做状态(可观察状态),但两者区别很大。(2.7.1章节重组优化会讲到)
kotlin
@Composable
fun CounterExample() {
// 不会导致重组,普通变量非可观察状态,compose感知不到其变化。
//var count = 0
// 定义一个状态变量
// var count = mutableStateOf(0)
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier.padding(16.dp)
) {
// 这个 Text 依赖 count 状态
Text(
text = "点击了 $count 次",
fontSize = 20.sp
)
// 点击按钮改变状态
Button(onClick = { count++ }) {
Text("点击我")
}
}
}
2.4.3 什么是状态容器?
-
定义:状态容器是指专门封装状态和状态操作逻辑的类。
当状态逻辑复杂时(例如:依赖多个状态又可能存在其他逻辑时)或者多个不相关的组件依赖相同状态时,此时把相关逻辑代码都写在可组合函数中,不仅代码混乱,还可能产生大量的冗余代码。此时就需要状态容器进行统一管理。所以状态容器的作用是:
- 分离UI和状态逻辑,让代码更清晰。
- 方便复用状态,多个组件共用一个容器
-
示例:E02/t26 SystemUI dock栏
状态容器:SystemUI 使用Viewmode
kotlin
class DockTempeViewModel : BaseViewModel<DockTempeIntent>(), ITempeSet {
companion object {
private const val TAG = "DockTempeViewModel"
val ON = SceneStatus.ON
val OFF = SceneStatus.OFF
val DISABLE = SceneStatus.DISABLE
}
/**
* 主驾温度
*/
var leftTempe by mutableIntStateOf(0)
/**
* 副驾温度
*/
var rightTempe by mutableIntStateOf(0)
}
}
主驾、副驾 dock栏 可组合函数:
kotlin
/**
* dock 左侧温度、座椅 区域
* */
@Composable
fun DockLeftTempeChairArea(modifier: Modifier) {
//获取 温度状态容器
val tempeViewModel = ScopeViewModelProvider.getScopeViewModel(
DockTempeViewModel::class.java
)
// 显示主驾温度
DockTempeTextView(modifier = Modifier
// 依赖温度状态容器中主驾温度状态
number = tempeViewModel.leftTempe,
color = if (ignStatusOn && hvacPowerOn && !mainViewModel.mainAllAppEditStatus) primaryColor else secondColor,
isShowOff = isShowOff
)
}
}
/**
* dock 副驾温度、座椅 区域
* */
private const val TAG = "DockPassengerTempeChairArea"
@Composable
fun DockPassengerTempeChairArea(modifier: Modifier) {
Box(
modifier
.fillMaxHeight()
.wrapContentWidth()
) {
//获取 温度状态容器
val tempeViewModel = ScopeViewModelProvider.getScopeViewModel(
DockTempeViewModel::class.java
)
// 显示副驾温度
DockTempeTextView(
modifier = Modifier
// 依赖温度状态容器中副驾温度状态
number = tempeViewModel.rightTempe,
color = if (ignStatusOn && hvacPowerOn && !passengerViewModel.passengerAllAppEditStatus) primaryColor else secondColor,
isShowOff = isShowOff
)
}
}
}
2.4.4 状态提升
- 定义:状态提升是指将状态从子组件"移动"父组件的过程,使状态可以被多个子组件共享和控制。
- 示例澄清:如2.3章节 状态驱动UI优化后的方案,如果NameComponent()或者AgeComponent()函数同时受isShowName和isShowAge状态控制,那么就需要对这两个状态提升。这个没什么需要强调,跟我们平时调用普通函数传参是一样的。
特性 | remember | rememberSaveable |
---|---|---|
重组时是否保留 | 是(核心功能) | 是(继承 remember 的能力) |
重建时是否保留 | 否(状态随组件实例销毁) | 是(通过 Bundle 持久化) |
适用数据类型 | 任意类型 | 基本类型、可序列化类型(或需自定义保存逻辑) |
性能开销 | 低(内存级保存) | 略高(涉及 Bundle 读写) |
典型使用场景 | 临时状态(如列表展开 / 折叠) | 需持久化的用户输入(如表单、设置) |
2.4.5 怎么保存界面状态?
2.4.5.1 为什么要保存界面状态?
目的:在可组合函数重组或界面重建(屏幕旋转、切换语言)时,用户数据和界面状态不丢失。
2.4.5.2 保存界面状态方式
如SystemUI直接使用状态容器(ViewModel或者其它外部方式)状态,可以实现重组或界面重建后数据和界面状态不丢失。这里我们主要说在可组合函数内部怎么保存界面状态,一般两种方式:
特性 | remember | rememberSaveable |
---|---|---|
重组时是否保留 | 是(核心功能) | 是(继承 remember 的能力) |
重建时是否保留 | 否(状态随组件实例销毁) | 是(通过 Bundle 持久化) |
适用数据类型 | 任意类型 | 基本类型、可序列化类型(或需自定义保存逻辑) |
性能开销 | 低(内存级保存) | 略高(涉及 Bundle 读写) |
典型使用场景 | 临时状态(如列表展开 / 折叠) | 需持久化的用户输入(如表单、设置) |
2.5 重组基础
定义 -> 触发 -> 执行
2.6.1 什么是重组
上面一直说到"重组",那什么是重组呢?- 定义:重组是 Compose 重新执行 "依赖变化状态的可组合函数",并更新对应 UI 的过程 ------ 本质是 "状态驱动 UI 的执行环节"。
-
关键澄清:
很多人会把 "重组" 和 "刷新 UI" 画等号,但两者是 "过程与结果" 的关系:
- 重组(过程):可组合函数重新执行,计算出 "当前状态对应的 UI 结构"。
- 刷新 UI(结果):Compose 根据重组结果,只更新 "真正变化的 UI 节点"(而非全量重绘)。
-
类比理解:如同 "做饭(重组)" 和 "吃到饭(刷新 UI)"------ 做饭是按菜谱(状态)重新备菜、烹饪的过程,吃到饭是最终结果;若食材(状态)没变化,就不需要重新做饭(跳过重组)。
这里为什么要强调两者的差异呢?因为理解了这两者的差异有助于后续2.7章节"重组优化"或者我们在实际开发中,优化重组提升性能的核心思想--"减少重组"。即用最少的重组次数到达相同的结果,就是优化重组。
2.6.2 重组的核心触发条件:
可观察状态变化只有当组合函数依赖的可观察状态发生变化时,才会触发重组。
kotlin
@Composable
fun CounterExample() {
// 不会导致重组,普通变量非可观察状态,compose感知不到其变化。
//var count = 0
// 定义一个状态变量
var count by remember { mutableStateOf(0) }
Column(
modifier = Modifier.padding(16.dp)
) {
// 这个 Text 依赖 count 状态
Text(
text = "点击了 $count 次",
fontSize = 20.sp
)
// 这个 Text 不依赖 count 状态
Text(
text = "Hello world",
fontSize = 20.sp
)
// 点击按钮改变状态
Button(onClick = { count++ }) {
Text("点击我")
}
}
}
2.6.3 重组的执行机制:智能依赖追踪
Compose编译器在编译期会自动记录"每个可组合函数依赖的状态",当状态发生变化时,只执行"依赖该状态的所有可组合函数",不执行无关函数。
关键特性:
-
精准定位依赖,不盲目执行整个界面的可组合函数,只定位到"状态依赖链上的函数",这也是compose性能高效的关键。
-
并行执行,多个不相关的可组合函数重组时,会并行执行(如两个独立的可组合函数依赖统一状态)。
关于并行执行之前一直存在疑问点:重组刷新UI,不应该在UI线程吗?那怎么还能后台线程并行执行呢? 还是上面提到的重组只是一个过程(计算过程),负责计算任务(包括组件类型、参数、层级等信息),这个过程中不会涉及到任何屏幕绘制,所以重组是可以在后台线程并行执行的。
-
跳过无变化节点(跳过重组):重组时会检查可组合函数的输入参数,会直接跳过该函数的执行,复用之前的结果。
如上2.6.2章节示例,count发生变化,不会导致第二个Text重组。(这里我之前有点转不过弯来)
重组时CounterExample()函数本身就是一个函数,所以重组时会从上至下依次执行,当执行到第二Text()函数时Compose会先检查参数是否发生变换,显然没有变化跳过执行第二Text()函数。 即这里第一次重组时第二个Text()函数会生成TextView对象,第二次重组时检查后发现没有变化,跳过执行第二个Text()函数,那么直接复用第一次重组生成的TextView对象。
2.6 副作用管理
定义:副作用是指在组合函数中,影响外部环境或者依赖外部环境的操作都认为是副作用。
- 副作用的代码不能直接写在组合函数中,因为可能在重组时引发不可预测的问题(如重复执行),所以需要使用Compose提供的副作用API包起来,即副作用管理。
副作用场景 | 推荐 API | 原因 |
---|---|---|
组件进入时自动发请求 | LaunchedEffect | 与组件生命周期绑定,自动取消 |
点击按钮发请求 | rememberCoroutineScope | 用户主动触发,需手动启动 |
注册广播 / 监听器(需清理) | DisposableEffect | 组件销毁时自动执行清理逻辑 |
每次重组后上报日志 | SideEffect | 简单副作用,无需生命周期控制 |
-
LaunchedEffect:处理与组件生命周期绑定的异步副作用,LaunchedEffect用于在组合函数中启动协程。
a、组件销毁时自动取消协程,避免内存泄漏。
b、添加观察的key后,当key发生变化,旧协程会被取消,新协程启动。
kotlin
@Composable
fun UserProfile(userId: String) {
//错误示例,在组合函数中直接请求网络,每次重组时都会发起网络请求
//fetchDataFromNetwork() // 网络请求(副作用)
var user by remember { mutableStateOf<User?>(null) }
// 副作用:网络请求,依赖 userId(userId 变化时会重新执行)
LaunchedEffect(userId) {
// 耗时操作放在协程中,不阻塞主线程
user = api.fetchUser(userId) // 网络请求(副作用)
}
if (user != null) {
Text("Name: ${user?.name}")
} else {
CircularProgressIndicator()
}
}
-
rememberCoroutineScope:处理用户交互触发的副作用
用于获取一个与组件生命周期绑定的协程作用域,适合处理 "用户主动触发" 的副作用(如点击按钮后发请求),组件销毁时自动取消所有未完成的协程,避免内存泄漏。
kotlin
@Composable
fun SubmitButton() {
// 获取与组件生命周期绑定的协程作用域
val scope = rememberCoroutineScope()
var result by remember { mutableStateOf<String?>(null) }
Button(onClick = {
// 点击按钮时启动协程(用户主动触发)
scope.launch {
result = api.submitData() // 网络请求(副作用)
}
}) {
Text("提交")
}
result?.let { Text("结果:$it") }
}
-
DisposableEffect:处理需要手动清理的副作用
用于注册那些需要 "手动取消" 的资源(如监听器、广播接收器),确保组件销毁时能正确清理。 onDispose 块中的代码会在组件销毁时执行,确保资源被释放。
kotlin
@Composable
fun BatteryLevelMonitor() {
val context = LocalContext.current
var batteryLevel by remember { mutableStateOf(0) }
// 副作用:注册电池电量监听器,需要手动取消
DisposableEffect(Unit) {
// 注册监听器(副作用)
val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
batteryLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0)
}
}
context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
// 组件销毁时执行:取消监听器(清理副作用)
onDispose {
context.unregisterReceiver(receiver)
}
}
Text("电池电量:$batteryLevel%")
}
-
SideEffect:处理需要在每次重组后执行的副作用
适用于简单的副作用(如更新日志、统计埋点),每次重组完成后都会执行
kotlin
@Composable
fun TrackScreenView(screenName: String) {
// 每次重组后上报页面浏览(副作用)
SideEffect {
Analytics.trackScreenView(screenName)
}
// ... 页面内容
}
-
总结:副作用管理的核心目的
a、隔离UI与副作用,让可组合函数专注于描述UI,副作用由专门的API进行处理,代码职责更清晰。
b、自动声明周期绑定,确保副作用在组件创建时启动、销毁时清理,避免内存泄漏。
c、控制执行时机,通过key控制副作用是否执行,避免无效操作(如重复执行)。
d、线程安全,配合协程,让副作用在后台执行,不阻塞UI现场
2.7 重组优化
为什么需要优化? ->优化思路 -> 常见优化实现
2.7.1 为什么需要优化?
上面提到虽然Compose通过"智能依赖追踪"实现高效重组,但做为初学者在实际开发中还是可能因为"状态管理不当"、"组件设计不合理"从而导致"无效重组"。例如:
- 不依赖状态的组件被误触发重组 每次点击操作都会导致BadComponent() ->UserCard()重组,原因:
1、count 状态变化导致BadComponent()重组
2、BadComponent()每次重组都会创建新的userInfo对象,进而继续导致UserCard()重组。
kotlin
@Composable
fun BadComponent() {
var count by remember { mutableStateOf(0) }
// 错误 :每次重组都会创建新的列表对象(即使内容相同)
val userInfo = listOf("张三", "30岁")
// 正确:用remember缓存列表对象,确保每次重组都是同一个引用
//val userInfo = remember { listOf("张三", "30岁") }
Column {
Text("计数: $count") // 正常重组
UserCard(info = userInfo) // 不依赖count,但会被误触发重组
Button(onClick = { count++ }) { ... }
}
}
@Composable
fun UserCard(info: List<String>) {
// 因为info每次都是新列表对象,即使内容相同,这个组件也会重组
Card {
Column {
Text("姓名:${info[0]}")
Text("年龄:${info[1]}")
}
}
}
-
频繁重组导致性能卡顿
每次在username 或者password输入框中输入单个字符,都会导致LoginScreen()重组,进而导致isLoginEnabled创建新的对象,最终导致Button()频繁重组。
kotlin@Composable fun LoginScreen() { // 状态源1:用户名输入 var username by remember { mutableStateOf("") } // 状态源2:密码输入 var password by remember { mutableStateOf("") } //错误写法,每次输入username或者password时,isLoginEnabled都会导致按钮重组 val isLoginEnabled = username.isNotEmpty() && password.isNotEmpty() //正确写法 用derivedStateOf组合两个状态,判断按钮是否可点击 //val isLoginEnabled by remember { // derivedStateOf { // 同时依赖username和password两个状态 // username.isNotEmpty() && password.isNotEmpty() // } //} // 依赖isLoginEnabled的按钮 Button( onClick = { /* 登录逻辑 */ }, enabled = isLoginEnabled ) {Text("登录")} } ```
2.7.2 优化思路
- 拆解,将大型的Composeable拆解成小型的组件,目的通过缩小依赖范围,避免无效重组。
- 创建对象,避免在可组合函数中直接创建对象,应使用remember缓存。
- 稳定参数与类型,通过直接使用稳定或者标记稳定的参数与类型,避免无效重组。
- 跳过重组,使用Compose API提供的接口跳过来优化重组。
2.7.3 常见优化重组
3 基础控件介绍与使用
Compose支持传统xml中绝大多数组件(TextView ->Text、ImageView -> Image等),并提供了部分更好用组件(RecycleView ->LazyRow/LazyColumn、EditText -> TextField等),但少部分特殊的组件未实现(wevView、surfaceView等)。
3.1 Text
kotlin
/**
* Compose 中用于显示文本的基础组件,相当于传统 XML 中的 TextView
*/
@Composable
fun Text(
/** 要显示的文本内容,必填参数 */
text: String,
/** 用于修改组件外观和行为的修饰符(如尺寸、间距、点击事件等),默认值为空修饰符 */
modifier: Modifier = Modifier,
/** 文本颜色,默认使用主题中的文本颜色(Color.Unspecified 表示继承父组件样式) */
color: Color = Color.Unspecified,
/** 文本大小,默认使用主题中的字体大小(TextUnit.Unspecified 表示继承) */
fontSize: TextUnit = TextUnit.Unspecified,
/** 字体样式(如斜体),null 表示使用默认样式 */
fontStyle: FontStyle? = null,
/** 字体粗细(如粗体、细体),null 表示使用默认粗细 */
fontWeight: FontWeight? = null,
/** 字体家族(如衬线字体、无衬线字体),null 表示使用默认字体 */
fontFamily: FontFamily? = null,
/** 字符间距,默认使用主题中的配置 */
letterSpacing: TextUnit = TextUnit.Unspecified,
/** 文本装饰(如下划线、删除线),null 表示无装饰 */
textDecoration: TextDecoration? = null,
/** 文本对齐方式(如左对齐、居中、右对齐),null 表示继承父组件对齐方式 */
textAlign: TextAlign? = null,
/** 行高(行间距),默认使用主题中的行高配置 */
lineHeight: TextUnit = TextUnit.Unspecified,
/** 文本溢出时的处理方式,默认是裁剪(Clip),其他选项有省略号(Ellipsis)等 */
overflow: TextOverflow = TextOverflow.Clip,
/** 是否自动换行,true 表示自动换行,false 表示不换行(可能导致溢出) */
softWrap: Boolean = true,
/** 最大显示行数,默认是无限制(Int.MAX_VALUE) */
maxLines: Int = Int.MAX_VALUE,
/** 文本布局完成后的回调,可获取文本测量信息(如宽度、高度、行数等) */
onTextLayout: (TextLayoutResult) -> Unit = {},
/** 文本样式集合,可统一设置字体、大小、颜色等属性,默认使用当前主题的文本样式 */
style: TextStyle = LocalTextStyle.current
)
大部分的参数都是自定义参数,就不过多介绍了,大家可以一个一个试试效果。主要介绍下Modifier、超文本、以及跑马灯效果,体验下compose的方便、
3.1.1 Modifier
Modifier是Compose非常核心的概念,用于配置和装饰组件的布局、外观、行为和交互方式。相当于传统xml中layout_with、layout_height、padding、click等等,并添加了滚动、动画等,这也是使用compose实现动画更加高效的原因。
3.1.1.1 Modifier 简介
布局控制和外观装饰大家可以一个一个试试效果,需要注意的的是:
padding在size之前 --> 外边距
padding在size之后 --> 内边距
能力分类 | 作用说明 | 常用 API 示例 |
---|---|---|
布局控制 | 定义组件在父容器中的 "位置、尺寸、约束" | - size(100.dp):固定组件宽高 - width(200.dp)/height(150.dp):单独控制宽 / 高 - weight(1f):在 Row/Column 中占剩余空间 - align(Alignment.Center):在父容器中对齐 - padding(16.dp):组件外/内边距(子内容与组件边缘的距离) |
外观装饰 | 美化组件的 "视觉样式" | - background(Color.Blue):设置背景色 - border(BorderStroke(2.dp, Color.Red)):设置边框 - cornerRadius(8.dp):设置圆角 - alpha(0.5f):设置透明度 - shadow(4.dp):添加阴影 |
交互响应 | 为组件添加 "用户操作触发的逻辑" | - clickable { /* 点击逻辑 */ }:点击事件 - scrollable(scrollState, Orientation.Vertical):滚动能力 - draggable(dragState):拖拽能力 - focusable():获取焦点(如输入框) |
高级能力 | 承载动画、状态联动等复杂逻辑 | - animateDpAsState(targetValue = 200.dp):尺寸动画 - swipeToDismiss(/* 滑动删除逻辑 */):滑动删除 - clip(RectangleShape):裁剪组件形状 - testTag("text_tag"):用于 UI 测试 |
3.1.1.2 Modifier 交互响应
函数名 | 作用 | 关键回调 |
---|---|---|
detectTapGestures | 检测点击相关手势(单击、双击、长按、按下 / 抬起) | - onTap:单击回调 - onDoubleTap:双击回调 - onLongPress:长按回调 - onPress:按下 / 抬起的过程回调(可取消) |
detectDragGestures | 检测拖拽手势(适用于简单拖拽,如滑动列表项) | - onDragStart:拖拽开始 - onDrag:拖拽中(返回偏移量) - onDragEnd:拖拽结束 - onDragCancel:拖拽取消 |
detectDragGesturesAfterLongPress | 长按后才允许拖拽(如长按列表项后拖动排序) | 同 detectDragGestures 的回调 |
detectTransformGestures | 检测 "缩放 + 旋转 + 平移" 组合手势(如图片缩放) | - onGesture:手势过程中回调,返回缩放比例、旋转角度、平移偏移 |
detectHorizontalDragGestures | 专门检测水平方向拖拽(过滤垂直方向干扰) | 类似 detectDragGestures,但只响应水平拖拽 |
detectVerticalDragGestures | 专门检测垂直方向拖拽(过滤水平方向干扰) | 类似 detectDragGestures,但只响应垂直拖拽 |
detectFlingGestures | 检测抛掷(fling)手势(如滑动后惯性滚动) | - onFling:抛掷时回调,返回初速度 - consumeFling:是否消费该手势 |
less
@Composable
fun Greeting(modifier: Modifier = Modifier) {
Text(
// 布局控制和外观装饰
text = "我是一个文本",
fontSize = 24.sp,
color = Color.White,
modifier = modifier
.padding(100.dp)
.background(Color.Blue)
.height(50.dp)
.fillMaxWidth()
.widthIn(min = 100.dp)
// 交互响应
// 1 响应点击
.clickable(onClick = {
Log.d("TAG", "点击了")
} )
// 2 响应手势
.pointerInput(Unit) {
detectTapGestures(
onTap = {
Log.d("TAG", "点击了")
} ,
onDoubleTap = {
Log.d("TAG", "双击了")
} ,
onLongPress = {
Log.d("TAG", "长按了")
} ,
onPress = {
Log.d("TAG", "按下了")
} ,
)
detectDragGestures(
onDragStart = {
Log.d("TAG", "开始拖拽")
} ,
onDragEnd = {
Log.d("TAG", "结束拖拽")
} ,
onDragCancel = {
Log.d("TAG", "取消拖拽")
} ,
onDrag = { change, dragAmount ->
Log.d("TAG", "拖拽中")
} ,
)
}
)
}
2025-09-22 20:27:53.296 4877-4877 TAG com.example.test D 拖拽中
2025-09-22 20:27:53.344 4877-4877 TAG com.example.test D 拖拽中
2025-09-22 20:27:53.612 4877-4877 TAG com.example.test D 拖拽中
2025-09-22 20:27:53.628 4877-4877 TAG com.example.test D 拖拽中
2025-09-22 20:27:53.710 4877-4877 TAG com.example.test D 拖拽中
2025-09-22 20:27:53.960 4877-4877 TAG com.example.test D 拖拽中
2025-09-22 20:27:53.977 4877-4877 TAG com.example.test D 拖拽中
2025-09-22 20:27:53.993 4877-4877 TAG com.example.test D 拖拽中
2025-09-22 20:27:54.770 4877-4877 TAG com.example.test D 结束拖拽
---------------------------- PROCESS ENDED (4877) for package com.example.test ----------------------------
2025-09-22 20:28:08.959 4948-4948 TAG com.example.test D 按下了
2025-09-22 20:28:09.363 4948-4948 TAG com.example.test D 长按了
---------------------------- PROCESS STARTED (4948) for package com.example.test ----------------------------
2025-09-22 20:28:12.974 4948-4948 TAG com.example.test D 按下了
2025-09-22 20:28:13.450 4948-4948 TAG com.example.test D 点击了
2025-09-22 20:28:16.685 4948-4948 TAG com.example.test D 按下了
2025-09-22 20:28:16.916 4948-4948 TAG com.example.test D 按下了
2025-09-22 20:28:17.098 4948-4948 TAG com.example.test D 双击了
3.1.1.3 Modifier 高级能力
- 水平方向拖拽:
ini
@Composable
fun Greeting(modifier: Modifier = Modifier) {
// 记录滑块的水平偏移量(初始在左侧)
var offsetX by remember { mutableStateOf(0f) }
val maxOffset = 500f
// 创建拖拽状态
val dragState = rememberDraggableState { delta ->
// 限制偏移量在 0 到 maxOffset 之间(避免超出容器)
offsetX = (offsetX + delta).coerceIn(0f, maxOffset)
}
Text(
// 布局控制和外观装饰
text = "我是一个文本",
fontSize = 24.sp,
color = Color.Black,
modifier = modifier
.padding(top = 100.dp)
.height(50.dp)
.widthIn(100.dp)
.widthIn(min = 100.dp)
.offset { IntOffset(offsetX.roundToInt(), 0) } // 应用水平偏移
.draggable(
state = dragState,
orientation = Orientation.Horizontal,
onDragStopped = {
// 拖拽结束后,自动吸附到最近的端点
offsetX = if (offsetX > maxOffset / 2) maxOffset else 0f
} )
.background(Color.Blue)
)
}
- 尺寸动画:
Modifier.animateDpAsState 是 Compose 中用于实现尺寸动画的便捷 API,它可以将 Dp 类型的状态变化转换为平滑的动画效果,常用于组件尺寸、边距、偏移量等属性的动态过渡。
ini
@Composable
fun Greeting(modifier: Modifier = Modifier) {
// 1. 定义目标状态(会触发动画的值)
var targetSize by remember { mutableStateOf(50.dp) }
// 2. 创建动画状态(监听目标状态变化,生成动画)
val animatedSize by animateDpAsState(
targetValue = targetSize, // 目标值(状态变化时触发动画)
animationSpec = tween(
durationMillis = 500, // 动画时长(毫秒)
easing = FastOutSlowInEasing // 缓动曲线
)
)
Text(
// 布局控制和外观装饰
text = "我是一个文本",
fontSize = 24.sp,
color = Color.White,
modifier = modifier
.padding(top = 100.dp)
.height(animatedSize)
.widthIn(100.dp)
.background(Color.Blue)
.clickable(onClick = {
targetSize = if (targetSize == 50.dp) 200.dp else 50.dp
} )
)
}
-
自定义扩展
假设点击时都需要上报点位
ini
@Composable
fun Greeting(modifier: Modifier = Modifier) {
Text(
// 布局控制和外观装饰
text = "我是一个文本",
fontSize = 24.sp,
color = Color.White,
modifier = modifier
.padding(top = 100.dp)
.height(100.dp)
.widthIn(100.dp)
.background(Color.Blue)
.MyClickable(type = 1)
)
Text(
text = "我 Another Text",
fontSize = 24.sp,
color = Color.White,
modifier = modifier
.padding(top = 200.dp)
.height(100.dp)
.widthIn(100.dp)
.background(Color.Red)
.MyClickable(type = 2)
)
}
@SuppressLint("UnnecessaryComposedModifier")
fun Modifier.MyClickable(type: Int) = composed {
this.then(Modifier.clickable(onClick = {
Log.d("fangwen", "上报点位,type:$type")
} ))
}
2025-09-23 09:07:56.416 9724-9724 fangwen com.example.test D 上报点位,type:1
2025-09-23 09:07:58.319 9724-9724 fangwen com.example.test D 上报点位,type:2
3.1.2 超文本

ini
@Composable
fun MarqueeWithBasicMarquee() {
Text(
text = "这是使用 basicMarquee 实现的跑马灯效果,简洁高效!",
modifier = Modifier
.width(200.dp) // 限制容器宽度(触发滚动的前提)
.basicMarquee(
iterations = Int.MAX_VALUE, // 无限循环(默认值)
initialDelayMillis = 1000, // 滚动前延迟1秒(默认300ms)
animationDurationMillis = 3000, // 滚动一次耗时3秒(默认2000ms)
spacing = 20.dp, // 文本首尾衔接时的间距(默认0dp)
velocity = 50.dpPerSecond // 滚动速度(默认30.dpPerSecond)), // 核心:启用跑马灯效果
)
}
3.1.3 跑马灯效果
一个basicMarquee函数就实现跑马灯效果,相对传统xml需要设置一堆属性而言还是方便很多。并且解决了传统xml三个痛点:- 灵活,basicMarquee提供很多参数,效果更灵活。
- 智能判断,compose会自动判断出仅当文本宽度超过容器宽度时,才会启动滚动。
- 焦点依赖,这也是TextView最大的痛点依赖焦点,当界面存在多个跑马灯组件或者组件失去焦点时跑马灯效果会被暂停,需要强制"抢夺"焦点,而basicMarquee避免了焦点相关的兼容性问题。
ini
@Composable
fun MarqueeWithBasicMarquee() {
Text(
text = "这是使用 basicMarquee 实现的跑马灯效果,简洁高效!",
modifier = Modifier
.width(200.dp) // 限制容器宽度(触发滚动的前提)
.basicMarquee(
iterations = Int.MAX_VALUE, // 无限循环(默认值)
initialDelayMillis = 1000, // 滚动前延迟1秒(默认300ms)
animationDurationMillis = 3000, // 滚动一次耗时3秒(默认2000ms)
spacing = 20.dp, // 文本首尾衔接时的间距(默认0dp)
velocity = 50.dpPerSecond // 滚动速度(默认30.dpPerSecond)), // 核心:启用跑马灯效果
)
}
3.2 Image
- 加载本地图片:
kotlin
/**
* Compose 中的 Image 组件用于显示图片,以下是各参数的详细说明及使用示例
*/
@Composable
fun ImageExample() {
// 加载本地资源图片(需在res/drawable目录下准备example_image.png)
val painter = painterResource(id = R.drawable.example_image)
Image(
// 1. 图片绘制器:指定要显示的图片资源
// 可以是本地资源(painterResource)、网络图片(配合Coil等库)或矢量图
painter = painter,
// 2. 内容描述:用于无障碍服务(如屏幕阅读器)的图片说明
// 为空表示该图片纯装饰性,不传达关键信息
contentDescription = "一张展示自然风光的示例图片,包含山脉和湖泊",
// 3. 修饰符:用于配置图片的尺寸、边距、点击事件等
modifier = Modifier
.size(300.dp) // 设置图片大小为300dp×300dp
.padding(16.dp) // 添加上下左右16dp内边距
.border(2.dp, Color.Gray, RoundedCornerShape(8.dp)) // 添加灰色边框和圆角
.clickable { println("图片被点击了") }, // 添加点击事件
// 4. 对齐方式:图片在容器内的对齐方式(当图片尺寸小于容器时生效)
alignment = Alignment.TopStart, // 图片在容器左上角对齐
// 5. 内容缩放模式:控制图片如何适应容器尺寸
contentScale = ContentScale.Crop, // 裁剪图片以填满容器(保持宽高比)
// 6. 透明度:0f(完全透明)到1f(完全不透明)
alpha = 0.8f, // 图片透明度为80%
// 7. 颜色过滤器:为图片应用颜色变换(如染色、灰度等)
colorFilter = ColorFilter.tint(Color.Blue, BlendMode.SrcAtop) // 将图片染成蓝色
)
}
- 加载网络图片
less
/**
* 使用 Coil 加载网络图片,并演示 Image 组件所有参数
*/
@Composable
fun NetworkImageExample() {
// 获取上下文(用于构建图片请求)
val context = LocalContext.current
// 1. 创建网络图片绘制器(使用Coil的rememberAsyncImagePainter)
val networkPainter = rememberAsyncImagePainter(
model = ImageRequest.Builder(context)
.data("https://picsum.photos/800/600") // 网络图片URL
.crossfade(true) // 淡入淡出过渡效果
.placeholder(R.drawable.placeholder) // 加载中占位图(本地资源)
.error(R.drawable.error_image) // 加载失败 fallback 图(本地资源)
.build()
)
Image(
// 1. painter:网络图片绘制器(Coil提供)
painter = networkPainter,
// 2. contentDescription:无障碍描述(屏幕阅读器会读取)
contentDescription = "网络加载的风景图片,包含山脉和森林",
// 3. modifier:配置尺寸、边距、交互等
modifier = Modifier
.padding(start = 400.dp, top = 300.dp)
.size(300.dp, 200.dp) // 宽300dp,高200dp
.padding(horizontal = 16.dp, vertical = 8.dp) // 水平16dp,垂直8dp内边距
.border(2.dp, Color.Green, RoundedCornerShape(12.dp)) // 绿色边框+圆角
.clickable { // 点击事件
Log.d("fangwen", "点击了网络图片")
} ,
// 4. alignment:图片在容器内的对齐方式(图片尺寸 < 容器时生效)
alignment = Alignment.Center, // 居中对齐
// 5. contentScale:图片适应容器的缩放模式
contentScale = ContentScale.Crop, // 裁剪填充(保持比例,可能裁掉边缘)
// 6. alpha:透明度(0f完全透明,1f完全不透明)
alpha = 0.9f, // 90%不透明度
)
}
3.3 AndroidView
AndroidView 的核心作用就是兼容传统View或者布局,从而实现项目从传统View渐进式迁移(实现分批逐步替换),进而保证项目的稳定迁移。当然也能通过AndroidView补充Compose暂不支持的功能(例如surfaceView、webview等)
kotlin
@Composable
fun EmbedXmlLayoutInCompose() {
// 1. Compose 状态:用于同步更新 XML 布局中的控件
var xmlText by remember { mutableStateOf("这是 XML 布局中的文本") }
var isXmlBtnClicked by remember { mutableStateOf(false) }
Column(
modifier = Modifier.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 2. 核心:通过 AndroidView 嵌入 XML 布局
AndroidView(
// 工厂函数:加载 XML 布局并返回 View 实例(仅初始化一次)
factory = { context ->
// 加载 XML 布局(参数1:XML 布局ID,参数2:父布局null,避免附加到父容器)
LayoutInflater.from(context).inflate(R.layout.android_view_layout, null).apply {
// 找到 XML 中的 Button,设置点击事件(回调到 Compose 状态)
val xmlBtn = this.findViewById<Button>(R.id.xml_btn_click)
xmlBtn.setOnClickListener {
isXmlBtnClicked = !isXmlBtnClicked
// 点击后更新 XML 中的 TextView 文本
xmlText = if (isXmlBtnClicked) "XML 按钮已点击!" else "这是 XML 布局中的文本"
}
}
} ,
// 更新函数:当 Compose 状态变化时,更新 XML 布局中的控件(多次执行)
update = { xmlRootView ->
// 找到 XML 中的 TextView,用 Compose 状态更新文本
val xmlTv = xmlRootView.findViewById<TextView>(R.id.xml_tv_title)
xmlTv.text = xmlText
} ,
// Compose 修饰符:控制 XML 布局在 Compose 中的尺寸、边距等
modifier = Modifier.padding(top = 50.dp)
)
// 3. Compose 原生控件:同步响应 XML 中的状态变化
Text(text = "XML 按钮点击状态:${if (isXmlBtnClicked) "已点击" else "未点击"}")
// Compose 按钮:更新状态,间接修改 XML 布局中的控件
Text("Compose 按钮:修改 XML 文本",Modifier.padding(top = 12.dp).clickable(onClick = {
xmlText = "Compose 按钮修改了 XML 文本!"
} ))
}
}
4 基础布局介绍与使用
4.1 Box -> FrameLayout

scss
Box(
modifier = Modifier
.padding(start = 50.dp, top = 50.dp)
.size(300.dp, 300.dp)
.background(Color.Green)
) {
Text(
"First",
modifier = Modifier.background(Color.Gray),
color = Color.White
)
Text(
"Second",
modifier = Modifier.padding(50.dp).background(Color.Blue),
color = Color.White
)
}
4.2 Row/Column -> LinearLayout
-
Row
scss
Row (
modifier = modifier
.size(300.dp, 300.dp)
.background(Color.Green)
) {
Text(
"First",
modifier = modifier.background(Color.Gray),
color = Color.White
)
Text(
"Second",
modifier = modifier.padding(start = 20.dp).background(Color.Blue),
color = Color.White
)
}
-
Column
scss
Column (
modifier = modifier
.size(300.dp, 300.dp)
.background(Color.Green)
) {
Text(
"First",
modifier = Modifier.background(Color.Gray),
color = Color.White
)
Text(
"Second",
modifier = Modifier.padding(top = 20.dp).background(Color.Blue),
color = Color.White
)
}
4.3 ConstraintLayout -> ConstraintLayout
先引入ConstraintLayout
csharp
//libs.versions.toml
constraintlayout = "1.0.1"
androidx-compose-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintlayout" }
//build.gradle
implementation(libs.androidx.compose.constraintlayout)

scss
ConstraintLayout(
modifier = modifier
.size(300.dp, 300.dp)
.background(Color.Green)
) {
val (first, second) = createRefs()
Text(
"First",
color = Color.White,
modifier = Modifier
.background(Color.Gray)
.constrainAs(first) {
top.linkTo(parent.top)
start.linkTo(parent.start)
}
)
Text(
"Second",
color = Color.White,
modifier = Modifier
.background(Color.Blue)
.constrainAs(second) {
// 相对于first控件,下方20dp 且 右边50.dp
top.linkTo(first.bottom,20.dp)
start.linkTo(first.end,50.dp)
} ,
)
}
4.4 LazyRow -> RecycleView
LazyRow/LazColumn -> RecycleView 主要特点:- 代码更加简洁,代码量官方称相对于RecycleView减少70%。(无需Adapted、ViewHolder)
- 扩展性更强,可直接使用compose主题、动画、交互API。(后续动画章节会讲到列表的item滑动删除、一键清理动画)
scss
// 数据模型
data class ListItem(
val id: Int,
val title: String,
val resouceId:Int
)
@Composable
fun SimpleLazyRow() {
// 模拟列表数据
val items = remember {
listOf(
ListItem(1, "风景1", R.drawable.placeholder),
ListItem(2, "风景2", R.drawable.placeholder),
ListItem(3, "风景3", R.drawable.placeholder),
ListItem(4, "风景4", R.drawable.placeholder),
ListItem(5, "风景5", R.drawable.placeholder),
ListItem(6, "风景6", R.drawable.placeholder),
ListItem(7, "风景7", R.drawable.placeholder)
)
}
// 水平滚动列表
LazyRow(
modifier = Modifier
.padding(start = 100.dp, top = 100.dp)
.fillMaxWidth()
.height(250.dp), // 固定列表高度
contentPadding = PaddingValues(horizontal = 16.dp), // 列表左右内边距
horizontalArrangement = Arrangement.spacedBy(12.dp) // item之间的间距
) {
// 遍历数据生成列表项
items(
items = items,
key = { item -> item.id }
) { item ->
Image(
// 1. painter:本地资源图片绘制器
painter = painterResource(id = item.resouceId),
// 2. contentDescription:无障碍描述(屏幕阅读器会读取)
contentDescription = item.title,
// 3. modifier:配置尺寸、边距等
modifier = Modifier
.size(200.dp, 150.dp)
.clickable {
Log.d("fangwen", "点击了图片: ${item.title}")
} ,
// 4. contentScale:图片适应容器的缩放模式
contentScale = ContentScale.Crop
)
}
}
}
5 参考资料
- 基础组件、布局组件使用
- 官方文档