Jetpack Compose

简介

Compose 是 Google 最新发布的用于构建原生 Android 界面的新工具包,经过两年的发展,其已于 2021 年 7月份发布正式版本 1.0.0。Compose 是基于响应式的编程模型,也就是说,开发者只需描述界面的外观,Compose 就会负责完成其余工作,界面会随着应用状态的变化而自动更新。

曾经实现一个功能是需要在 Activity 中编写 Java/Kotlin 的代码,在 xml 中编写布局代码。而 Compose 完全抛弃了之前的方式,新创了一种“组合函数”编写页面的方式,这就是响应式 UI。

从目前来看,Compose 与 Flutter 的发展趋势一样,都在往全平台的方向发展,即一套代码可同时运行在 Android、桌面的平台上。

基础知识

Compose 可通过简单的可组合函数来构建页面。由于可组合函数是用 Kotlin 而不是 XML 编写的,所以开发者可以像编写业务代码一样使用循环语句等动态地创建页面。综合来说,Compose 可以让开发者使用更少的代码编写应用页面,它凭借对 Android 平台 API 的直接访问和对 Material Design、深色主题、动画等功能的内置支持来加速应用开发。

配置支持 Compose 的应用

使用 Compose 需要使用 Android Studio 4.2 或更高的版本。 将 Jetpack Compose 添加到应用中,首先修改项目的 build.gradle:

build.gradle(LittleHelper)
1
2
3
4
5
6
7
8
9
10
11
12
buildscript {
// Compose 版本号
ext{
compose_version = '1.1.0-beta01'
}
// Kotlin 版本号
ext.kotlin_version = '1.7.0'
dependencies {
classpath "com.android.tools.build:gradle:7.2.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

然后修改模块下的 build.gradle:

build.gradle(:app)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
android {
...
// JDK 1.8 版本
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
// 启用 Compose
buildFeatures {
compose true
}
composeOptions{
kotlinCompilerVersion '1.7.0'
kotlinCompilerExtensionVersion '1.2.0'
}
namespace 'com.example.littlehelper'
}

添加相关依赖资源:

build.gradle(:app)
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
dependencies {
// Compose UI 等相关包的依赖资源
// Integration with activities
implementation 'androidx.activity:activity-compose:1.5.1'
// Compose Material Design
implementation 'androidx.compose.material:material:1.2.0'
// Animations
implementation 'androidx.compose.animation:animation:1.2.0'
// Tooling support (Previews, etc.)
implementation 'androidx.compose.ui:ui-tooling:1.2.0'
// Integration with ViewModels
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1'
// UI Tests
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.2.0'

// When using a MDC theme
implementation "com.google.android.material:compose-theme-adapter:1.1.15"
// When using a AppCompat theme
implementation "com.google.accompanist:accompanist-appcompat-theme:0.25.0"

implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.0")
implementation('androidx.core:core-ktx:1.6.0')
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'com.google.android.material:material:1.4.0'
implementation("androidx.compose.ui:ui:1.2.1")
implementation("androidx.compose.foundation:foundation:1.2.1")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.5.1")
}

使用 AndroidStudio 新建的项目就是如下所示 – 运行程序,会显示 Hello Android!

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
class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
Greeting("Android")
}
}
}
}

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

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MyApplicationTheme {
Greeting("Android")
}
}
}
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
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,
*/
)

@Composable
fun MyApplicationTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) {
DarkColorPalette
} else {
LightColorPalette
}

MaterialTheme(
colors = colors,
typography = Typography,
shapes = Shapes,
content = content
)
}
Color.kt
1
2
3
4
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
val Purple700 = Color(0xFF3700B3)
val Teal200 = Color(0xFF03DAC5)
Shape.kt
1
2
3
4
5
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
Type.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Set of Material typography styles to start with
val Typography = Typography(
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)

从 Activity 中可以看到,Greeting 方法接收了一个 name 参数,并使用了 @Composable 进行了注解。这样的函数是一个可组合函数,Compose 就是围绕可组合函数构建的。

可组合函数与常用注解

