笔记
入门教程: https://zhuanlan.zhihu.com/p/433581686
官网:Android Compose导读、具体教程
- Jetpack Compose:是个利用“声明式编程”构建Android原生界面(UI)的工具包
- 环境要求:
ComposeApp
仅支持Kotlin
;- 最低
sdk
版本为21(Android5.0); - 可用Android Studio(2020-3-1以上)、Idea
jdk11
以上
- 用
@Compose
注解的方法,只能被@Compose
注解的方法中调用 Modifier
:设置UI位置
、padding
等Modifier.plus(otherModifier)
//把其他的modifier加入到当前的Modifier中fillMaxHeight
(填充整个高度)、fillMaxWidth
、fillMaxSize
类似于 match_patch、填充整个父布局
Jetpack-Compose
快速上手
Compose是什么
Jetpack Compose:利用声明式编程构建Android原生界面(UI)的 工具包
Compose的优点
- 更少的代码、代码量锐减
- 强大的工具/组件支持
- 直观的 Kotlin API
- 简单易用
可组合函数
预览
布局
配置布局
Material Design
围绕Color
、Typography
(排版)、Shape
(形状)这三大要素构建的。
列表和动画
布局
标准布局组件
@Compose
所有关于构建View的方法都必须添加@Compose
注解才可以。并且@Compose
协程的Suspend
的使用方法比较类似,被@Compose
注解的方法只能在同样被@Comopse
解的方法中才能被调用。
1 |
|
预览@Preview
@Preview
注解的方法可以在不运行App的情况下就可以确认布局的情况。
1 |
|
常用的参数:
name
: String: 为该Preview命名,该名字会在布局预览中显示。showBackground
: Boolean: 是否显示背景,true为显示。backgroundColor
: Long: 设置背景的颜色。showDecoration
: Boolean: 是否显示Statusbar和Toolbar,true为显示。group
: String: 为该Preview设置group名字,可以在UI中以group为单位显示。fontScale
: Float: 可以在预览中对字体放大,范围是从0.01。widthDp
: Int: 在Compose中渲染的最大宽度,单位为dp。heightDp
: Int: 在Compose中渲染的最大高度,单位为dp。
上面的参数都是可选参数,还有像背景设置等的参数并不是对实际的App进行设置,只是对Preview中的背景进行设置,为了更容易看清布局。setContent
setContent的作用是和Layout/View中的setContentView是一样的。
setContent的方法也是有@Compose注解的方法。所以,在setContent中写入关于UI的@Compopse方法,即可在Activity中显示。1
2
3
4
5
6
7
8
9
10
11override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyJetpackComposeTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
Greeting("Android")
}
}
}
}主题Theme
在创建新的Compose项目时会自动创建一个Theme.kt文件。 我们可以通过更改颜色来完成对主题颜色的设置。1
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
44package com.zm.myjetpackcompose.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material.MaterialTheme
import androidx.compose.material.darkColors
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
private val DarkColorPalette = darkColors(
primary = Purple200,
primaryVariant = Purple700,
secondary = Teal200
)
private val LightColorPalette = lightColors(
primary = Purple500,
primaryVariant = Purple700,
secondary = Teal200
/* Other default colors to override
background = Color.White,
surface = Color.White,
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
*/
)
fun MyJetpackComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: () -> Unit) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}
MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}Modifier
Modifier是各个Compose的UI组件一定会用到的一个类。它是被用于设置UI的摆放位置,padding等信息的类。padding
设置各个UI的padding1
2
3
4Modifier.padding(10.dp) // 给上下左右设置成同一个值
Modifier.padding(10.dp, 11.dp, 12.dp, 13.dp) // 分别为上下左右设值
Modifier.padding(10.dp, 11.dp) // 分别为上下和左右设值
Modifier.padding(InnerPadding(10.dp, 11.dp, 12.dp, 13.dp))// 分别为上下左右设值plus
可以把其他的Modifier加入到当前的Modifier中。1
Modifier.plus(otherModifier) // 把otherModifier的信息加入到现有的modifier中
fillMaxHeight、fillMaxWidth、fillMaxSize
类似于match_parent、填充整个父layout。1
Modifier.fillMaxHeight() // 填充整个高度
width、heigh、size
设置Content的宽度和高度。1
2
3Modifier.width(2.dp) // 设置宽度
Modifier.height(3.dp) // 设置高度
Modifier.size(4.dp, 5.dp) // 设置高度和宽度 复制代码widthIn、heightIn、sizeIn
设置Content的宽度和高度的最大值和最小值。1
2
3Modifier.widthIn(2.dp) // 设置最大宽度
Modifier.heightIn(3.dp) // 设置最大高度
Modifier.sizeIn(4.dp, 5.dp, 6.dp, 7.dp) // 设置最大最小的宽度和高度gravity
在Column中元素的位置。1
2
3Modifier.gravity(Alignment.CenterHorizontally) // 横向居中
Modifier.gravity(Alignment.Start) // 横向居左
Modifier.gravity(Alignment.End) // 横向居右rtl、ltr
开始布局UI的方向。1
2
3
4
5
6
7
8
9Modifier.rtl // 从右到左
Modifier.ltr // 从左到右
// Modifier的方法都返回Modifier的实例的链式调用,所以只要连续调用想要使用的方法即可。
fun Greeting(name: String) {
Text(text = "Hello $name!", modifier = Modifier.padding(20.dp).fillMaxSize())
}
固有特性测量Modifier
Compose 有一项规则,即,子项只能测量一次,测量两次就会引发运行时异常。但是,有时需要先收集一些关于子项的信息,然后再测量子项。
借助固有特性,您可以先查询子项,然后再进行实际测量。
对于可组合项,您可以查询其 IntrinsicSize.Min
或 IntrinsicSize.Max
:
Modifier.width(IntrinsicSize.Min)
- 需要多大的最小宽度才能正确显示内容?Modifier.width(IntrinsicSize.Max)
- 您需要多大的最大宽度才能正确显示内容?Modifier.height(IntrinsicSize.Min)
- 需要多高的最小高度才能正确显示内容?
(在父组件加)可将其子项的高度强行调整为最小固有高度。Modifier.height(IntrinsicSize.Max)
- 您需要多高的最大高度才能正确显示内容?
线性布局Column,Row
Column 线性布局 ≈ Android LinearLayout-VERTICAL
Row 水平布局 ≈ Android LinearLayout-HORIZONTAL
Column
和Row
可以理解为在View/Layout
体系中的纵向和横向的ViewGroup
。
Modifier
用上述的方法传入已经按需求设置好的Modifier即可。Arrangement.Horizontal
,Arrangement.Vertical
需要给Row
传入Arrangement.Horizontal
,为Column
传入Arrangement.Vertical
。 这些值决定如何布置内部UI组件。
可传入的值为Center
,Start
,End
,SpaceEvenly
,SpaceBetween
,SpaceAround
。Alignment.Vertical
,Alignment.Horizontal
需要给Row
传入Alignment.Vertical
,为Column
传入Alignment.Horizontal
。 使用方法和Modifier
的gravity
中传入参数的用法是一样的.@Composable ColumnScope.() -> Unit
需要传入标有@Compose
的UI方法。但是这里我们会有lamda函数的写法来实现。1
2
3
4
5Column {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceAround, verticalAlignment = Alignment.CenterVertically) {
Text(text = "Hello $name!")
}
}
Row
,Arrangement
的值和对应效果如下:
帧布局Box
帧布局 ≈ Android FrameLayout
,可将一个元素放在另一个元素上,如需在 Row
中设置子项的位置,请设置 horizontalArrangement
和 verticalAlignment
参数。对于 Column
,请设置 verticalArrangement
和 horizontalAlignment
参数 。
寻呼机【分页器】HorizontalPager/VerticalPager
左右翻页:HorizontalPager
(默认宽度满屏,默认一次翻一页,可配置)
上下翻页:VerticalPager
(默认高度满屏,默认一次翻一页,可配置)
这些可组合项的功能与 View 系统中的 ViewPager
类似。
流式布局FlowRow
、FlowColumn
自定义版式
在界面树中布置每个节点的过程分为三个步骤。每个节点必须:
- 测量所有子项
- 确定自己的尺寸
- 放置其子项
[!注意]
注意:Compose 界面不允许多遍测量。这意味着,布局元素不能为了尝试不同的测量配置而多次测量任何子元素。
自适应布局
Feed 布局能够以可配置网格的形式排列等效的内容元素,以便用户快速、方便地查看大量内容。
对齐线AlignmentLine
ConstraintLayout
在实现对齐要求比较复杂的较大布局时,很实用。
需要引入
1 | implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1" |
在View系统中是建议用ConstraintLayout来创建复杂的大型布局,因为扁平视图
修饰符
修饰符的顺序
Slots API
TopAppBar
Scaffold
是个页面脚手架。可以为最常见的顶级 Material 组件(如 TopAppBar
、BottomAppBar
、FloatingActionButton
和 Drawer
)提供槽位。通过使用 Scaffold
,可轻松确保这些组件得到适当放置且正确地协同工作。
使用列表LazyColumn/LazyRow
- 可以滚动的布局
1
2
3
4
5
6
7
8// 我们可以使用 verticalScroll() 修饰符使 Column 可滚动,但以上布局并无法实现重用,可能导致性能问题
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
messages.forEach { message ->
MessageRow(message)
}
} LazyColumn/LazyRow == RecylerView/listView
列表布局,解决了滚动时的性能问题,LazyColumn和LazyRow之间的区别就在于它们的列表项布局和滚动方向不同。- 内边距
1
2
3
4
5LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
) {
// ...
} - item间距
1
2
3
4
5LazyColumn(
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
// ...
} - 浮动列表的浮动标题,使用 LazyColumn 实现粘性标题,可以使用stickyHeader()函数
1
2
3
4
5
6
7
8
9
10
11
12
fun ListWithHeader(items: List<Item>) {
LazyColumn {
stickyHeader {
Header()
}
items(items) { item ->
ItemRow(item)
}
}
} - 网格布局LazyVerticalGrid
1
2
3
4
5
6
7
8
9
10
fun PhotoGrid(photos: List<Photo>) {
LazyVerticalGrid(
cells = GridCells.Adaptive(minSize = 128.dp)
) {
items(photos) { photo ->
PhotoItem(photo)
}
}
}
自定义布局
通过重组基础布局实现
Canvas绘制
1 |
|
firstBaselineToTop
MyOwnColumn
StaggeredGrid
约束布局
引用
约束条件
解耦API
Intrinsics
状态
什么是状态
与其它声明式 UI 框架一样,Compose 的职责非常单纯,仅作为对数据状态的反应。如果数据状态没有改变,则 UI 永远不会自行改变。在 Compose 中,每一个组件都是一个被 @Composable
修饰的函数,其状态就是函数的参数,当参数不变,则函数的输出就不会变,唯一的参数决定唯一输出。反言之,如果要让界面发生变化,则需要改变界面的状态,然后 Composable 响应这种变化。
Compose中的状态State
State
如同传统试图中,需要使用 StateFlow 或者 LiveData 将状态变量包装成一个可观察类型的对象。Compose 中也提供了可观察的状态类型,可变状态类型 MutableState 和 不可变状态类型 State。我们需要使用 State/MutableState 将状态变量包装起来,这样即可触发重组。更为方便的是,声明式 UI 框架中,不需要我们显式注册监听状态变化,框架自动实现了这一订阅关系。
1 |
|
我们使用了 mutableStateOf()
方法初始化了一个 MutableState 类型的状态变量,并传入默认值 0 ,使用的时候,需要调用 counter.value
。
再次运行,结果发现,点击按钮,计数器值还是没有变化。
和上一次不一样了,这次发现,点击按钮之后, Text(text = "${counter.value}")
有重新执行,即发生了重组,但是执行的时候,参数没有改变,依然是 0,其实这里涉及到一个重组作用域的概念,就是重组是有一个范围的,关于重组作用范围,稍后再讲。这里需要知道,发生了重组,Text(text = "${counter.value}")
有重新执行,那么 val counter: MutableState<Int> = mutableStateOf(0)
也有重新执行,相当于重组时,counter 被重新初始化了,并赋予了默认值 0 。所以点击按钮发生了重组,但是计数器的值没有发生改变。要解决这个问题,则需要使用到 Compose 中的一个重要函数 remember
。
remember
remember 函数的源码:
1 | /** |
remember 方法的作用是,对其包裹起来的变量值进行缓存,后续发生重组过程中,不会重新初始化,而是直接从缓存中取。具体使用如下:
1 |
|
再次运行,这次终于正常了。
上面的代码中,我们创建 State 的方法如下:
1 | val counter: MutableState = remember { mutableStateOf(0) } |
使用时,通过 counter.value
来使用,这样的代码看起来就很繁琐,我们可以进一步精简写法。 首先, Kotlin 支持类型推导,所以可以写成下面这样:
1 | val counter = remember { mutableStateOf(0) } |
另外,借助于 Kotlin 委托语法,Compose 实现了委托方式赋值,使用 by
关键字即可,用法如下:
1 | var counter by remember { mutableStateOf(0) } |
并导入如下方法:
1 | import androidx.compose.runtime.getValue |
在使用时,直接使用 counter++
和 counter--
,需要注意的一点是,没有使用委托方式创建的对象,类型是 MutableState
类型,我们用 val
声明,使用委托方式创建对象,对象类型是 MutableState 包装的对象类型,这里由于赋初始值为 0 ,根据类型推导,counter 就是 Int
型,由于要修改 counter 的值,所以须使用 var
将其声明为一个可变类型对象。
rememberSaveable
1 | var counter by rememberSaveable { mutableStateOf(0) } |
用法与 remember 方法用法类似,区别在于,rememberSaveable 在当 Activity 销毁重建时,状态值不会重新初始化(如:横竖屏旋转,UiMode 切换等场景中),能够对其包裹的数据进行缓存。那是否说明 rememberSaveable 可以在所有的场景替换 remember , remember 方法就没用了? rememberSaveable 方法比 remember 方法功能更强劲,代价就是性能要差一些,具体使用根据实际场景来选择。
Parcelize
MapSaver
ListSaver
UI更新循环
非结构化状态
单一信息源(Single Source of Truth, SSOT)
在 Jetpack Compose 中,单一信息源的核心理念是:
UI 应完全由一个统一的状态驱动,而这个状态应该存放在一个明确的地方(通常是 ViewModel 或 remember
状态对象中)。
例如,一个按钮的点击次数应该只存储在一个地方,而不是分别在按钮和显示组件中重复存储。
1 | //反例 |
好处:
- 数据一致性:
状态存储在一个地方,UI 和逻辑依赖同一个数据来源,避免了因重复存储导致的数据冲突。 - 更易调试:
如果状态有问题,只需要检查单一信息源,而不用逐一排查各个组件。 - 降低复杂性:
数据集中管理,避免在多个地方维护相同的状态逻辑。 - 增强可扩展性:
当需求增加时,你只需在单一信息源中修改或扩展逻辑,而不需要更新多个地方的状态代码。
单向数据流
单向数据流(UDF) 是一种数据流动模式,强调 “数据向下流动,事件向上传递” 。在 Compose 中,这意味着状态由父组件管理并传递给子组件,而子组件通过回调通知父组件更新状态。
1 | //反例--状态由子组件直接管理 |
好处:
- 逻辑清晰:状态只从一个地方流向 UI,事件只回到状态来源,这样的数据流向使得应用的逻辑非常直观。
- 易于维护:所有状态更新都集中管理,减少了因状态散落在多个地方而带来的调试难度。
- 组件的复用性更高:子组件无需管理状态,只负责展示数据,因而可以在多个场景中重复使用,而不需要关心不同的状态逻辑。
无状态组件
stateless 是指这个组件除了依赖参数以外,不依赖其他任何状态。比如 Text
组件
1 | Text("Hello, Compose") |
Stateless不依赖外部状态,仅依赖传入进来的参数,是一个“纯函数”,即唯一输入对应唯一输出。就是参数不变UI也不变,它的重组只能是来自上层的调用,因此 Compose 编译器对其进行了优化,当 Stateless 的参数没有变化时,它就不会参与重组,重组的范围局限于 Stateless 外部。另外 Stateless 不耦合任何业务,功能更纯粹,所以复用性更好,更易于测试。
有状态组件
stateful 除了依赖参数外还持有或访问了外部的状态。比如 TextField
组件
1 | var text by remember { mutableStateOf("文本框初始值")} |
状态提升
stateful 组件改造成 stateless 组件的过程称为状态上提。
通常做法:将内部状态移除,以参数的形式传入。需要回调给调用方的事件,也以参数形式传入。
例子
1 |
|
不适合使用状态提升:
例如,当状态只与特定子组件有关且不会被其他组件使用时,可以考虑将状态保留在子组件中。
状态管理
使用 stateful 管理状态
简单的的 UI 状态,且与业务无关的状态,适合在 Compose 中直接管理。
比如我有一个菜单列表,点一开关,展开一个菜单,再点一下,收起菜单,列表的状态,仅由点击开关这一单一事件决定。并且,列表的状态与任何外部业务无关。那么这种就适合在 Compose 内部进行管理。
使用 StateHolder 管理状态
当业务有一定的复杂度之后,我们可以将业务逻辑相关的状态统一封装到一个 StateHoler 进行管理。剥离 Ui 逻辑,让 Composable 专注 UI 布局。
使用 ViewModel 管理状态
从某种意义上讲,ViewModel 也是一种特殊的 StateHolde。但因为它是保存在 ViewModelStore 中,所以有以下特点:
- 存活范围大,可以脱离 Composition 存在,被所有 Composable 共享。
- 存活时间长,不会因为横竖屏切换或者 UiMode 切换导致数据丢失。
因此,ViewModel 适合管理应用程序全局状态,而且 ViewModel更倾向于管理哪些非 UI 的业务状态。
以上管理方式可以同时使用,结合具体的业务灵活搭配。
LiveData、Rxjava、Flow 转 State
在 MVVM 架构中,使用 ViewModel 来管理状态,如果是新项目,把状态直接定义 State 类型就可以了。
对于传统试图项目,一般使用 LiveData、Rxjava 或者 Flow 这类响应式数据框架。而在 Compose 中需要 State 触发重组,刷新 UI,也有相应的方法,将上述响应式数据流转换为 Compose 中的 State。当上有数据变化时,可以驱动 Composable 完成重组。具体方法如下:
拓展方法 | 依赖库 |
---|---|
LiveData.observeAsState() |
androidx.compose:runtime-livedata |
Flow.collectAsState() |
不依赖三方库,Compose 自带 |
Observable.subscribeAsState() |
androidx.compose:runtime-rxjava2 或者 androidx.compose:runtime-rxjava3 |
重组
组件树
MutableState
ViewModel和状态
主题
Material Design
定义主题
使用颜色
处理文本
使用形状
CompositionLocal
简介
自定义CompositionLocal
替代方案
集成
从XML创建可组合
ViewModels & LiveData
Compose 中使用 View 控件
共用主题
手势
点击
滚动
滚动修饰符
可滚动的修饰符
嵌套滚动
拖动
滑动
多点触控
平移
缩放
旋转
动画
简单值动画
可见性动画
内容大小动画
多值动画
重复动画
手势动画
导航
集成导航
参数传递
深层链接
核心思想
编程思想
声明性编程范式
声明性的函数构建一个简单的界面组件,无需修改任何 XML 布局,也不需要使用布局编辑器,只需要调用 Jetpack Compose 函数来声明想要的元素,Compose 编译器即会完成后面的所有工作。
简单的可组合函数
1 |
|
声明性范式转变
在 Compose 的声明性方法中,微件相对无状态,并且不提供 setter 或 getter 函数。实际上,微件不会以对象形式提供。您可以通过调用带有不同参数的同一可组合函数来更新界面。这使得向架构模式(如 ViewModel)提供状态变得很容易,如应用架构指南中所述。然后,可组合项负责在每次可观察数据更新时将当前应用状态转换为界面。
动态内容
组合函数是用 Kotlin 而不是 XML 编写
重组
在 Compose 中,您可以使用新数据再次调用可组合函数。这样做会导致函数进行重组 – 系统会根据需要使用新数据重新绘制函数发出的微件。Compose 框架可以智能地仅重组已更改的组件。
- 可组合函数可以按任何顺序执行
- 可组合函数可以并行运行
- 重组会跳过尽可能多的内容
- 重组是乐观的操作
- 可组合函数可能会非常频繁地运行