Android Jetpack Compose使用及优势
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
什么是Jetpack Compose
Jetpack Compose是一组集成库用于开发安卓应用程序的用户界面。它是一个强大的工具提供了许多新的功能旨在使应用程序更加美观、稳定和易于使用。它可以与现有的代码集成并提供一个用于创建用户界面的独立框架。
Jetpack Compose的主要目标是改进应用程序的用户界面并提高应用程序的稳定性和性能。Compose的主要组成部分是UI它允许您构建您的应用程序的所有界面组件。而且与其他Android SDK方法不同Jetpack Compose随着时间的推移可以更容易地进行迭代和维护。这使得它成为学习和使用的可靠工具。
优势和特点
Jetpack Compose具有以下特点和优点
1. 更快的开发过程Composed的UI可以以可重复的代码块组成。使您可以 差异化地开发您的UI并以最快的速度得到反馈。
2. 更直观的开发过程: Compose使得UI设计更为直观。您可以轻松地在代码中添加各种UI元素并将其组合起来实时预览结果从而可以更容易地进行开发。
3. 更容易实现一致的用户界面Compose框架使得开发者能够实现一致的用户界面。开发者可以轻松地定义主题和风格并与应用程序中的其他组件协调。
4. 较少的代码量与传统的Android开发方法相比使用Jetpack Compose可以大大减少代码。这降低了应用程序的维护成本提高了开发速度。
5. 更好的性能由于Jetpack Compose可以更好地减少运行时的布局计算因此可以获得更好的性能。这意味着应用程序可以更快地响应用户操作提高了用户体验。
据谷歌官方介绍Jetpack Compose 有以下特点
- 更少的代码使用更少的代码实现更多的功能并且可以避免各种错误从而使代码简洁且易于维护。
- 直观的 Kotlin API只需描述界面Compose 会负责处理剩余的工作。应用状态变化时界面会自动更新。
- 加快应用开发兼容现有的所有代码方便随时随地采用。借助实时预览和全面的 Android Studio 支持实现快速迭代。
- 功能强大凭借对 Android 平台 API 的直接访问和对于 Material Design、深色主题、动画等的内置支持创建精美的应用。
Compose 出生的目的就是为了重新定义 Android 上 UI 的编写方式为了「提高 Android 原生的 UI 开发效率让 Android 的 UI 开发方式能跟上时代的步伐」。
一、声明式 vs 指令式编程
1、定义
无论是官网文档还是介绍Compose的优点时都会说到Compose是声明式的。我们来回顾下在wiki上有着如下定义
声明式编程英语Declarative programming或译为声明式编程是对与命令式编程不同的编程范型的一种合称。它们建造计算机程序的结构和元素表达计算的逻辑而不用描述它的控制流程。
指令式编程英语Imperative programming是一种描述电脑所需作出的行为的编程范型。几乎所有电脑的硬件都是指令式工作几乎所有电脑的硬件都是能执行机器语言而机器代码是使用指令式的风格来写的。
通俗的来说就是声明式编程是一种把程序写成描述结果的形式而不是如何获得结果的形式。它主要关注结果而不是实现细节。声明式编程的代码通常更简洁更容易理解和维护。
命令式编程则是一种把程序写成指令的形式告诉计算机如何实现结果。它更加关注细节如何实现任务。命令式编程的代码通常更长更难理解和维护。
2、个人理解
Compose其实就是UI框架它最主要的功能就是让开发人员更加快速的实现 页面逻辑&交互效果 这是目的。
对于传统的XML来说我们通过请求去服务器获取数据请求成功后我们需要findViewById
找到页面元素View再设置View的属性更新页面展示状态。整个过程是按 http请求 -> 响应 -> 寻找对应View -> 更新对应View
按部就班就地执行这种思想就是命令式编程。
但是Compose描述为 http请求 -> 响应 -> 更新mutableData -> 引用对应数据的View自动重组
整个过程不需要我们开发去写更新UI的代码发出命令而是数据发生改变UI界面自动更新可以理解为声明式。
二、Compose优势
目前对于我的体验感受来说Compose的优势体现在以下几个点
- 页面架构清晰。对比以前mvpmvvm或结合viewbinding少去了很多接口及编写填充数据相关的代码
- 动画API简单好用。强大的动画支持使得写动画非常简单。
- 开发效率高写UI速度快style、shape等样式使用简单。
- 另外、还有一些官方优势介绍
三、Compose 的重组作用域
虽然Compose 编译器在背后做了大量工作来保证 recomposition 范围尽可能小我们还是需要对哪些情况发生了重组以及重组的范围有一定的了解 。
假设有如下代码
@Composable
fun Foo() {
var text by remember { mutableStateOf("") }
Log.d(TAG, "Foo")
Button(onClick = {
text = "$text $text"
}.also { Log.d(TAG, "Button") }) {
Log.d(TAG, "Button content lambda")
Text(text).also { Log.d(TAG, "Text") }
}
}
其打印结果为
D/Compose: Button content lambda
D/Compose: Text
按照开发经验第一感觉会是text变量只被Text控件用到了。
分析一下Button控件的定义为
参数 text
作为表达式执行的调用处是 Button 的尾lambda而后才作为参数传入 Text()
。 所以此时最小重组范围是 Button 的 尾lambda 而非 Text()
另外还有两点需要关注
- Compose 关心的是代码块中是否有对 state 的 read而不是 write。
- text 指向的 MutableState 实例是永远不会变的变的只是内部的 value
重组中的 Inline 陷阱
非inline函数 才有资格成为重组的最小范围理解这点特别重要
我们将代码稍作改动为 Text()
包裹一个 Box{...}
@Composable
fun Foo() {
var text by remember { mutableStateOf("") }
Button(onClick = { text = "$text $text" }) {
Log.d(TAG, "Button content lambda")
Box {
Log.d(TAG, "Box")
Text(text).also { Log.d(TAG, "Text") }
}
}
}
日志如下
D/Compose: Button content lambda
D/Compose: Box
D/Compose: Text
要点
Column
、Row
、Box
乃至Layout
这种容器类 Composable 都是inline
函数因此它们只能共享调用方的重组范围也就是 Button 的 尾lambda
如果你希望通过缩小重组范围提高性能怎么办
@Composable
fun Foo() {
var text by remember { mutableStateOf("") }
Button(onClick = { text = "$text $text" }) {
Log.d(TAG, "Button content lambda")
Wrapper {
Text(text).also { Log.d(TAG, "Text") }
}
}
}
@Composable
fun Wrapper(content: @Composable () -> Unit) {
Log.d(TAG, "Wrapper recomposing")
Box {
Log.d(TAG, "Box")
content()
}
}
- 自定义非 inline 函数使之满足 Compose 重组范围最小化条件。
四、Compose开发时提高性能的关注点
当 Compose 更新重组时它会经历三个阶段跟传统View比较类似
- 组合Compose 确定要显示的内容 - 运行可组合函数并构建界面树。
- 布局Compose 确定界面树中每个元素的尺寸和位置。
- 绘图Compose 实际渲染各个界面元素。
基于这3个阶段 尽可能从可组合函数中移除计算。每当界面发生变化时都可能需要重新运行可组合函数可能对于动画的每一帧都会重新执行您在可组合函数中放置的所有代码。
1、合理使用 remember
它的作用是
- 保存重组时的状态并可以有重组后取出之前的状态
引用官方的栗子
@Composable
fun ContactList(
contacts: List<Contact>,
comparator: Comparator<Contact>,
modifier: Modifier = Modifier
) {
LazyColumn(modifier) {
// DON’T DO THIS
items(contacts.sortedWith(comparator)) { contact ->
// ...
}
}
}
LazyColumn
在滑动时会使自身状态发生改变导致ContactList
重组从而contacts.sortedWith(comparator)
也会重复执行。而排序是一个占用CPU算力的函数对性能产生了较大的影响。
正确做法
@Composable
fun ContactList(
contacts: List<Contact>,
comparator: Comparator<Contact>,
modifier: Modifier = Modifier
) {
val sortedContacts = remember(contacts, sortComparator) {
contacts.sortedWith(sortComparator)
}
LazyColumn(modifier) {
items(sortedContacts) {
// ...
}
}
}
- 使用
remember
会对排序的结果进行保存使得下次重组时只要contacts
不发生变化 其值可以重复使用。 - 也就是说它只进行了一次排序操作避免了每次重组时都进行了计算。
提示
- 更优的做法是将这类计算的操作移出Compose方法放到ViewModel中再使用
collectAsState
或LanchEffect
等方式进行观测自动重组。
2、使用LazyColumn、LazyRow
等列表组件时指定key
如下一段代码是一个很常见的需求from官网
NoteRow记录每项记录的简要信息当我们进入编辑页进行修改后需要将最近修改的一条按修改时间放到列表最前面。这时假若不指定每项Item的Key其中一项发生了位置变化都会导致其他的NoteRow
发生重组然而我们修改的只是其中一项进行了不必要的渲染。
@Composable
fun NotesList(notes: List<Note>) {
LazyColumn {
items(
items = notes
) { note ->
NoteRow(note)
}
}
}
正确的做法
- 为每项Item提供 项键就可避免其他未修改的NoteRow只需挪动位置避免发生重组
@Composable
fun NotesList(notes: List<Note>) {
LazyColumn {
items(
items = notes,
key = { note ->
// 为每项Item提供稳定的、不会发生改变的唯一值通常为项ID
note.id
}
) { note ->
NoteRow(note)
}
}
}
3、使用 derivedStateOf
限制重组
假设我们需要根据列表的第一项是否可见来决定划到顶部的按钮是否可见代码如下
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
val showButton = listState.firstVisibleItemIndex > 0
AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
- 由于列表的滑动会使
listState
状态改变而使用showButton
的AnimatedVisibility
会不断重组导致性能下降。
解决方案是使用派生状态。如下
val listState = rememberLazyListState()
LazyColumn(state = listState) {
// ...
}
val showButton by remember {
derivedStateOf {
listState.firstVisibleItemIndex > 0
}
}
AnimatedVisibility(visible = showButton) {
ScrollToTopButton()
}
- 派生状态可以这样理解只有在
derivedStateOf
里的状态发生改变时只关注和派发对UI界面产生了影响的状态。这样AnimatedVisibility
只会在改变时发生重组。对应的应用场景是状态发生了改变但是我们只关注对界面产生了影响的状态进行分发这种情况下就可以考虑使用。
4、尽可能延迟State的读行为
之前我们提到对于一个Compose页面来说它会经历以下步骤
- 第一步Composition这其实就代表了我们的Composable函数执行的过程。
- 第二步Layout这跟我们View体系的Layout类似但总体的分发流程是存在一些差异的。
- 第三步Draw也就是绘制Compose的UI元素最终会绘制在Android的Canvas上。由此可见Jetpack Compose虽然是全新的UI框架但它的底层并没有脱离Android的范畴。
- 最后Recomposition也就是重组并且重复1、2、3步骤。
尽可能推迟状态读取的原因其实还是希望我们可以在某些场景下直接跳过Recomposition的阶段、甚至Layout的阶段只影响到Draw。
分析如下代码
@Composable
fun SnackDetail() {
// Recomposition Scope
// ...
Box(Modifier.fillMaxSize()) { Start
val scroll = rememberScrollState(0)
// ...
Title(snack, scroll.value) // 1状态读取
// ...
}
// Recomposition Scope End
}
@Composable
private fun Title(snack: Snack, scroll: Int) {
// ...
val offset = with(LocalDensity.current) { scroll.toDp() }
Column(
modifier = Modifier
.offset(y = offset) // 2状态使用
) {
// ...
}
}
上面的代码有两个注释注释1代表了状态的读取注释2代表了状态的使用。这种“状态读取与使用位置不一致”的现象其实就为Compose提供了性能优化的空间。
那么具体我们该如何优化呢简单来说就是让“状态读取与使用位置一致”。
改为如下
// 代码段12
@Composable
fun SnackDetail() {
// Recomposition Scope
// ...
Box(Modifier.fillMaxSize()) {
val scroll = rememberScrollState(0)
// ...
Title(snack) { scroll.value } // 1Laziness
// ...
}
// Recomposition Scope End
}
@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
// ...
val offset = with(LocalDensity.current) { scrollProvider().toDp() }
Column(
modifier = Modifier
.offset(y = offset) // 2状态读取+使用
) {
// ...
}
}