Jetpack-Compose 学习笔记(一)—— Compose 初探

历时两年,Android 团队推出了全新的原生 Android 界面 UI 库——Compose。当然,Compose 也是属于 Jetpack 工具库中的一部分,官方宣称可以简化并加快 Android 上的界面开发,可以用更少的代码去快速打造生动而精彩的应用。1.0 版本就在上个月底刚刚发布,而且可以在生产环境中使用!不管咋样,先上手看一看!

1. 上手成本如何?

个人感觉,还行,有一定的学习成本。前提条件,对 Kotlin 语言熟悉,因为 Compose 都是用 Kotlin 语言开发实现的,对其他的 Jetpack 库熟悉就更好了。

Compose 可以和现有的工程项目进行互操作。比如,我们可以将 Compose UI 放到现有布局的 View 中,也可以将 View 放到 Compose UI 中。

作为 Jetpack 工具库的一部分,Compose 当然也可以十分方便地与 LiveDada、ViewModel、Paging 等工具一起整合,从而提高编码效率。

Compose 也提供了 Material Design 组件和主题的实现,同时还有简明的动画 API 可以让应用更加灵动,体验更好。

2. 官方广告概述

Google 毕竟憋了两年,怎么说也得有两把刷子的。Compose 是 Google 新推出的适用于 Android 的新式声明性界面工具包。个人理解的声明性的意思是:UI 的控件只需要我们一开始的时候声明创建出来,绑定了数据就可以了,后续的更新可以全部交给 Compose 处理。

Google 是考虑到现在的应用展示的绝大多数不是静态数据,更多的是会实时更新的。而现有的 xml 界面,更新比较复杂繁琐,很容易出现同步错误。并且软件维护的复杂性还会随着需要更新的视图数量而增长,为了解决这一问题,Google 才想完全舍弃原有的用 xml 写视图的方案,重新开发出 Compose 这一整套的解决方案。

Compose 首先会生成整个屏幕,然后仅仅执行必要的更改。它是将 State 状态转化成 UI 界面,并且会智能地跳过那些数据没有发生改变的控件,重新生成已经发生改变的控件,这一过程称之为重组(recomposition)。此外,Compose 布局模型不允许多次测量,最多进行两次测量就可算出各组件的尺寸。

3. 环境搭建

对 IDE 版本有要求,需要下载最新版的 Android Studio —— Android Studio Arctic Fox,目前是 2020 3.1 版本。这个版本在“新建项目”中支持选择 Compose 模板,并且有即时预览 Compose 界面等功能。

一般情况下,对于这种新的技术,我们都会先在主项目中的非核心功能进行实践,慢慢摸索,等到坑踩得差不多了,才会考虑将之前老的工程代码用新的方法重构。所以,Compose 也支持添加到现有的项目中进行使用。

3.1 配置 Kotlin 和 Gradle

需要确保项目中使用的 Kotlin 版本在 1.5.10 及以上。

还需要将应用的最低 API 级别设置为 21 或更高,即 Android 5.0 版本及以上。另外还需将 app 目录下的 gradle 文件中启用 Jetpack Compose,并设置 Kotlin 编译器插件的版本。

android {
    defaultConfig {
        ...
        minSdkVersion 21    // SDK 版本最低为 21
    }

    buildFeatures {
        // Enables Jetpack Compose for this module
        compose true    // 开启 Compose
    }
    ...

    // Set both the Java and Kotlin compilers to target Java 8.
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }

    composeOptions {
        // 编译器插件版本设置
        kotlinCompilerExtensionVersion '1.0.0-rc02'
    }
}

3.2 添加工具包依赖项

官方文档上需要添加的依赖如下:

dependencies {
    implementation 'androidx.compose.ui:ui:1.0.0-rc02'
    // Tooling support (Previews, etc.)
    implementation 'androidx.compose.ui:ui-tooling:1.0.0-rc02'
    // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
    implementation 'androidx.compose.foundation:foundation:1.0.0-rc02'
    // Material Design
    implementation 'androidx.compose.material:material:1.0.0-rc02'
    // Material design icons
    implementation 'androidx.compose.material:material-icons-core:1.0.0-rc02'
    implementation 'androidx.compose.material:material-icons-extended:1.0.0-rc02'
    // Integration with activities
    implementation 'androidx.activity:activity-compose:1.3.0-rc02'
    // Integration with ViewModels
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha07'
    // Integration with observables
    implementation 'androidx.compose.runtime:runtime-livedata:1.0.0-rc02'
    implementation 'androidx.compose.runtime:runtime-rxjava2:1.0.0-rc02'

    // UI Tests
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.0.0-rc02'
}

