Jetpack Compose 入门(翻译)

  • jetpack compose 官方入门翻译 聊胜于无而已

  • 资料来源:

    https://developer.android.com/jetpack/compose/tutorial

  • 更新

    1
    2022.04.20 初始

导语

nowakelock 的重构一直进展缓慢,许多想法设计上已经成型,但是实现上困难远超预估..似乎今年到现在所有的预估都失效了,如同牢笼,锁链无法挣脱…扯远了…

计划中是以 jetpack compose 重构界面,减小以后维护的负担.

这一篇是官方入门文档的非完全翻译,当然相关资料已经有很多了,因此聊胜于无而已.

jetpack compose

jetpack compose 真正在 android 上实现了代码定义 UI,而不是 xml 等一堆复杂的元素+声明.

  • 官方介绍优点是 全面兼容,代码更少,更强大的描述能力….
  • 每个新技术都是如此,要不然怎么拿出来卖呢..
  • 个人直观感觉,纯维护界面的成本比原来 nowakelock 可能少了 1/3 左右,因此入坑.

L1 compose 函数

整个 compose 都是围绕这可组合的函数建立的,这些函数让你可以以编程方式描述用户界面,并将数据绑定到 UI,而不是像传统的方式一样(定义一个元素,添加到父布局…等等),任何 compose 函数都由 @Composable 修饰.

添加一个文本元素

下载安装 Android studio 最新版,开始创建 project

  • New Project
  • Phone and Tablet 下选择 Empty Compose Activity
  • 输入工程名教程内是 ComposeTutorial,点击 Finish

现在工程内已经有了一些代码,但我们会一步步重新实现它们.

显示 Hello world!

  • 在 Activity -> onCreat 方法添加 setContent 方法块,调用 Composable 函数完成渲染 UI.
  • 任何 @Composable 修饰的函数都只能在其他 Composable 函数中调用(有些类似协程)
  • 最终 Jetpack Compose 会使用一个 Kotlin 编译器插件,将 Composable 函数转换到 UI.这里示例中就是 Text 最终转化为了 UI 的文本.
1
2
3
4
5
6
7
8
9
10
11
12
13
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material.Text

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Text("Hello world!")
}
}
}
  • Hello world

定义一个 composable 函数

定义一个 composable 函数,只需要在普通函数基础上使用 @Composable 修饰,现在参数将 Hello World 示例中 Text 拆分到独立的 composable 函数中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ...
import androidx.compose.runtime.Composable

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MessageCard("Android")
}
}
}

@Composable
fun MessageCard(name: String) {
Text(text = "Hello $name!")
}

android studio 中预览 jetpack compose

没有了 xml 的束缚,jetpack compose 非常自由,但是预览 UI 成了一个麻烦.

@Preview 注解使得可以在 AS 直接预览 composable 函数效果,而不需要构建并安装应用.

这个注解修饰的函数不能携带任何参数,预览带参数的 composable 函数,需要再包装一层.

1
2
3
4
5
@Preview
@Composable
fun PreviewMessageCard() {
MessageCard("Android")
}

Preview

L2 布局

UI 元素是分层次的,元素包含在其他元素中.JC 中是层层调用 composable 函数实现这样的布局.

lesson2-01.svg

添加多组文本

这一节我们将实现一个简单的消息屏幕,包含一些动画效果的消息列表.

定义数据类 Message 包含作者名和描述.同时在 MessageCard 函数中添加两个文本.

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
// ...

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MessageCard(Message("Android", "Jetpack Compose"))
}
}
}

data class Message(val author: String, val body: String)

@Composable
fun MessageCard(msg: Message) {
Text(text = msg.author)
Text(text = msg.body)
}

@Preview
@Composable
fun PreviewMessageCard() {
MessageCard(
msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
)
}

没有父布局情况下,不出所料,两个文本互相覆盖.
lesson2-02

使用 Column

Column 函数可以让子元素垂直排列,同样的 Row 是水平排列,Box 是直接堆叠.

1
2
3
4
5
6
7
8
9
import androidx.compose.foundation.layout.Column

@Composable
fun MessageCard(msg: Message) {
Column {
Text(text = msg.author)
Text(text = msg.body)
}
}

添加图片元素

添加发信人照片丰富留言卡,使用 Resource Manager 或者导入 这个图片 .

使用 Row 作为父布局,Image显示图像排列文本.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Row
import androidx.compose.ui.res.painterResource

@Composable
fun MessageCard(msg: Message) {
Row {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = "Contact profile picture",
)

Column {
Text(text = msg.author)
Text(text = msg.body)
}

}

}

设置布局

上面的布局的结构对了,但元素的间隔并不是很好,图片太大了.

Composs 使用 modifiers 来完成上述调整.

  • 改变 composable 元素的尺寸 布局 外观
  • 添加高级交互等(例如点击事件)
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
// ...
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp

