Jetpack Compose

一,写在前面的话

1,什么是Jetpack Compose?

Jetpack Compose是一个现代工具包,旨在简化UI开发。它结合了响应式编程模型和Kotlin编程语言的简洁性和易用性。同时,它完全是声明性的,意味着您可以通过调用一系列将数据转换为UI层次结构的方法来描述UI。当数据更改时,框架会自动调用这些功能,从而为您更新视图层次结构。

www.bilibili.com/video/BV1Ey...

先体验一下

2,为什么要有Compose?

1)Android的UI由XML和代码一起定义,XML和代码之间存在很强的耦合,容易出现修改不一致导致运行错误。Compose的UI完全由代码完成,提升了内聚和封装性;

2)Compose通过引入声明式编程,UI自动更新,只需要专注于数据的更新逻辑,可以让代码编写更快更简单。

二,怎样使用Jetpack Compose?

1,一个最简单的例子

这样,一个简单的Text就完成了。

为了提升区分度,Compose的方法名需要大写开头,并添加 @Composable 注解,有这个注解的方法才会被认为是Compose的方法。
一个Compose方法,必须要在其他的Compose方法内才可以被调用。

因为使用代码去编写布局,不再依托xml文件,ui预览的方式也有了一定的变化。每个Compose方法都可以单独进行预览,只需要在方法上添加 @Preview 注解就可以,一个文件中可以有多个 @Preview 注解,系统会为每个带有 @Preview 的Compose方法生成各自的预览图。

需要注意的是,添加 @Preview 必须保证该Compose方法没有未定的参数,也就说,方法要么无参,要么所有参数都有默认值。

2,一个稍微复杂的例子

使用Compose如何去实现一个列表的功能呢?

暂时无法在文档外展示此内容

如下图所示,只需要一个LazyColumn或者LazyRow,然后在里面完成list与item的绑定就OK了。

如果使用RecyclerView实现一个列表应该怎么做呢?首先要在容器Activity或者Fragment的xml中放一个RecyclerView,然后在Activity中通过findViewById找到他,再写一个ViewHolder,以及他的xml,然后又是一系列的findViewById。再写一个Adapter,实现各种方法完成与ViewHolder的绑定。还要在Adapter中添加一些数据绑定的方法,去更新adapter中的数据。 而如果使用Compose,代码量就会减少很多了。

这就是声明性编程范式带来的直观改变。声明性界面模型大大简化了与构建和更新界面关联的工程设计。我们后面会详细描述下这个概念,以及Compose是如何work的。

三,Compose的一些特点

1,声明式

命令式编程需要告诉系统每一步要做什么,具体该怎么做,通过这样的一种流程实现我们想要的功能。通过调用 View 的某些 set 方法来更新 UI 的状态,这是一个手动更新 UI 的过程,这个过程的维护性通常会随着 UI 复杂度的增加而增加,非常容易出错。

声明式编程则是隐藏了怎么做这个流程,你只需要操作一些基本元素,通过组合方式,把你想要的效果搭建出来就好了。使用声明式 UI 编写界面可以极大的减少代码量,同时也提高了容错率,整个 UI 构建逻辑也变得更加清晰、易读。

举个最简单的栗子🌰

假设有一个带有未读消息图标的电子邮件应用。如果没有消息,应用会绘制一个空信封;如果有一些消息,我们会在信封中绘制一些纸张;而如果有 100 条消息,我们就把图标绘制成好像在着火的样子......

使用命令式接口,我们可能会写出一个下面这样的更新数量的函数:

在这段代码中,我们接收新的数量,并且必须搞清楚如何更新当前的 UI 来反映对应的状态。

作为替代,使用声明式接口编写这一逻辑则会看起来像下面这样:

再来一个例子

暂时无法在文档外展示此内容

