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...

相关推荐
*才华有限公司*几秒前
安卓前后端连接教程
android
氦客27 分钟前
Android Compose中的附带效应
android·compose·effect·jetpack·composable·附带效应·side effect
雨白1 小时前
Kotlin 协程的灵魂:结构化并发详解
android·kotlin
我命由我123451 小时前
Android 开发问题:getLeft、getRight、getTop、getBottom 方法返回的值都为 0
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Modu_MrLiu1 小时前
Android实战进阶 - 用户闲置超时自动退出登录功能详解
android·超时保护·实战进阶·长时间未操作超时保护·闲置超时
Jeled1 小时前
Android 网络层最佳实践:Retrofit + OkHttp 封装与实战
android·okhttp·kotlin·android studio·retrofit
信田君95271 小时前
瑞莎星瑞(Radxa Orion O6) 基于 Android OS 使用 NPU的图片模糊查找APP 开发
android·人工智能·深度学习·神经网络
tangweiguo030519872 小时前
Kotlin 实现 Android 网络状态检测工具类
android·网络·kotlin
nvvas3 小时前
Android Studio JAVA开发按钮跳转功能
android·java·android studio
怪兽20143 小时前
Android多进程通信机制
android·面试