android 14升级新功能介绍:
添加专用 ScreenShotCallback 来规范监听截屏的开发方式
添加全新 API 来简化 Hightlight 的实现方式
重新设计 Back Arrow 和支持目标界面的预览 Predictive Back Preview 来统一、加强 Android 平台上 Back 导航的体验
支持 Custom Share Action 的标准分享,来满足丰富、灵活的分享需求
引入全新的区域设置 Locale Preferences 来改善用户习惯的体验
引入全新的、独立的语法性别 Grammar Gender 来提高文本表述的准确度
引入特定 API Path Interator 来方便开发者对 Path 历史进行回溯
通过改善 Package Installer 来全方位提升 App 安装、更新方面的细节体验
Compose是一个用于替代Android View的全新声明式UI框架
声明式编程(英语:Declarative programming)或译为声明式编程,是对与命令式编程不同的编程范型的一种合称。它们建造计算机程序的结构和元素,表达计算的逻辑而不用描述它的控制流程。
指令式编程(英语:Imperative programming);是一种描述电脑所需作出的行为的编程范型。几乎所有电脑的硬件都是指令式工作;几乎所有电脑的硬件都是能执行机器语言,而机器代码是使用指令式的风格来写的。
通俗的来说就是:
声明式编程是一种把程序写成描述结果的形式,而不是如何获得结果的形式。它主要关注结果,而不是实现细节。声明式编程的代码通常更简洁,更容易理解和维护。
命令式编程则是一种把程序写成指令的形式,告诉计算机如何实现结果。它更加关注细节,如何实现任务。命令式编程的代码通常更长,更难理解和维护。
Compose其实就是UI框架,它最主要的功能就是让开发人员更加快速的实现 页面逻辑&交互效果 这是目的。
对于传统的XML来说,我们通过请求去服务器获取数据,请求成功后,我们需要findViewById找到页面元素View,再设置View的属性,更新页面展示状态。整个过程是按 http请求 -> 响应 -> 寻找对应View -> 更新对应View按部就班就地执行,这种思想就是命令式编程。
但是Compose描述为 http请求 -> 响应 -> 更新mutableData -> 引用对应数据的View自动重组,整个过程不需要我们开发去写更新UI的代码(发出命令),而是数据发生改变,UI界面自动更新,可以理解为声明式。
目前对于我的体验感受来说,Compose的优势体现在以下几个点:
页面架构清晰。对比以前mvp,mvvm或结合viewbinding,少去了很多接口及编写填充数据相关的代码
动画API简单好用。强大的动画支持,使得写动画非常简单。
开发效率高,写UI速度快,style、shape等样式使用简单。
另外、还有一些官方优势介绍
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等方式进行观测自动重组。
如下一段代码,是一个很常见的需求(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)
}
}
}
假设我们需要根据列表的第一项是否可见来决定划到顶部的按钮是否可见,代码如下:
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只会在改变时发生重组。对应的应用场景是,状态发生了改变,但是我们只关注对界面产生了影响的状态进行分发,这种情况下,就可以考虑使用。
之前我们提到,对于一个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 } // 1,Laziness
// ...
}
// 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,状态读取+使用
) {
// ...
}
}
理解: 由于我们将scroll.value变成了Lambda,所以,它并不会在composition期间产生状态读取行为,这样,当scroll.value发生变化的时候,就不会触发「重组」,这就是 延迟 的意义。
参考学习:https://www.jetpackcompose.app/What-is-the-equivalent-of-DrawerLayout-in-Jetpack-Compose