一,写在前面的话

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,代码量就会减少很多了。

Jetpack Compose

Jetpack Compose

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

三,Compose的一些特点

1,声明式

指令式编程需求告知系统每一步要做什么,详细该怎么做,经过这样的一种流程完结咱们想要的功用。经过调用 View 的某些 set 办法来更新 UI 的状况,这是一个手动更新 UI 的进程,这个进程的保护性一般会跟着 UI 复杂度的增加而增加,非常简单犯错。

声明式编程则是躲藏了怎么做这个流程,你只需求操作一些基本元素,经过组合办法,把你想要的作用搭建出来就好了。运用声明式 UI 编写界面能够极大的减少代码量,一同也提高了容错率,整个 UI 构建逻辑也变得愈加明晰、易读。

举个最简略的栗子

假设有一个带有未读音讯图标的电子邮件运用。假如没有音讯,运用会制作一个空信封;假如有一些音讯,咱们会在信封中制作一些纸张;而假如有 100 条音讯,咱们就把图标制作成好像在着火的姿态……

Jetpack Compose

运用指令式接口,咱们或许会写出一个下面这样的更新数量的函数:

Jetpack Compose

在这段代码中,咱们接收新的数量,而且有必要搞清楚怎么更新当时的 UI 来反映对应的状况。

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

Jetpack Compose

再来一个比方

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

Jetpack Compose

在 Compose 中,UI 的改写是经过从头烘托生成整个屏幕完结的,这是所有声明式 UI 改写页面的工作原理。可是 Compose 会根据数据的改动,仅针对需求改动的元素做必要的修正,经过改动数据而从头生成组合 UI 这一进程称为重组 (recompose) 。当数据更新了,虽然是从整个屏幕开端从头烘托,但与数据修正无关的元素,会坚持之前生成的实例,由于 @Composable 办法烘托是非常快的、幂等且无副作用。

非常重要的一点就是幂等,幂等的意思是指能够运用相同参数重复履行,并能取得相同成果的函数。这些函数不会影响系统状况,也不用忧虑重复履行会对系统造成改动。说白了就是与数据修正无关的 @Composable 元素在屏幕从头烘托的进程中不会被重组,仅调用或许已更改的函数或 lambda,而越过其他函数或 lambda。经过越过所有未更改参数的函数或 lambda,Compose 能够高效地重组。

2,重组(recompose)

Jetpack Compose

  • 可组合函数(带有@Compose注解的办法)能够按任何次序履行。
  • 可组合函数能够并行履行。
  • 重组会越过尽或许多的可组合函数和 lambda。
  • 重组是达观的操作,或许会被取消。
  • 可组合函数或许会像动画的每一帧一样非常频频地运转。

1)可组合函数或许按任何次序履行

Compose 能够挑选识别出某些界面元素的优先级高于其他界面元素,因而首要制作这些元素。

假如某个可组合函数包括对其他可组合函数的调用,这些函数能够按任何次序运转。

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

StartScreenMiddleScreenEndScreen 的调用能够按任何次序进行。这意味着,举例来说,您不能让 StartScreen() 设置某个全局变量(顺便效应)并让 MiddleScreen() 运用这项更改。相反,其间每个函数都需求坚持独立。

2)可组合函数能够并行运转

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

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

调用某个可组合函数时,调用或许产生在与调用方不同的线程上。这意味着,应避免运用修正可组合 lambda 中的变量的代码,既由于此类代码并非线程安全代码,又由于它是可组合 lambda 不允许的顺便效应。如下过错的比方:

@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,重组的流程

Jetpack Compose

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

Jetpack Compose

核心代码在Composer.kt

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

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

Jetpack Compose

一个正在履行的 Composable 的层级结构能够运用这个数据结构,而且咱们能够在其间刺进一些东西。

Jetpack Compose

让咱们假设现已完结了层级结构的履行。在某个时分,咱们会从头组合一些东西。所以咱们将游标重置回数组的顶部并再次遍历履行。在咱们履行时,能够挑选仅仅检查数据而且什么都不做,或是更新数据的值。

Jetpack Compose

咱们也许会决定改动 UI 的结构,而且希望进行一次刺进操作。在这个时分,咱们会把空地移动至当时位置。

Jetpack Compose

现在,咱们能够进行刺进操作了。

Jetpack Compose

在了解此数据结构时,很重要的一点是除了移动空地,它的所有其他操作包括获取 (get)、移动 (move) 、刺进 (insert) 、删去 (delete) 都是常数时间操作。移动空地的时间复杂度为 O(n)。咱们挑选这一数据结构是由于 UI 的结构一般不会频频地改动。当咱们处理动态 UI 时,它们的值虽然产生了改动,却一般不会频频地改动结构。当它们的确需求改动结构时,则很或许需求做出大块的改动,此刻进行 O(n) 的空地移动操作就是一个很合理的权衡。

Jetpack Compose

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

@Composable
fun Counter() {
 var count by remember { mutableStateOf(0) }
 Button(
   text="Count: $count",
   onPress={ count += 1 }
 )
}

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

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

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

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

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 时:

Jetpack Compose

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

现在,所有这些组目标现已占有了很多的空间,它们为什么要占有这些空间呢?这些组目标是用来办理动态 UI 或许产生的移动和刺进的。编译器知道哪些代码会改动 UI 的结构,所以它能够有条件地刺进这些分组。大部分情况下,编译器不需求它们,所以它不会向插槽表 (slot table) 中刺进过多的分组。为了阐明一这点,请您检查以下条件逻辑:

@Composable fun App() {
 val result = getData()
 if (result == null) {
   Loading(...)
 } else {
   Header(result)
   Body(result)
 }
}

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

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。这会使一个分组刺进空地并运转载入界面。

Jetpack Compose

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

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

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

此刻,代码现已会像一般的情况一样履行,而且新的 UI —— header 和 body —— 也已被刺进其间。

Jetpack Compose

在这种情况下,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)

Jetpack Compose

setContent实际为Compose为ComponentActivity供给了一个扩展办法,详细完结无非是把setContentView进行了一层包装,在调用setContentView前,调用了ComposeView的setContent办法。

Jetpack Compose

可是有些情况下,咱们需求在同一个页面混合运用Compose和Android原生View。Compose还是供给了一些尽量兼容的一些方案,比方说咱们想在一个现有的view中运用Compose,只需求在对应的Xml文件中增加一个ComposeView,然后像普通的view一样,在代码中经过findViewById获取到,然后就能够开端尽情的setContent了。

ComposeView继承自View,一同也作为Compose在Android上的宿主,担任Android和Compose在UI上的链接。

Jetpack Compose

Jetpack Compose

2,Compose中嵌套现有代码

当时Compose的生态还不是特别的完善,很多功用还没有做好兼容,或许一些咱们常用的结构,还没有产出Compose版别,比方常用的Fresco等。或许要运用一些自界说的View,如VerticalViewPager等。

为了解决这个问题,Compose相同供给了别的一种反向的兼容功用,就是在Compose中兼容运用现有的view。

Jetpack Compose

Jetpack Compose

五,跨平台

Compose除了能够运用于Android外,还能够用于桌面,如下图所示,相同的一份代码,编译完结后就能够在mac上运转了。

github.com/JetBrains/C…

Jetpack Compose

Jetpack Compose

无法仿制加载中的内容

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

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

普通的apk

Jetpack Compose

Compose的apk包

Jetpack Compose

Jetpack Compose

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…