可组合函数是 Compose 的基本构建块,它是 Unit 的一种函数,用于描述界面中的某一部分,该函数接收输入并生成屏幕上显示的内容。

在上述 TestActivity 中,以 Greeting 方法为例:

1
2
3
4
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!")
}

Greeting 接收了一个 name 参数,所有的可组合函数都必须使用 @Composable 注解,编辑器会将带有此注解的函数转为页面。Greeting 中调用了 Text 函数,Text 函数也是一个可组合函数,Text 函数的作用是显示一个文本,类似于传统 View 下的 TextView 组件,但这里的 Text 与 TextView 是没有任何关系的。

在上述 TestActivity 中,除了 Greeting 方法外,还默认生成了一个 DefaultPreview 方法:

1
2
3
4
5
6
7
@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
MyApplicationTheme {
Greeting("Android")
}
}

DefaultPreview 除了使用 @Composable 注解外,还使用了 @Preview(showBackground = true) 注解。因为 Compose 要求可组合函数必须为任何参数提供默认值,所以无法直接对 Greeting 函数进行预览。通过 @Preview 注解就可以在布局中预览 Greeting 函数生成的视图。


基础组件的使用

同传统 View 一样,Compose 也包含了许多基础组件,如文本组件、图片组件以及布局组件。

文本组件和图片组件

文本组件

提供了基础的 BasicText 和 BasicTextField,它们是用于显示文字以及处理用户输入内容的主要函数。但 Android 平台中推荐使用更高级的 Text 和 TextField,因为它们是遵循 Material Design 准则的,所以展示出来的效果比较美观。

Text 函数构造方法如下:

Text.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Composable
fun Text(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current
) { ... }

上述代码中设置了 color、fontSize 等参数,假设现在需将文字大小设置为 20sp,文字颜色设置为红色,修改 Greeting 方法的代码即可:

1
2
3
4
@Composable
fun Greeting(name: String) {
Text(text = "Hello $name!", fontSize = 20.sp, color = Color.Red)
}

图片组件

新建一个 showImage 方法,添加注解,并在 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
class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
showImage()
}
}
}
}

/**
* 通过 painterResource 为 painter 设置图片资源
* contentDescription 是对图片的描述
*/
@Composable
fun showImage(){
Image(
painter = painterResource(id = R.drawable.back),
contentDescription = "this is a picture"
)
}
}

布局组件

提供了 Column 函数用于将元素垂直排列,Row 函数用于将元素水平排列,Box 函数用于将元素堆叠排列。

例如使用 Column 函数将两个 Text 组件垂直排列显示:

1
2
3
4
5
6
7
@Composable
fun Greeting(name: String) {
Column() {
Text(text = "Hello $name!", fontSize = 20.sp, color = Color.Red)
Text(text = "Hello $name!", fontSize = 20.sp, color = Color.Red)
}
}

常见的一个需求是,比如在新闻列表项目中,经常会用到左边为图片、右侧为标题与内容的布局,代码如下:

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
class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
News()
}
}
}
}

/**
* 定义一个可组合函数 News
*/
@Composable
fun News(){
// 垂直排列
Column() {
// 4 列
for (i in 0..3){
// 水平排列(一张图片和两个垂直排列的 Text 组件)
// Modifier 是 Compose 提供的修饰符,可以为组件设置边距、大小等。
Row(Modifier.padding(20.dp)) {
Image(painter = painterResource(id = R.drawable.touxiang), contentDescription = "this is a picture")
Column(Modifier.padding(horizontal = 20.dp)) {
Text(text = "这是新闻标题", fontSize = 20.sp, color = Color.Black)
Text(text = "这是新闻内容", fontSize = 18.sp, color = Color.DarkGray)
}
}
}
}
}
}

列表组件的使用

在 Android 原生 View 体系中,可以使用 RecycleView、GridView 组件分别实现垂直列表、网格列表的样式。

垂直列表组件 LazyColumn

实现类似 RecycleView 可滚动的效果(但简洁了许多):

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
class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
MyApplicationTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
News()
}
}
}
}