在 Compose 中,UI 的刷新是通过重新渲染生成整个屏幕实现的,这是所有声明式 UI 刷新页面的工作原理。但是 Compose 会根据数据的变化,仅针对需要改变的元素做必要的修改,通过改变数据而重新生成组合 UI 这一过程称为重组 (recompose) 。当数据更新了,虽然是从整个屏幕开始重新渲染,但与数据修改无关的元素,会保持之前生成的实例,因为 @Composable 方法渲染是非常快的、幂等且无副作用。

非常重要的一点就是幂等,幂等的意思是指可以使用相同参数重复执行,并能获得相同结果的函数。这些函数不会影响系统状态,也不用担心重复执行会对系统造成改变。说白了就是与数据修改无关的 @Composable 元素在屏幕重新渲染的过程中不会被重组,仅调用可能已更改的函数或 lambda,而跳过其余函数或 lambda。通过跳过所有未更改参数的函数或 lambda,Compose 可以高效地重组。

2,重组(recompose)

  • 可组合函数(带有@Compose注解的方法)可以按任何顺序执行。
  • 可组合函数可以并行执行。
  • 重组会跳过尽可能多的可组合函数和 lambda。
  • 重组是乐观的操作,可能会被取消。
  • 可组合函数可能会像动画的每一帧一样非常频繁地运行。

1)可组合函数可能按任何顺序执行

Compose 可以选择识别出某些界面元素的优先级高于其他界面元素,因而首先绘制这些元素。

如果某个可组合函数包含对其他可组合函数的调用,这些函数可以按任何顺序运行。

scss 复制代码
@Composable

fun ButtonRow() {

    MyFancyNavigation {

        StartScreen()

        MiddleScreen()

        EndScreen()

    }

}

StartScreenMiddleScreenEndScreen 的调用可以按任何顺序进行。这意味着,举例来说,您不能让 StartScreen() 设置某个全局变量(附带效应)并让 MiddleScreen() 利用这项更改。相反,其中每个函数都需要保持独立。

2)可组合函数可以并行运行

Compose 可以通过并行运行可组合函数来优化重组。这样一来,Compose 就可以利用多个核心,并以较低的优先级运行可组合函数(不在屏幕上)。

这种优化意味着,可组合函数可能会在后台线程池中执行。如果某个可组合函数对 ViewModel 调用一个函数,则 Compose 可能会同时从多个线程调用该函数。

调用某个可组合函数时,调用可能发生在与调用方不同的线程上。这意味着,应避免使用修改可组合 lambda 中的变量的代码,既因为此类代码并非线程安全代码,又因为它是可组合 lambda 不允许的附带效应。如下错误的例子:

kotlin 复制代码
@Composable

fun ListWithBug(myList: List<String>) {

   var items = 0 // 不应使用该变量



   Row(horizontalArrangement = Arrangement.SpaceBetween) {

       Column {

           for(item in myList) {

               Text("Item: $item")

               items++ 

           }

       }

       Text("Count: $items")

   }

}

在本例中,每次重组时,都会修改 items。这可以是动画的每一帧,或是在列表更新时。但不管怎样,界面都会显示错误的项数。因此,Compose 不支持这样的写入操作;通过禁止此类写入操作,我们允许框架更改线程以执行可组合 lambda。

3)重组会跳过尽可能多的内容

如果界面的某些部分无效,Compose 会尽力只重组需要更新的部分。这意味着,它可以跳过某些内容以重新运行单个按钮的可组合项,而不执行界面树中在其上面或下面的任何可组合项。

4)重组是乐观的操作

只要 Compose 认为某个可组合项的参数可能已更改,就会开始重组。重组是乐观的操作,也就是说,Compose 预计会在参数再次更改之前完成重组。如果某个参数在重组完成之前发生更改, Compose 可能会取消重组,并使用新参数重新开始。

取消重组后,Compose 会从重组中舍弃界面树。如有任何附带效应依赖于显示的界面,则即使取消了组成操作,也会应用该附带效应。这可能会导致应用状态不一致。

5)可组合函数可能会非常频繁地运行

在某些情况下,可能会针对界面动画的每一帧运行一个可组合函数。如果该函数执行成本高昂的操作(例如从设备存储空间读取数据),可能会导致界面卡顿。