@Composable
fun MessageCard(msg: Message) {
// Add padding around our message
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = "Contact profile picture",
modifier = Modifier
// Set image size to 40 dp
.size(40.dp)
// Clip image to be shaped as a circle
.clip(CircleShape)
)

// Add a horizontal space between the image and the column
Spacer(modifier = Modifier.width(8.dp))

Column {
Text(text = msg.author)
// Add a vertical space between the author and message texts
Spacer(modifier = Modifier.height(4.dp))
Text(text = msg.body)
}
}
}

Material Design

在 compose 中许多 Material Design 元素都是开箱即用,这一节是使用 MD 组件修改上述布局.

使用 Material Design

现在信息卡片有了一个布局,但看起来并不是太好.

Jetpack Compose 提供了 MD 的实现和开箱即用的体验,我们将使用 MD 风格改善现有涉及.

使用 ComposeTutorialTheme 包装 MessageCard 函数,ComposeTutorialTheme 是项目创建的 Material 主题.同时也别忘记了在 @PreviewsetContent 也进行同样的动作,这样会确保无论是预览还是应用运行中应用主题的一致性.

Material Design 的三个支柱: 颜色 排版 和 形状.接下来就逐一添加.

注: AS 的 Empty Compose Activity 模板会生成一个默认主题,如果你的应用命名不是 ComposeTutorial,那么你可以在 ui.theme 子包的 Theme.kt 文件中找到默认生成的主题名.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// ...

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTutorialTheme {
MessageCard(Message("Android", "Jetpack Compose"))
}
}
}
}

@Preview
@Composable
fun PreviewMessageCard() {
ComposeTutorialTheme {
MessageCard(
msg = Message("Colleague", "Take a look at Jetpack Compose, it's great!")
)
}
}

颜色

使用 MaterialTheme.colors 可以使用已经主题中已经定义好的颜色,可以在任何需要使用颜色的地方引用.

下面对标题修改样式,为 image 添加一个边界.

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
// ...
import androidx.compose.foundation.border
import androidx.compose.material.MaterialTheme

@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
)

Spacer(modifier = Modifier.width(8.dp))

Column {
Text(
text = msg.author,
color = MaterialTheme.colors.secondaryVariant
)

Spacer(modifier = Modifier.height(4.dp))
Text(text = msg.body)
}
}
}

排版

Material 排版在 MaterialTheme 中生效,只需要添加它们即可.

下面是为文本添加一个排版.

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
// ...

@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))

Column {
Text(
text = msg.author,
color = MaterialTheme.colors.secondaryVariant,
style = MaterialTheme.typography.subtitle2
)

Spacer(modifier = Modifier.height(4.dp))

Text(
text = msg.body,
style = MaterialTheme.typography.body2
)
}
}
}

形状

使用 Surface composable 包裹消息体的 Text,这样可以在 Surface 中自定义消息体的形状高度,同时添加填充,以调整元素之间布局.

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
// ...
import androidx.compose.material.Surface

@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colors.secondary, CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))

Column {
Text(
text = msg.author,
color = MaterialTheme.colors.secondaryVariant,
style = MaterialTheme.typography.subtitle2
)

Spacer(modifier = Modifier.height(4.dp))

Surface(shape = MaterialTheme.shapes.medium, elevation = 1.dp) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
style = MaterialTheme.typography.body2
)
}
}
}
}

启用暗色主题

Jectpack Compose 默认支持暗色主题,使用了 MD 的颜色文本和背景都将自动适配暗色主题.

在 Compose 预览界面可以声明多个 @Preview 以预览暗色模式效果.

  • Composable 函数和 @Preview,可以是一对多,也可以是一对一.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...
import android.content.res.Configuration

@Preview(name = "Light Mode")
@Preview(
uiMode = Configuration.UI_MODE_NIGHT_YES,
showBackground = true,
name = "Dark Mode"
)
@Composable
fun PreviewMessageCard() {
ComposeTutorialTheme {
MessageCard(
msg = Message("Colleague", "Hey, take a look at Jetpack Compose, it's great!")
)
}
}

暗色模式颜色的选取定义在了 IDE 自动生成的 Theme.kt 文件中了.

截至现在,信息卡片在暗色和明亮主题显示都还不错.

L4 列表和动画

列表和动画在应用内无处不在,这一节将学习如何使用 Compose 创建 list 和添加动画效果.

为信息卡片创建列表

现在我们来创建能够显示多条 Message 的 list.使用 LazyColumnLazyRow 完成这项工作.这些 composables 函数仅渲染在 UI 正在显示的元素,非常高效.

在下面的代码中, LazyColumn 有一个子元素 items,其接收一个 list 类型的元素作为参数,lambda 部分为 list 每一个子项创建卡片.