其实如果只是想上手看看效果,没必要添加 Integration with activities、ViewModels、observables 这些库。

4. 简单上手

Compose 核心内容就是可组合的函数,如同它的英文名称一样,将 UI 拆解成一个个可组合在一起的 Composable 函数,方便维护与复用。但是,可组合函数只能在其他的可组合函数的范围内调用。要使函数成为可组合函数,只需在该函数上方添加 @Composable 注解即可。其实可以直接把被 @Composable 注解的函数看成是一个 View。

@Composable 注解可告诉 Compose 编译器:此函数旨在将数据转换为界面。并且生成界面的 Compose 函数不需要返回任何内容,因为它们描述的是所需的屏幕状态,而不是构造界面的组件。

还有一个很强大的功能是,Compose 是支持在 IDE 中预览可组合函数的,只需要在 Composable 函数上再添加一个 @Preview 注解就可以了,限制条件是 @Preview 注解只能修饰一个无参的函数,所以,如果你要预览,就得保证你预览的函数无参,或者再用一个无参函数包起来:

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

@Preview
@Composable
fun WrapperView() {
    Greeting("hahahaha")
}

就这样子,你就可以在 IDE 中看到预览的效果了,甚至都没有执行入口!
Compose 的 Hello World 代码也比较简单,只需要在 setContent 方法里添加需要展示的 Composable 函数即可:

// code 2
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {    // 设置显示内容,相当于 setContentView
            Greeting("Hello World!")
        }
    }
}

4.1 Compose 布局初探

如果写过 Flutter,那么你会发现,Compose 的布局与 Flutter 类似。Column 可以将元素从上到下进行排列,类似于 LinearLayout 布局的 oritation 设置为 vertical。Row 就是将元素从左到右进行排列,类似于 LinearLayout 布局的 oritation 设置为 horizonal。还有 Box 堆叠布局,类似于 FrameLayout 布局,这里不再展开。下面是 Column 布局的一个简单例子,代码显示的效果如代码下方的图片所示:

// code 3
@Composable
fun NewStory() {
    Column(
        // 许多对象都有这个 Modifier 属性,这个属性非常重要,这里是设置了 padding
        modifier = Modifier.padding(16.dp)
    ) {
        Image(
            painter = painterResource(id = R.drawable.header),
            contentDescription = null
        )
        Text("今天天气好")
        Text(text = "郑州")
        Text(text = "July 2021")
    }
}
image.png

这里要说下 contentDescription 属性,官方教学文档的原文是:

Note: You also need to provide a contentDescription for the image. The description is used for accessibility. However, in a case like this where the image is purely decorative, it's appropriate to set the description to null, as we do here.

意思是:我们需要为 image 提供一个 contentDescription 属性。这个属性用于可访问性(=。=?)。然鹅,如果这个 image 纯粹只是装饰作用,那么也可以像我们在这里设置的一样,设为 null。

懵逼脸。然后去源码看看这个到底是个啥?

*@param contentDescription text used by accessibility services to describe what this image
*represents. This should always be provided unless this image is used for decorative purposes,
*and does not represent a meaningful action that a user can take. This text should be
*localized, such as by using [androidx.compose.ui.res.stringResource] or similar

意思是:" 这个属性是可访问性服务用于描述此图片代表的是什么。这个属性的信息应该都要提供,除非此图只是用于装饰的目的,或者并没有表示用户有特殊意义的操作。此外,属性的信息文本应该存放在本地资源中,如 res 目录下的 string 或类似的地方。"

额。。。还是有点懵,去网上看了下 ImageView 中的 contentDescription 属性,好像是为了方便视力有障碍的人群所设置的。反正绝大多数情况下可以忽略,如有实际用途,欢迎交流讨论。

此外,Compose 的布局还有很灵活的,还记得在 LinearLayout 布局中可以设置 weight 来控制填充父布局吗?在 Compose 也有类似的用法,直接上代码吧~