3,重组的流程

就这样一小点带有 @Composable 注解的代码,反编译后看到的代码就是下面这个样子

核心代码在Composer.kt

在Composer中,持有了一个SlotTabel,存储了所有的数据。

间隙缓冲区是一个集合,它在内存中使用扁平数组 (flat array) 实现。这一扁平数组比它代表的数据集合要大,而那些没有使用的空间就被称为间隙。

一个正在执行的 Composable 的层级结构可以使用这个数据结构,而且我们可以在其中插入一些东西。

让我们假设已经完成了层级结构的执行。在某个时候,我们会重新组合一些东西。所以我们将游标重置回数组的顶部并再次遍历执行。在我们执行时,可以选择仅仅查看数据并且什么都不做,或是更新数据的值。

我们也许会决定改变 UI 的结构,并且希望进行一次插入操作。在这个时候,我们会把间隙移动至当前位置。

现在,我们可以进行插入操作了。

在了解此数据结构时,很重要的一点是除了移动间隙,它的所有其他操作包括获取 (get)、移动 (move) 、插入 (insert) 、删除 (delete) 都是常数时间操作。移动间隙的时间复杂度为 O(n)。我们选择这一数据结构是因为 UI 的结构通常不会频繁地改变。当我们处理动态 UI 时,它们的值虽然发生了改变,却通常不会频繁地改变结构。当它们确实需要改变结构时,则很可能需要做出大块的改动,此时进行 O(n) 的间隙移动操作便是一个很合理的权衡。

让我们来看一个计数器示例:

kotlin 复制代码
@Composable

fun Counter() {

 var count by remember { mutableStateOf(0) }

 Button(

   text="Count: $count",

   onPress={ count += 1 }

 )

}

这是我们编写的代码,不过我们要看的是编译器做了什么。

当编译器看到 Composable 注解时,它会在函数体中插入额外的参数和调用。

首先,编译器会添加一个 Composer.start 方法的调用,并向其传递一个编译时生成的整数 key。

编译器也会将 Composer 对象传递到函数体里的所有 composable 调用中。

kotlin 复制代码
fun Counter($Composer: Composer) {

 $Composer.start(123)//随机整数

 var count by remember($Composer) { mutableStateOf(0) }

 Button(

   $Composer,

   text="Count: $count",

   onPress={ count += 1 },

 )

 $Composer.end()

}

当此 Composer 执行时,它会进行以下操作:

  • Composer.start 被调用并存储了一个存有随机数的组对象 (group object)
  • remember 插入了这个组对象 (group object)
  • mutableStateOf 的值被返回,而 state 实例会被存储起来
  • Button 基于它的每个参数存储了一个分组

最后,当我们到达 Composer.end 时:

数据结构现在已经持有了来自组合的所有对象,整个树的节点也已经按照深度优先遍历的执行顺序排列。

现在,所有这些组对象已经占据了很多的空间,它们为什么要占据这些空间呢?这些组对象是用来管理动态 UI 可能发生的移动和插入的。编译器知道哪些代码会改变 UI 的结构,所以它可以有条件地插入这些分组。大部分情况下,编译器不需要它们,所以它不会向插槽表 (slot table) 中插入过多的分组。为了说明一这点,请您查看以下条件逻辑:

kotlin 复制代码
@Composable fun App() {

 val result = getData()

 if (result == null) {

   Loading(...)

 } else {

   Header(result)

   Body(result)

 }

}

在这个 Composable 函数中,getData 函数返回了一些结果并在某个情况下绘制了一个 Loading composable 函数;而在另一个情况下,它绘制了 Header 和 Body 函数。编译器会在 if 语句的每个分支间插入分隔关键字。

scss 复制代码
fun App($Composer: Composer) {

 val result = getData()

 if (result == null) {

   $Composer.start(123)

   Loading(...)

   $Composer.end()

 } else {

   $Composer.start(456)

   Header(result)

   Body(result)

   $Composer.end()

 }

}