示例的 Message list 数据 -> sample dataset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ...
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items

@Composable
fun Conversation(messages: List<Message>) {
LazyColumn {
items(messages) { message ->
MessageCard(message)
}
}
}

@Preview
@Composable
fun PreviewConversation() {
ComposeTutorialTheme {
Conversation(SampleData.conversationSample)
}
}

为信息拓展添加动画

信息卡片下的信息总有文本量太大,需要点击才能查看全部的时候,这一节我们将为这个过程添加适当的动画效果.

为了标记当前文本处于缩略还是拓展显示状态,需要追踪这个状态的变化,为此需要使用 remembermutableStateOf.

remember 可以将当前文本的状态存储在内存中,mutableStateOf 可以跟踪值的变化.当值被更新时会重绘受影响的 UI.

下面的代码中 isExpanded 是标记文本拓展的标志位.Column 绑定了点击取反.而当 isExpanded 值变化时,除法下面 TextmaxLines 属性重取,进行 UI 的重绘.

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// ...
import androidx.compose.foundation.clickable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeTutorialTheme {
Conversation(SampleData.conversationSample)
}
}
}
}

@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))

// We keep track if the message is expanded or not in this
// variable
var isExpanded by remember { mutableStateOf(false) }

// We toggle the isExpanded variable when we click on this Column
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
Text(
text = msg.author,
color = MaterialTheme.colors.secondaryVariant,
style = MaterialTheme.typography.subtitle2
)

Spacer(modifier = Modifier.height(4.dp))

Surface(
shape = MaterialTheme.shapes.medium,
elevation = 1.dp,
) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
// If the message is expanded, we display all its content
// otherwise we only display the first line
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
style = MaterialTheme.typography.body2
)
}
}
}
}
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
44
45
46
47
48
49
50
51
52
53
54
55
// ...
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize

@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))

// We keep track if the message is expanded or not in this
// variable
var isExpanded by remember { mutableStateOf(false) }
// surfaceColor will be updated gradually from one color to the other
val surfaceColor by animateColorAsState(
if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface,
)

// We toggle the isExpanded variable when we click on this Column
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
Text(
text = msg.author,
color = MaterialTheme.colors.secondaryVariant,
style = MaterialTheme.typography.subtitle2
)

Spacer(modifier = Modifier.height(4.dp))

Surface(
shape = MaterialTheme.shapes.medium,
elevation = 1.dp,
// surfaceColor color will be changing gradually from primary to surface
color = surfaceColor,
// animateContentSize will change the Surface size gradually
modifier = Modifier.animateContentSize().padding(1.dp)
) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
// If the message is expanded, we display all its content
// otherwise we only display the first line
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
style = MaterialTheme.typography.body2
)
}
}
}
}

上面的代码实现了点击拓展文本框,整个响应当然可以更加复杂而不只是修改一个 maxLines 而已.

下面的代码实现了文本块尺寸和 Surface 背景颜色的将近修改(不是粗暴的一次到位).

  • 对应 animateContentSizeanimateColorAsState
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
44
45
46
47
48
49
50
51
52
53
54
55
// ...
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize

@Composable
fun MessageCard(msg: Message) {
Row(modifier = Modifier.padding(all = 8.dp)) {
Image(
painter = painterResource(R.drawable.profile_picture),
contentDescription = null,
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.border(1.5.dp, MaterialTheme.colors.secondaryVariant, CircleShape)
)
Spacer(modifier = Modifier.width(8.dp))

// We keep track if the message is expanded or not in this
// variable
var isExpanded by remember { mutableStateOf(false) }
// surfaceColor will be updated gradually from one color to the other
val surfaceColor by animateColorAsState(
if (isExpanded) MaterialTheme.colors.primary else MaterialTheme.colors.surface,
)

// We toggle the isExpanded variable when we click on this Column
Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
Text(
text = msg.author,
color = MaterialTheme.colors.secondaryVariant,
style = MaterialTheme.typography.subtitle2
)

Spacer(modifier = Modifier.height(4.dp))

Surface(
shape = MaterialTheme.shapes.medium,
elevation = 1.dp,
// surfaceColor color will be changing gradually from primary to surface
color = surfaceColor,
// animateContentSize will change the Surface size gradually
modifier = Modifier.animateContentSize().padding(1.dp)
) {
Text(
text = msg.body,
modifier = Modifier.padding(all = 4.dp),
// If the message is expanded, we display all its content
// otherwise we only display the first line
maxLines = if (isExpanded) Int.MAX_VALUE else 1,
style = MaterialTheme.typography.body2
)
}
}
}
}

其他

jetpack compose 初步的速览就是这样了,更以往写 android UI 的思路完全不同,还在适应中.

逢乱世,愈加迷茫,唯一能做的尽量提高效率,活下来.