Compose
@Composable、Modifier、三大布局(Column/Row/Box)、基础组件(Text/Image/Icon/ 网络图片)
# 核心思想
声明式 UI 框架
定义 State
↓
根据 State 描述 UI
↓
State 改变
↓
Compose 自动更新 UI
2
3
4
5
6
7
# setContent·
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Compose UI
Text("Hello Android")
}
}
}
2
3
4
5
6
7
8
9
10
11
执行流程
@ComposableMainActivity
↓
onCreate()
↓
setContent {}
↓
Compose UI
2
3
4
5
6
7
# @Composable
告诉 Compose:这个函数用于构建 UI。
@Composable
fun Greeting() {
Text("Hello Android")
}
2
3
4
# @Preview
@Preview 是 Android Studio(AS)的实时预览注解——加上它之后,不需要把 App 跑到模拟器或真机上,就能在 AS 右侧的 Split/Design 面板里直接看到这个组件渲染出来的样子,写 UI 时可以边改边看,效率高很多。
@Preview(showBackground = true) // showBackground 显示白色背景
@Composable
fun GreetingPreview() {
MaterialTheme {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text("第一行", fontSize = 20.sp, fontWeight = FontWeight.Bold)
Text("第二行", fontSize = 16.sp, color = Color.Gray)
Text("第三行", fontSize = 14.sp)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 常用组件
# Text
Text(
text = "Hello",
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
2
3
4
5
# Button
Button(
onClick = {
println("点击按钮")
}
) {
Text("点击")
}
2
3
4
5
6
7
# 图片 Image
| ContentScale | 效果 | 使用场景 |
|---|---|---|
| Fit | 保持原图比例,完整显示整张图,容器多余区域会留白 | 图标、产品预览图,需要图片全部可见 |
| Crop | 保持原图比例,把容器填满,超出容器的图片部分会被裁剪 | 头像、轮播 Banner、封面图 |
| FillBounds | 直接拉伸图片填满容器,不维持原图比例,图片可能变形 | 一般不推荐使用 |
本地图片
Image(
painter = painterResource(
id = R.drawable.logo // main/res/drawable/logo
),
contentDescription = "Logo"
)
2
3
4
5
6
网络图片
Coil 是 Jetpack Compose 生态最流行的图片加载第三方库,支持网络图片、本地图片,自带缓存、占位图、错误图。
添加依赖(
app/build.gradle.kts)dependencies { implementation("io.coil-kt:coil-compose:2.5.0") }1
2
3AsyncImage 基础使用代码
AsyncImage( model = "https://xxx/test.jpg", // 图片url contentDescription = "网络图片", contentScale = ContentScale.Crop, placeholder = { // 加载中占位图 Box(Modifier.background(Color.LightGray)) }, error = { // 加载失败展示图 Box(Modifier.background(Color.Red)) }, modifier = Modifier.size(120.dp) )1
2
3
4
5
6
7
8
9
10
11
12
13
14SubcomposeAsyncImage精细控制
import coil.compose.SubcomposeAsyncImage SubcomposeAsyncImage( model = "https://example.com/your-image.jpg", contentDescription = "图片描述", loading = { // 图片加载中占位:圆形进度条 Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() } }, error = { // 加载失败占位:错误图标 Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Icon(Icons.Default.Error, contentDescription = null) } } )1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Icon图标
@Composable
fun IconDemo() {
Row(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
// Material 预设图标
Icon(
imageVector = Icons.Default.Home, // 首页图标矢量图
contentDescription = "首页", // 无障碍描述,读屏软件使用
tint = Color.Black, // 图标着色
modifier = Modifier.size(32.dp) // 图标尺寸
)
// 搜索图标
Icon(
imageVector = Icons.Default.Search,
contentDescription = "搜索",
tint = Color(0xFF1A73E8), // 自定义十六进制颜色
modifier = Modifier.size(32.dp)
)
// 爱心图标(截图中第三个)
Icon(
imageVector = Icons.Default.Favorite,
contentDescription = "收藏",
tint = Color(0xFF1A73E8),
modifier = Modifier.size(32.dp)
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| 参数 | 作用 |
|---|---|
imageVector | 矢量图标资源,Icons.Default.xxx,矢量图放大不会模糊 |
contentDescription | 无障碍文本;不需要读屏时可以传 null |
tint | 图标颜色,矢量图标会被这个颜色染色 |
modifier | 修饰符,常用 .size() 设置图标大小 |
# Image和Icon和AsyncImage比较
| 组件 | 用途 | 资源类型 | 特点 |
|---|---|---|---|
Image | 展示本地图片 | 位图 PNG/JPG/WebP,painterResource(R.drawable.xxx) | 放大容易模糊,不支持 tint 染色,适合照片、大图 |
Icon | 展示矢量图标 | Material 矢量图标 Icons.Default.xxx | 矢量图,支持 tint 动态改色,放大不失真,适合小图标 |
AsyncImage(Coil) | 加载网络图片 | 网络图片 URL | 自带缓存、加载 / 错误占位,Coil 库提供 |
# 输入框 TextField
@Composable
fun InputDemo() {
var name by remember {
mutableStateOf("")
}
TextField(
value = name,
onValueChange = {
name = it
},
label = {
Text("请输入姓名")
}
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Modifier修饰符
设置尺寸、间距、背景、点击、位置等 UI 属性。
Text(
text = "Hello",
modifier = Modifier.padding(16.dp) // 四周增加 16dp 间距
)
// 链式调用
Modifier
.fillMaxWidth()
.height(50.dp)
.padding(16.dp)
// 例子
Button(
onClick = {},
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text("登录")
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
dp 和 sp
dp | sp |
|---|---|
| 宽度、高度、padding、margin 类似效果 | 字体大小 |
Text(
text = "Hello",
fontSize = 18.sp,
modifier = Modifier.padding(16.dp)
)
2
3
4
5
Color
@Composable
fun ColorDemo() {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// 方式1:预设颜色
Box(modifier = Modifier.size(60.dp).background(Color.Red))
Spacer(modifier = Modifier.height(8.dp))
// 方式2:ARGB 十六进制(最常用)
// 0xFF=完全不透明,1A73E8=RGB色值
Box(modifier = Modifier.size(60.dp).background(Color(0xFF1A73E8)))
Spacer(modifier = Modifier.height(8.dp))
// 方式3:带透明度
Box(modifier = Modifier.size(60.dp).background(Color.Black.copy(alpha = 0.3f)))
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
常用 Modifier 速查表
Modifier
// 尺寸
.width(100.dp) // 固定宽度
.height(50.dp) // 固定高度
.size(48.dp) // 宽高相同
.fillMaxWidth() // 填满父容器宽度
.fillMaxSize() // 填满父容器
.weight(1f) // 按比例分配空间(仅在 Row / Column 中)
// 间距
.padding(16.dp) // 四周内边距
.padding(horizontal = 16.dp, vertical = 8.dp) // 分别设置水平、垂直内边距
// 外观
.background(Color.White) // 背景色
.clip(RoundedCornerShape(8.dp)) // 圆角裁剪
.border(1.dp, Color.Gray, RoundedCornerShape(8.dp)) // 边框
// 交互
.clickable { onClick() } // 点击事件
// 其他
.aspectRatio(2.65f) // 宽高比(宽:高 = 2.65:1)
.offset(x = 10.dp, y = -4.dp) // 偏移
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Modifier 从左到右依次应用
padding 在前,background 在后:padding 在背景外面,背景区域缩小。
background 在前,padding 在后:padding 在背景里面,背景铺满,内部留出空白。
例子(搜索框圆角)
// 先 clip 裁剪圆角,再绘制背景色,背景会跟着裁剪后的圆角形状显示。
Modifier.clip(RoundedCornerShape(8.dp)).background(Color.White)
2
# 三大布局
# Column垂直排列
Column {
Text("第一行")
Text("第二行")
Button(
onClick = {}
) {
Text("按钮")
}
}
2
3
4
5
6
7
8
9
# Row水平排列
Row {
Text("姓名:")
Text("张三")
}
2
3
4
# Box堆叠布局
Box {
Text("底层")
Text("上层")
}
2
3
4
# Spacer间距
空白占位组件,专门用来做元素之间的间距,本身不会渲染任何内容。
@Composable
fun SpacerDemo() {
Column(modifier = Modifier.padding(16.dp)) {
Text("标题", fontSize = 20.sp, fontWeight = FontWeight.Bold)
Spacer(modifier = Modifier.height(8.dp)) // 标题和内容之间 8dp 间距
Text("这是内容文字", fontSize = 14.sp, color = Color.Gray)
}
}
2
3
4
5
6
7
8
Spacervs spacedBy
Spacer 是空白占位组件,手动添加间距;spacedBy是布局参数,统一给所有子元素设置相同间距。推荐优先用 spacedBy 做统一间距;需要局部不同大小间距时,使用 Spacer。
# weight按比例分配
weight() 只能直接放在 Row / Column 的子元素上,用来瓜分容器剩余可用空间。
搭配 .fillMaxSize().子元素拿到分配到的空间后,fillMaxSize() 让 Box 填满分到的这块区域,色块才会完整显示。
// 比例 1:1:1 → 各占 1/3
Row(modifier = Modifier.fillMaxWidth().height(40.dp)) {
Box(modifier = Modifier.weight(1f).fillMaxSize().background(Color.Red))
Box(modifier = Modifier.weight(1f).fillMaxSize().background(Color.Green))
Box(modifier = Modifier.weight(1f).fillMaxSize().background(Color.Blue))
}
Spacer(modifier = Modifier.height(8.dp))
// 比例 2:1 → 左边占 2/3,右边占 1/3
Row(modifier = Modifier.fillMaxWidth().height(40.dp)) {
Box(modifier = Modifier.weight(2f).fillMaxSize().background(Color(0xFFFF9800)))
Box(modifier = Modifier.weight(1f).fillMaxSize().background(Color(0xFF2196F3)))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 完整入门页面
@Composable
fun UserPage() {
var count by remember {
mutableStateOf(0)
}
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Compose 入门",
fontSize = 24.sp
)
Text(
text = "点击次数:$count"
)
Button(
onClick = {
count++
}
) {
Text("点击")
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 状态管理和列表
# 状态管理
@Composable
fun Counter() {
var count by remember {
mutableStateOf(0)
}
Button(
onClick = {
count++
}
) {
Text("点击次数:$count")
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# remember和mutableStateOf
| API | 作用 |
|---|---|
remember | 记住值,界面重新绘制(重组)时不丢失 |
mutableStateOf | 创建可观察的状态,值变化时自动刷新UI |
by 是 Kotlin 的属性委托语法。
// 不使用 by
val count = remember { mutableStateOf(0) }
count.value++
Text("${count.value}")
// 使用 by
var count by remember { mutableStateOf(0) }
count++
Text("$count")
2
3
4
5
6
7
8
9
10
11
12
# 状态提升
类似 React 里的:
父组件 state
↓
props 传递
↓
子组件展示
↓
callback 修改父组件 state
@Composable
fun Parent(){
var text by remember{
mutableStateOf("")
}
SearchBox(
text=text,
onTextChange={
text=it
}
)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 重组 Recomposition
状态改变,Compose 自动重新渲染受影响的界面。
// 流程
State 改变
↓
Compose 检测变化
↓
重新执行相关 Composable
↓
更新 UI
2
3
4
5
6
7
8
9
# 列表
# LazyColumn
Android Compose 中非常常见的列表组件
val names = listOf(
"张三",
"李四",
"王五"
)
LazyColumn {
items(names) { name ->
Text(name)
}
}
2
3
4
5
6
7
8
9
10
11
# 使用数据模型渲染列表
为什么不用普通 class?
data class自动生成方法;toString()、equals()、copy()
// 定义数据模型
data class QuestionItem(
val id: String,
val stem: String,
val difficulty: Int,
val views: Int
)
// 创建模拟数据
val mockQuestions = listOf(
QuestionItem(
"1",
"Activity生命周期有哪些回调?",
1,
120
),
QuestionItem(
"2",
"Handler的底层原理是什么?",
3,
580
),
)
// 渲染
@Composable
fun QuestionList(){
LazyColumn {
items(mockQuestions){ question ->
Column {
Text(
text = question.stem
)
Text(
text = "难度:${question.difficulty}"
)
Text(
text = "浏览量:${question.views}"
)
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 混合使用
item {} 和 items(list) {} 的区别
| 方法 | 作用 | 数量 |
|---|---|---|
item {} | 单个固定内容 | 一个 |
items(list){} | 循环列表数据 | 多个 |
LazyColumn {
// 顶部轮播图
item {
BannerView()
}
// 分类 Tab
item {
CategoryTabs()
}
// 题目列表
items(questionList) { question ->
QuestionItem(question)
}
// 底部提示
item {
Text("没有更多数据")
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# LaunchedEffect
执行副作用 / 协程任务
LaunchedEffect(Unit) {
println("页面进入")
}
2
3
4
5
| key 值 | 行为 | 适用场景 |
|---|---|---|
Unit | 只执行一次 | 进入页面时加载数据 |
| 某个变量 | 变量变化时重新执行 | 切换分类时重新加载 |
多个变量LaunchedEffect(key1, key2) | 任意一个 key 变化,都会重新执行 | 分类 + 排序条件都变化时刷新 |
LaunchedEffect 内部启动了一个协程,里面可以直接调用:suspend 函数
LaunchedEffect(Unit) {
delay(1000)
println("1秒后执行")
}
2
3
4
5
6
7
例子2:根据 id 请求数据
@Composable
fun DetailPage(
userId:Int
){
// 仅执行一次
LaunchedEffect(Unit) {
loadPageData()
}
// userId变化就重新执行
LaunchedEffect(userId) {
getUser(userId)
}
// category 或 sort 任一变化就执行
LaunchedEffect(category, sort) {
loadData(category, sort)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# rememberCoroutineScope
| LaunchedEffect | rememberCoroutineScope | |
|---|---|---|
| 启动方式 | 自动 | 手动 |
| 触发时机 | Composable进入/Key变化 | 点击等事件 |
| 使用方式 | 直接写代码块 | scope.launch |
| 生命周期 | Compose管理 | Compose管理 |
| 常见场景 | 初始化请求 | 用户操作 |
// 页面加载
LaunchedEffect(Unit){
viewModel.loadData()
}
// 点击刷新
// 获取协程作用域
val scope = rememberCoroutineScope()
Button(
onClick = {
scope.launch {
viewModel.refresh()
}
}
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# HorizontalPager
左右分页 / 轮播图
| 属性 / 方法 | 类型 | 作用 | 示例 | 使用场景 |
|---|---|---|---|---|
rememberPagerState | State对象 | 保存 HorizontalPager 的状态 | val pagerState = rememberPagerState() | 管理轮播图、分页状态 |
pageCount | Int | 总页数(只读) | pageCount = { banners.size } | 告诉 Pager 有多少页 |
initialPage | Int | 初始显示第几页,默认 0 | initialPage = 1 | 打开页面默认展示指定页 |
currentPage | Int | 当前正在显示的页面索引,从0开始 | pagerState.currentPage | 获取当前页码 |
targetPage | Int | 用户滑动时准备到达的页面 | pagerState.targetPage | 判断滑动目标 |
isScrollInProgress | Boolean | 当前是否正在滚动 | pagerState.isScrollInProgress | 控制加载动画、指示器显示 |
自动无线轮播完整版本
利用超大虚拟页数 Int.MAX_VALUE,从中间位置开始滑动; 通过取模运算 page % realCount,把巨大的 page 映射到真实数据下标,实现无限左右滑动的视觉效果。
@Composable
fun InfiniteBannerDemo() {
// 真实轮播数据源
val colors = listOf(
Color(0xFFE91E63), // 粉色
Color(0xFF2196F3), // 蓝色
Color(0xFF4CAF50) // 绿色
)
val realCount = colors.size
if (realCount == 0) return
// 虚拟页数:Int最大值,模拟无限
val virtualCount = Int.MAX_VALUE
// 起始页面放在中间,保证左右都能滑动很多页
val startIndex = virtualCount / 2
// 创建Pager状态
val pagerState = rememberPagerState(
initialPage = startIndex,
pageCount = { virtualCount }
)
// 自动轮播:3秒自动翻页
LaunchedEffect(pagerState) {
while (true) {
delay(3000.milliseconds)
pagerState.animateScrollToPage(pagerState.currentPage + 1)
}
}
// 当前真实下标,用于底部指示器
val realIndex = pagerState.currentPage % realCount
Box(modifier = Modifier.fillMaxSize()) {
HorizontalPager(state = pagerState) { page ->
// 核心:虚拟page对真实数量取模,拿到真实索引
val index = page % realCount
Box(
modifier = Modifier
.fillMaxSize()
.background(colors[index]),
contentAlignment = Alignment.Center
) {
Text(
text = "第${index + 1}页",
fontSize = 24.sp,
color = Color.White
)
}
}
// 底部圆点指示器
Row(
modifier = Modifier
.align(Alignment.BottomCenter)
.padding(bottom = 8.dp),
horizontalArrangement = Arrangement.Center
) {
repeat(realCount) { index ->
Box(
modifier = Modifier
.size(8.dp)
.clip(RoundedCornerShape(4.dp))
.background(
if (realIndex == index) Color.White
else Color.White.copy(alpha = 0.5f)
)
)
Spacer(modifier = Modifier.width(4.dp))
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| 代码 | 作用 | 关键点 |
|---|---|---|
virtualCount = Int.MAX_VALUE | 虚拟总页数,制造无限滑动效果 | 并不是真的无限,只是数字极大,用户几乎滑不到边界 |
startIndex = virtualCount / 2 | 起始页面放在中间 | 保证向左、向右都可以滑动大量页面,不会很快到头 |
page % realCount | 虚拟页码映射真实数据下标 | 写在 HorizontalPager 内部的 page 作用域,每个页面单独计算 |
realIndex = pagerState.currentPage % realCount | 当前激活页真实索引 | 放在 Pager 外部,专门给底部指示器圆点判断选中状态 |
LaunchedEffect(pagerState) | 自动轮播 | while (true) 循环 + delay,3s 自动滚动到下一页;key 绑定 pagerState,页面销毁自动取消协程 |
# Material3页面框架
# Scaffold
顶部标题栏 页面内容 底部导航 悬浮按钮
Scaffold(
topBar = {
TopAppBar(
title = { Text("面试通", fontWeight = FontWeight.Bold) },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFF1A73E8), // 背景色
titleContentColor = Color.White
)
)
}
) { paddingValues ->
Column(
modifier = Modifier.padding(paddingValues)
) {
Text("页面内容")
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# MaterialTheme
Compose 通常结合 Material Design 使用。
MaterialTheme {
Text("Hello")
}
2
3
常见结构
setContent {
MyApplicationTheme {
MyPage()
}
}
2
3
4
5
SecondaryScrollableTabRow 分类 Tab
ModalBottomSheet 底部弹窗
@Composable
fun FilterSheet(
onDismiss:()->Unit
){
var selectedSort by remember {
mutableStateOf("默认")
}
ModalBottomSheet(
onDismissRequest = onDismiss
){
Column(
modifier =
Modifier
.padding(16.dp)
.height(300.dp)
){
Row(
modifier =
Modifier.fillMaxWidth(),
verticalAlignment =
Alignment.CenterVertically
){
Text(
"重置",
modifier =
Modifier.clickable {
selectedSort="默认"
}
)
Text(
"筛选题目",
fontWeight =
FontWeight.Bold
)
}
Text(
"当前排序:$selectedSort"
)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# Navigation
添加依赖
implementation(
"androidx.navigation:navigation-compose"
)
2
3
NavController + NavHost
NavController负责控制页面跳转,NavHost负责存放和展示页面。
@Composable
fun NavDemo() {
// 创建并记住 NavController
val navController = rememberNavController()
// 监听当前页面变化,页面切换时自动更新
val navBackStackEntry by navController.currentBackStackEntryAsState()
// 当前页面的路由名
val currentRoute = navBackStackEntry?.destination?.route
NavHost(
navController = navController,
startDestination = Routes.HOME // 启动后首先显示首页
) {
// 注册页面(下一步实现)
composable(
Routes.LOGIN
){
LoginScreen()
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
携带参数跳转
@Composable
fun QuestionPage(
navController: NavController,
itemId: String?
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(color = Color.Blue)
.padding(top = 30.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("问题详情页 ID=${itemId}", fontSize = 25.sp)
Button(onClick = {
navController.popBackStack()
}) {
Text("返回")
}
}
}
composable(Routes.QUESTION) { navBackStackEntry ->
// 获取路由传递的参数
val itemId = navBackStackEntry.arguments?.getString(key = "itemId")
QuestionPage(navController, itemId)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27