让我们假设这段代码第一次执行的结果是 null。这会使一个分组插入空隙并运行载入界面。

函数第二次执行时,让我们假设它的结果不再是 null,这样一来第二个分支就会执行。

对 Composer.start 的调用有一个 key 为 456 的分组。编译器会看到插槽表中 key 为 123 分组与之并不匹配,所以此时它知道 UI 的结构发生了改变。

于是编译器将缝隙移动至当前游标位置并使其在以前 UI 的位置进行扩展,从而有效地消除了旧的 UI。

此时,代码已经会像一般的情况一样执行,而且新的 UI ------ header 和 body ------ 也已被插入其中。

在这种情况下,if 语句的开销为插槽表中的单个条目。通过插入单个组,我们可以在 UI 中任意实现控制流,同时启用编译器对 UI 的管理,使其可以在处理 UI 时利用这种类缓存的数据结构。

四,和原生View的互操作性

1,现有代码中嵌套Compose

Compose是一种ui框架,渲染机制与现有的View体系有很大的差异,就目前而言,不可能像java转kotlin那样润物细无声的完成转换,所以即便工程中引入了Compose,也不太可能把现有的代码整体替换为Compose的写法。

使用Compose时,在Activity中使用的是setContent这样一个方法,而没有我们常见的setContentView,此外,默认的Activity的父类也从AppCompatActivity变为了AppCompatActivity的爷爷ComponentActivity。在setContent中,传入了我们所写的Compose方法。

(AppCompatActivity- > FragmentActivity -> ComponentActivity -> Activity)

setContent实际为Compose为ComponentActivity提供了一个扩展方法,具体实现无非是把setContentView进行了一层包装,在调用setContentView前,调用了ComposeView的setContent方法。

但是有些情况下,我们需要在同一个页面混合使用Compose和Android原生View。Compose还是提供了一些尽量兼容的一些方案,比如说我们想在一个现有的view中使用Compose,只需要在对应的Xml文件中添加一个ComposeView,然后像普通的view一样,在代码中通过findViewById获取到,然后就可以开始尽情的setContent了。

ComposeView继承自View,同时也作为Compose在Android上的宿主,负责Android和Compose在UI上的链接。

2,Compose中嵌套现有代码

当前Compose的生态还不是特别的完善,很多功能还没有做好兼容,或者一些我们常用的框架,还没有产出Compose版本,比如常用的Fresco等。或者要使用一些自定义的View,如VerticalViewPager等。

为了解决这个问题,Compose同样提供了另外一种反向的兼容功能,就是在Compose中兼容使用现有的view。

五,跨平台

Compose除了可以应用于Android外,还可以用于桌面,如下图所示,同样的一份代码,编译完成后就可以在mac上运行了。

github.com/JetBrains/C...

无法复制加载中的内容

六,引入Compose后的要求和问题

1,包体积会有3M多的提升

普通的apk

Compose的apk包

2, 版本要求

1)在项目中使用的是 Kotlin 1.4.21 或更高版本

2)minSdkVersion 大于21

参考链接

developer.android.google.cn/jetpack/Com...

mp.weixin.qq.com/s/t-RY6UZIr...

mp.weixin.qq.com/s/ssZMERV4P...

mp.weixin.qq.com/s/pRurO-7up...

mp.weixin.qq.com/s/0mAbKEuBH...

zhuanlan.zhihu.com/p/145959813

www.jianshu.com/p/ffc2745c5...

相关推荐
深海呐1 小时前
Android AlertDialog圆角背景不生效的问题
android
ljl_jiaLiang1 小时前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼1 小时前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android
落落落sss2 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
消失的旧时光-19434 小时前
kotlin的密封类
android·开发语言·kotlin
服装学院的IT男6 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽7 小时前
Android 源码集成可卸载 APP
android
码农明明7 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读
秋月霜风8 小时前
mariadb主从配置步骤
android·adb·mariadb
Python私教9 小时前
Python ORM 框架 SQLModel 快速入门教程
android·java·python