// code 4
@Composable
fun MyScreenContent(names: List<String> = listOf("Android","there")) {
    Column(modifier = Modifier.fillMaxHeight()) {    // 类似于 match_parent
        Column(modifier = Modifier.weight(1f)) {    // 占满父布局剩余的高度空间
            for (name in names) {
                Text(text = name)
                Divider(color = Color.Black)
            }
        }
        Button(onClick = { }) {
            Text(text = "这是第二个 Button")
        }
    }
}

这个布局就是可以将 Button 放在父布局的底部位置,然后父布局剩余空间都会被内层的 Column 布局占满。

当然,Compose 可以轻松地遵循 Material Design 原则,因为可以直接在任何 Composable 函数外部用 MaterialTheme {} 包裹起来,这就可以使用 MaterialTheme 的属性了。包括字体样式、色值等。这里代码都比较简单,不再赘述。代码示例:https://gitee.com/xiuzhizhu/ComposeDemo.git

4.2 Compose 可构建容器函数

Compose 支持构建容器函数,容器函数类似于 Theme 主题,可以将一些基础的设置信息放在容器函数中,这样放入这个容器函数中的 Composable 函数就会根据设置的信息进行绘制、渲染。举个简单的栗子:

// code 5
// 声明一个容器函数
@Composable
fun MyApp(content: @Composable () -> Unit) {
    MaterialTheme() {
        Surface(color = Color.Yellow, modifier = Modifier.padding(10.dp)) {
            content()
        }
    }
}

// 实际运用
MyApp {
    Text(text = "被容器函数所修饰的 Text")
}

所有放入 MyApp 容器中的 Composable 函数都会带上容器函数中设置的属性。这样可提高代码复用性和可读性。

4.3 Compose 状态初探

Compose 的核心内容就是响应 state 状态的改变。Compose 通过调用 Composable 函数可以将 data 数据展示在 UI 上,Compose 本身也提供了工具去观察 data 数据的变化,从而可以自动地回调展示 UI,这一过程官方称为重组,前面也有说到。可以理解为更新 UI。

在 Composable 函数内部我们可以使用 mutableStateOf 方法去添加一个可变的 state,为了避免每次重组都会出现不同的状态,所以可以用 remember 记住这个可变状态。

// code 6
@Composable
fun Counter() {
    val count = remember { mutableStateOf(0)}  // 初始化为 0
    Button(onClick = { count.value++ }) {
        Text(text = "已点击了 ${count.value} 次!")
    }
}

这样每次点击 Button,都会更新点击的次数值。

4.4 Compose 列表初探

列表布局使用频率还是比较高的,像 ListView 和 RecyclerView 都是耳熟能详的用于展示列表的 View 控件。那么 LazyColumn 就相当于 Compose 中的 RecyclerView,用于展示可滑动的长列表。它提供了 items API 用于展示简单的列表布局。

// code 7
@Composable
fun NameList(names: List<String>, modifier: Modifier = Modifier) {
    LazyColumn(modifier = modifier) {
        items(items = names) { name ->
            Greeting(name = name)
            Divider(color = Color.Black)    // 分割线类对象
        }
    }
}

然而,LazyColumn 不会像 RecyclerView 一样缓存列表中的布局,而是在滚动浏览它时,它会渲染新的列表 View,并没有回收机制,但是相比于实例化 Android View,渲染 Composable UI 组件效率更高。

4.5 Compose 自定义主题

Compose 中有自带的一些主题,比如 MaterialTheme,被这些 Theme 包裹,就可以呈现出这些 Theme 所设置的属性了。当然也可以单独将这些 Theme 中某些属性拿出来,比如字体。然后就可以使用该主题下设置的各种字体样式了,同样的还有色值:

// code 8
@Composable
fun Greeting(name: String) {
    val greetingTypography = MaterialTheme.typography // 获取 MaterialTheme 字体样式
    val greetingColors = MaterialTheme.colors // 获取 MaterialTheme 色值
    Text(text = "Hello $name",
        color = greetingColors.onBackground,    // 使用 MaterialTheme 的 onBackground 色值
        style = greetingTypography.body2)
}

还可以调用 copy 方法复制某主题的样式,然后在此基础上改写自己的一些样式属性:

// code 9
@Composable
fun Greeting(name: String) {
    val customStyle = MaterialTheme.typography.h5.copy(color = Color.Green)
    Text(text = "Hello $name",
        style = customStyle)
}

如何自定义一个自己的 Theme?其实也很简单,下面是一个例子:

// code 10
// 主要方法,被此方法包裹的 Composable 函数都会被设置为自定义主题
@Composable
fun CustomTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),  // 默认根据系统来设置是否为暗夜模式
    content: @Composable () -> Unit  // 被传入的 Composable 函数
){
    val colors = if (darkTheme) {
        DarkColors
    } else {
        LightColors
    }

    MaterialTheme(colors = colors) { // 将设置好的色值传入
        content()
    }
}

private val DarkColors = darkColors( // 暗夜模式下的色值
    primary = Red300,
    primaryVariant = Red700,
    onPrimary = Color.Black,
    secondary = Red300,
    onSecondary = Color.Black,
    error = Red200
)

private val LightColors = lightColors( // 白天模式下的色值
    primary = Red700,
    primaryVariant = Red900,
    onPrimary = Color.White,
    secondary = Red700,
    secondaryVariant = Red900,
    onSecondary = Color.White,
    error = Red800
)

是不是觉得简单?是的,在 Compose 中自定义一个主题就是这么简单。

5. 编程思想

再来说一说官方文档里提到的 Compose 的编程思想吧。它采用的是声明性界面模型,该模型工作原理是先从开始生成整个屏幕,然后仅执行必要的更改。重组就是使用新数据再次调用 Composable 函数,从而进行更新的。当然重组过程仅调用可能已更改的函数或 lambda,而跳过其余函数或 lambda,所以 Compose 可以高效地重组。

其中,官方建议在更新时,不要依赖于执行 Composable 函数所产生的附带效应,因为可能会跳过函数的重组。附带效应指的是对应用的其余可见部分的任何更改。危险的附带效应有1)写入共享对象的属性(这个应该是怕有其他的逻辑正在读取共享对象属性来更新 UI 等,使得 UI 变化不准确。);2)更新 ViewModel 中的可观察项(原理同1));3)更新 SharedPreference(原理同1))。(不是很理解,可能日后真正使用后会更有体会吧~欢迎一起讨论)

Composable 函数可能会像每一帧一样频繁地重新执行,例如在呈现动画时。Composable 函数应快速执行,避免在播放动画期间出现卡顿。如果需要执行耗时操作,如从 SharedPreference 中读取数据,那么建议在后台协程中处理,然后使用回调传递当前值来触发更新。还有几个值得注意的 Tips:

1、Composable 函数可以按任何顺序执行
如果某个 Composable 函数中包含有几个 Composable 函数,那么这些 Composable 函数可能按任何顺序运行,Compose 会识别出哪些界面元素的优先级高于其他的界面元素,从而优先绘制这些元素。

2、 Composable 函数可以并行运行
Compose 可以通过并行运行 Composable 函数来优化重组。所以,Compose 可以利用多个核心,并以较低的优先级运行 Composable 函数。因此,Composable 函数可能会在后台线程池中执行。调用某个 Composable 函数时,调用可能发生在与调用方不同的线程中。

3、重组会跳过尽可能多的内容
Compose 会尽力只重组需要更新的部分,每个 Composable 函数和 lambda 又可以自行重组更新。Compose 若在一次重组时发现参数又更新了,则会取消当前的重组,并用新参数重新开始。

官方推荐将 Composable 函数写在顶级函数,方便以后复用。

Compose 博大精深,许多概念性的东西并没有理解得很透彻,还需慢慢实践才能得出真知啊!欢迎留言交流,互相学习!

ps. 简单的 Demo:https://gitee.com/xiuzhizhu/ComposeDemo.git
官方教程网站:https://developer.android.google.cn/courses/pathways/compose

参考文献

  1. Jetpack Compose 1.0 正式发布!打造原生 UI 的 Android 现代工具包
  2. Jetpack Compose 基础知识
  3. Compose 编程思想

尾巴:这是 Compose 系列笔记的首篇,相信细心的同学也发现了,这篇笔记是根据官方教程网站上的学习路线进行记录学习的。自己在学习的过程中,遇到不明白的地方也会查阅大量的资料进行补充,喜欢的话,欢迎分享转发加关注~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容