/**
* 定义一个可组合函数 News
*/
@Composable
fun News(){
LazyColumn(content = {
for (i in 0..30){
item {
Row(Modifier.padding(20.dp)) {
Image(painter = painterResource(id = R.drawable.touxiang), contentDescription = "this is a picture")
Column(Modifier.padding(horizontal = 20.dp)) {
Text(text = "这是新闻标题$i", fontSize = 20.sp, color = Color.Black)
Text(text = "这是新闻内容$i", fontSize = 18.sp, color = Color.DarkGray)
}
}
}
}
})
}
}

LazyColumn 函数方法如下:

LazyDsl.kt
1
2
3
4
5
6
7
8
9
10
11
12
fun LazyColumn(
modifier: Modifier = Modifier,
state: LazyListState = rememberLazyListState(),
contentPadding: PaddingValues = PaddingValues(0.dp),
reverseLayout: Boolean = false,
verticalArrangement: Arrangement.Vertical =
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
content: LazyListScope.() -> Unit
) {

其中最主要的是 Content 参数,Content 参数用于描述代码块的内容,是 LazyListScope 类型的扩展函数。LazyListScope 接口提供了 item、items 以及 stickyHeader 方法,用于添加项目,上述代码就是通过 item 方法来添加单个内容的。

在实际开发中,数据可能是通过网络请求获取的,可将数据修改为传参的方式,修改代码如下:

1
data class NewsData(val newsTitle:String,val newsDesc:String)
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
class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
MyApplicationTheme {
Surface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colors.background) {
val list = arrayListOf<NewsData>()
for (i in 0..30){
list.add(NewsData("我是新闻标题$i","我是新闻内容$i"))
}
News(newsData = list)
}
}
}
}

/**
* 显示新闻数据的 View
*/
@Composable
fun newsView(newsData:NewsData){
Row(Modifier.padding(20.dp)) {
Image(painter = painterResource(id = R.drawable.touxiang), contentDescription = "this is a picture")
Column(Modifier.padding(horizontal = 20.dp)) {
Text(text = newsData.newsTitle, fontSize = 20.sp, color = Color.Black)
Text(text = newsData.newsDesc, fontSize = 18.sp, color = Color.DarkGray)
}
}
}

@Composable
fun News(newsData: List<NewsData>){
LazyColumn(content = {
for (i in newsData.indices){
item {
newsView(newsData = newsData[i])
}
}
})
}
}

水平列表组件 LazyRow

LazyRow 的使用方式与 LazyColumn 基本一致,可直接将上述代码的 LazyColumn 替换为 LazyRow:

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
class TestActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
MyApplicationTheme {
Surface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colors.background) {
val list = arrayListOf<NewsData>()
for (i in 0..30){
list.add(NewsData("菜单$i","菜单$i"))
}
News(newsData = list)
}
}
}
}

/**
* 显示新闻数据的 View
*/
...

@Composable
fun News(newsData: List<NewsData>){
LazyRow(content = {
for (i in newsData.indices){
item {
newsView(newsData = newsData[i])
}
}
})
}
}

网格列表组件 LazyVerticalGrid

LazyVerticalGrid 实现的效果与原生 View 体系中的 GridView 组件一致。

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
class TestActivity : AppCompatActivity() {

@OptIn(ExperimentalFoundationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
MyApplicationTheme {
Surface(modifier = Modifier.fillMaxSize(),color = MaterialTheme.colors.background) {
val list = arrayListOf<NewsData>()
for (i in 0..30){
list.add(NewsData("菜单$i","菜单$i"))
}
News(newsData = list)
}
}
}
}

/**
* 显示新闻数据的 View
*/
...

@ExperimentalFoundationApi
@Composable
fun News(newsData: List<NewsData>){
// columns 指定单元格的列数
LazyVerticalGrid(columns = GridCells.Fixed(2), content = {
for (i in newsData.indices){
item {
newsView(newsData = newsData[i])
}
}
})
}
}

备注

参考资料

《Android Jetpack开发 原理解析与应用实战》

欢迎关注微信公众号:非也缘也