Android:Jetpack Compose

1. Android架构发展历程

Android架构演进史:MVC -> MVP -> MVVM -> MVI -> Compose

1.1 MVC架构

MVC是由Android Studio的目录结构决定的:
  • java文件夹下放置工程代码
  • res文件夹下放置资源文件(页面、图片等)

下面是MVC标准架构图
  • View:视图层,用于数据展示
  • Controller:控制层,用于业务逻辑
  • Model:数据层,用于处理远端传递的数据(接受数据、发送数据)

        但通常情况下,由Activity或Fragment持有并控制View和Controller,导致只有Model层独立,而View层和Controller层之间没有隔离,使得同一个类(Activity或Fragment)中的代码变得 臃肿、不宜维护、耦合度高,违反单一职责原则。在实际使用中,MVC标准架构就会退化成如下的MVC简化架构图。

        为了 真正分离View渲染和业务逻辑,MVP架构诞生。

1.2 MVP架构

MVP是升级版的MVC本质是面向接口编程,实现依赖倒置原则
  • View:负责数据展示
  • Presenter:负责业务逻辑【通过回调通知视图数据变化】
  • Model:负责存储和处理远端传递的数据(接受数据、发送数据)

        MVP和MVC很像,但是与Controller层不同的是,Presenter层完全从View中分离出来,作为独立的第三方类完成Model层与View层之间的双向通讯,与View层 解耦提高视图或者逻辑的 重用性。Presenter层通过接口回调与View层实现数据传递,在类似如下的场景(异步操作需要顺序执行)容易出现 回调地狱。虽然可以通过 RxJava的zip等操作符Kotlin的高阶函数等解决方案减少接口数量(实质上的回调函数 并未减少,只是转换成为另一种方式),但仍会容易出现Presenter层代码维护困难、Presenter层和View层之间回调接口过多等问题。

         譬如,使用Kotlin的高阶函数完成回调,代码可能会出现以下这种情况。遇到写出类似这种变量或方法的程序员建议直接拉出去枪毙十分钟。 因为好的代码不是自己写得爽,而是要让别人能看懂。
整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。
—— Grady Booch
推荐阅读 《代码整洁之道》
val k01: (String) -> (String) -> (Boolean) -> (Int) -> (String) -> Int = 
{ it: String -> 
    { it: String -> 
        { it: Boolean -> 
            { it: Int -> 
                { it: String -> 
                    99
                } 
            } 
        } 
    } 
}

1.3 MVVM架构

MVVM架构最早由微软提出,并应用在客户端UI框架WPF中。实际上, MVVM是MVP的简化版
  • View:负责界面展示
  • ViewModel:负责业务逻辑【通过观察者模式通知数据变化】
  • Model:负责数据存储和处理相关逻辑

        MVVM与MVP一样将逻辑层和视图层分离,为避免出现大量回调函数,使用双向绑定(只要你改动,我就敢变)的方式将View层和ViewModel层绑定,即通过让View层和ViewModel层的可观察对象(比如LiveData)进行绑定,实现数据显示的动态变化,是一种数据驱动页面思想的实现。相较于MVP,MVVM更为轻量化。因为MVVM不仅将View和ViewModel分离,还保证了ViewModel中的代码简洁不臃肿。但是在界面UI复杂的情况下,MVVM可能出现每个UI需要不同的数据,导致数据分散不容易维护

1.3.1 双向绑定实现一:DataBinding

数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。
        借助布局文件中的绑定组件,开发者可以移除 Activity 中譬如findViewById()这样的界面框架调用,使其维护起来更简单、方便。还可以提高应用性能,并且有助于 防止内存泄漏以及 避免发生 Null 指针异常。当然也可以使用 ViewBinding替代findViewById()达成上述类似的效果,这就是另外的知识点了,在此不做过多赘述。但是在项目中使用纯DataBinding方案也存在以下的问题:
  • 模板代码过多,还需要加注解
  • 在xml布局文件中掺杂Java/Kotlin代码,出问题不容易Debug,对于开发人员非常不友好
  • 需要在xml布局文件中绑定多个数据字段
   使用DataBinding的六大原则
  1. 能不用可观察变量尽量不要用
  2. 多个变量会同时改变的情况尽量使用一个可观察变量进行包装
  3. data标签能少导入一个变量尽量少导入
  4. XML布局尽量少或者不使用过多的逻辑判断
  5. 避免对一个数据进行多次绑定
  6. 严格遵守上述五条

1.3.2 双向绑定实现二:LiveData + ViewModel【Goolge标准框架流程】

        在MVVM早期,开发者使用Google官方的DataBinding框架实现数据层和视图层双向绑定。但是DataBinding框架较为复杂,一处代码修改就可能导致多处联动代码的修改。为减少模板代码,随后Google推出Jetpack组件(Lifecycle, LiveData, ViewModel, Room, Paging, Navigation)并推荐使用它集中的LiveData + ViewModel,再配合Kotlin的协程和Flow,可实现更方便的流式代码编写。

     LiveData的本质就是观察者模式,它就像一个容器 存储了某个 数据的引用。当容器中的数据发生改变,我们就能及时在回调函数中进行相应的处理。LiveData的设计存在诸多闪光点:
  • 防止内存泄露
    • 使用LiveData的观察者方法时,会将传入的Activity对象向上抽象为LifecyclerOwner,它会和我们传递进去的回调函数组合成为LifecycleBoundObserver对象。
    • 上述组合出来的对象最终实现Lifecycle组件中的LifecycleObserver接口,就可以 观测Activity或Fragment的 生命周期,进而在特定的时机将LiveData解除订阅并释放所持有的内存资源,防止出现内存泄露、内存溢出, 避免应用崩溃
    • 为LiveData配备一个Map来存储所有的观察者,用于处理多个观察者同时观察订阅。
    • 将LifecycleBoundObserver对象传入Activity或Fragment,在不同的生命周期中通知LiveData。
  • 数据更新回调默认只在前台触发
          在通知回调时,会判断LiveData的持有者(Activity或Fragment)的生命周期,默认只有在onStart()执行后和onPause()被执行前,回调函数才会被触发。换句话说, 只有页面可见的时候才会触发回调,动态改变显示的数据。当然,也只有页面可见时的数据变化有意义。

    观察者模式

    无论是DataBinding还是LiveData, 双向绑定的本质都是 对View层的观察者模式的实现。那么, 观察者模式到底是什么?
观察者模式:指多个对象间存在一对多的依赖关系,当目标对象(被观察者)的状态发生改变时,所有依赖于它的对象(观察者)都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。
  • 对于 被观察者来说,它只知道一个拥有共同接口的观察者对象列表,而不清楚每个具体的观察者是什么
  • 对于 单个观察者来说,它不关心数据从哪里来,只关心传递过来的数据如何进行处理。更专业的说法是, 事件的发射上游和接收事件的下游互不干涉,大幅 降低相互持有依赖关系所带的强 耦合
  • 对于 多个观察者来说,它们之间不存在依赖关系,可根据系统需要对观察者进行增删操作, 提高系统的 扩展性符合开闭原则
    但是观察者模式也存在以下 问题
  • 当观察者过多时,目标对象完成所有观察者的通知的时间将会大幅增加
  • 若被观察者和观察者之间存在循环依赖,则可能导致系统崩溃
  • 观察者只能知道被观察者变化了,而不清楚被观察者发生变化的原因

1.4 MVI架构

MVI架构是MVVM的升级版,更强调 数据单向流动状态集中管理,以保障 数据唯一性
  • View:负责界面的展示
  • Intent:负责封装与发送用户的操作
  • Model:负责存储视图的数据和状态

        在MVI架构中,用户的操作会被抽象成Intent, 解决了MVVM架构中ViewModel 数据分散的问题。换句话说,所有的数据Data聚合成一个统一的State,更加 方便ViewModel的管理。同时,也解决了MVVM中双向绑定造成的View和ViewModel之间的耦合问题。
        但当页面的功能较为复杂时,容易引起State的体积变大。同时,每次需要更新状态时都需要对State整体进行更新,即不支持局部刷新。以上这两点,最终会导致内存开销过大,有出现内存溢出的风险。

1.5 Compose单向数据流

        单向数据流架构下 逻辑清晰,具有 数据来源单一数据变动可溯源的优点,和Composable的编程特点十分契合。
对于一个Composable函数,主要包括两部分:
  • State:状态,即界面中可跨越Composable函数的生命周期的数据
  • Composable:可组合函数,即界面中需要被显示的各个元素,不可跨越跨越Composable函数的生命周期

        以上的模型其实是对Composable单向数据流模型,优化后的模型,通过“ 状态上提”, 分割了State和Composable函数的 职责,并将State提升至ViewModel中,而Composable中不实现任何业务相关逻辑,实现与界面中元素的低耦合性。实际上,状态上提就是将Statefule Composable改造为Stateless Composable的过程。
        对于更加复杂的业务,我们可以使用“状态分层管理策略”:

1.6 小结

        Android的架构演进大致经历了以下这些阶段:MVC -> MVP -> MVVM -> MVI -> Compose。这其中每一个新架构都是为了解决原有架构的弊端,架构的关注点也从代码转向数据流动。如果有人问哪个架构最好,我一定会说: 只有最适合项目的架构,而没有最好的架构

2. 什么是Compose

        之前说了那么多关于Android架构发展历程的事情,主要是为了铺垫本篇的主角——Compose。Compose是Google于2019年Google IO大会上推出的一个UI框架,建议配合单向数据源模型,并使用ViewModel承载跨生命周期的数据,通常被称为Jetpack Compose。

2.1 定义

Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发。
—— developers

2.2 特点

  • 一切皆函数:每个微件都可对应为函数,函数会被编译成为View,通过组合这些函数就可实现一个页面。
  • 直观:不需要手动刷新数据,只需描述页面。 当应用状态变化时,界面会自动更新
  • 更少的代码:实现相同的功能只需 原来一半的代码量,构建变得 简洁、易维护
  • 加速开发:提供大量开箱即用的Material 组件,可以使得开发者可以将注意力 聚焦于业务逻辑上,而不是在动画、主题变化等事情上。
  • 功能强大:Compose与大部分代码都兼容, 最低兼容到API21,并且View和Compose可以相互调用。
  • 结构扁平会出现 重复测量。

2.3微件

        在Compose中,像Text()、Column()、Row()、Image()等这些描述屏幕元素的函数被称为微件,类似AndridView中的TextView、LineaLayout、ImageView。看过源码的话,我们都清楚view之间都是继承关系,它们的顶级父类都是View。但是在Compose中的微件之间都没有继承关系,它们可以通过组合嵌套完成自由搭配,实现屏幕显示。

2.4 编程思想

@Composable 
fun Greeting(names: List<String>) { 
    names.forEach { name -> 
        Text(text = "Hello $name") 
    } 
}

2.4.1 声明性范式编程

        Compose属于 声明式 编程思想而不是传统的命令式编程思想。声明式编程更像是我想要什么(what),命令式编程更像是如何让去做(how)。其实就是Compose 封装了“怎么做”,使得开发者能够 更加专注于“要什么”。当然, 阅读源码了解“怎么做”,仍然是开发者提升自己的最佳方式
        在过去的几年中,整个行业已经向声明性界面模型进行转变,该界面模型简化了与界面构建和更新相关联的工程工作,其原理是在概念上 从头开始重新生成整个屏幕。它只会执行必要的更新工作,可避免手动更新有状态的视图层次结构。
        然而,“重新生成整个屏幕”面临在时间、计算能力和电池用电量等方面产生高昂成本的问题。Compose会智能地选择在任何给定时间需要重新绘制界面的哪些部分。Compose如何智能地进行选择,将在“重组”中给出解释。

2.4.2 简单的组合函数

        Compose中 使用组合函数替换视图widget,它 可以接收数据。通常使用 @Composable注解告知Compose编译器,当前函数是组合函数旨在将数据转换为界面。

2.4.3 声明性范式转变

        在传统命令式界面模型中,使用xml文件实现界面,并通过getter/setter方法以对象形式向应用逻辑提供实例化的视图对象。在Compose中,使用组合函数实现界面构建,开发者甚至可以使用带有不同参数的同一组合函数实现界面的更新。
        对于数据的传递,应用逻辑可向顶层组合函数提供数据,顶层组合函数根据层次结构继续向下层组合函数传递数据,最终实现界面更新。

        对于用户与界面的交互事件(如点击事件)的传递,事件的发生会通知应用逻辑,应用逻辑随后会改变应用状态。当状态改变后系统会使用新数据再次调用组合函数完成界面刷新,这个过程被称为 重组

2.4.4 动态内容

        Compose的页面使用Kotlin而不是XML编写,因此它可以像Kotin代码一样动态构建页面。开发者甚至可以通过if、when等逻辑判断语句显示特定的界面,编写Compose就像编写Kotlin一样灵活强大。

2.5 重组

        对于View体系中的组件,开发者通过setter更改其内部状态。在Compose中,微件树widget无状态,不提供setter和getter方法。实际上,Compose中的微件最终实现的是 LayoutNode。通过使用新数据调用可组合函数,触发重组,完成UI刷新。

        对于以下可组合函数,每当点击该按钮时,State会变化,触发重组。大范围的重组势必会影响性能,但是Compose编译器做了大量工作以保证重组的范围尽可能的小,避免无效开销。

@Composable fun Example() { 
    var text by remember { mutableStateOf("") } 
    Log.d(TAG, "Example") 

    Button(
        onClick = { 
            text = "$text $text" 
        }.also { 
            Log.d(TAG, "Button") 
        }){ 
            Log.d(TAG, "Button content lambda") 
            Text(text).also { Log.d(TAG, "Text") 
        }
    } 
} 

// 运行结果: 
// Button content lambda 
// Text

2.5.1 Compose如何确定重组范围

        经Compose编译器处理后的Composable代码其内部对State进行读取时会自动与其建立绑定,当State变化时,Compose会找到这些关联的Composable函数或者Lambda并将这些代码块 标记为inchange。在下一渲染帧来到前,Compose会触发重组,并在重组过程中执行 inchange代码块。
       可被标记为 inchange 代码块的代码:
  • 非inline并且无返回值的Composable 函数/Lambda
    • inline关键字标记的函数会在代码的编译期在调用处展开(类似C语言和汇编的宏),导致无法在下次重组时找到合适的调用入口,即无法在栈中找到inline函数的入口
    • 对于有返回值的函数,返回值会影响到调用方,无法进行单独重组,必须连同调用方一同进行重组
  • 遵循重组最小化原则
        只有会受到 State 变化影响的代码块才会参与到重组,不依赖 State 的代码不参与重组

2.5.2 重组是并发进行的    

    重组中的Composable不一定执行在UI线程中,有可能执行在后台的线程池中,有利于 发挥多核处理器的性能优势,但需要开发者 考虑线程安全问题。
“并行化”重组正在开发中,当前的Composable重组仍然发生在主线程中,但是在未来的某一时刻,重组随时会变成并行执行,这要求我们现在就要以这种观点去开发。
——《Jetpack Compose 从入门到实战》

2.5.3 重组是乐观的操作    

        某个Composable函数的参数改变就会触发重组,并且预计在参数再次改变完成前就会重组完毕。如果参数在重组完成前再次发生改变,Compose就会取消本次重组并使用新值重新进行重组。当取消重组后,Compose会舍弃界面树,因此 附带效应(可组合函数范围之外发生的应用状态变化)引起的界面变化会使得应用的状态不一致。
确保所有可组合函数和 lambda 都幂等且没有 附带效应,以处理乐观的重组。
—— developers

2.5.4 Composable函数会频繁的执行

        在某些极端的情况下,可能需要动画的一帧就需要调用一次可组合函数,如果该函数的执行时间较长,如从设备磁盘中读取数据等,极有可能造成界面的卡顿。因此,Google官方建议将执行时间较长的函数定义为参数,并使用mutableStateOf或LiveData将相应的值传给Compose。

2.6 生命周期与副作用

        Compose的DSL(Domain Specific Language, 领域特定语言)很形象地描述了UI的视图结构,它被抽象为一颗视图树,被称为Composition。当Composable函数首次执行时,这颗视图树被创建;当State状态发生变化时,Compos就会触发相应Composable的重组。

2.6.1 生命周期

         所有对界面UI 的操作,实质上都是对视图树节点的添加、修改和删除。那么,围绕着节点的添加、更新,就可以为Composable定义它的生命周期。

  • onActive:添加到视图树,即Composable首次被执行
  • onUpate:重组,即Composable跟随重组不断执行,更新视图树上相对应的节点
  • onDispose:从视图树上移除,即Composable不再被执行
        虽然Composable有时会承担页面的角色,但相较于Activity/Fragment,它更类似传统的View,并没有前后台状态切换的概念,生命周期更为简单。因此,当页面不再显示时,Composable节点会被立刻销毁。

2.6.2 副作用

        在Composable执行的过程中,我们可能会依据用户的操作执行诸如弹出Toast、访问本地或远程数据、保存或修改本地文件等操作,而这些操作往往都与UI界面的变化无关。换句话说,以上的这些操作都不应该跟随界面重组的重组而反复执行。这些与 UI界面的变化无关的行为被称为副作用
        强烈建议使用官方提供的API处理副作用,实际上是通过协程处理与UI变化无关的行为。

2.7 架构

Jetpack Compose 不是一个单体式项目;它由一些模块构建而成,这些模块组合在一起,构成了一个完整的堆栈。
—— developers

     好处:
  • 灵活控制
    层级越高的组件,使用起来更加简单,但相对限制更多;层级越低的组件,可扩展性超高,但使用起来也相对复杂。使用者可以根据需求灵活选择使用哪一层级的组件。
  • 自定义简单
    自定义高级别组件的时候,可以非常容易的通过组合低级别的组件来完成自定义的工作。

3. 为什么用Compose

这主要涉及Google官方文档中的三个问题:
  1. Compose 是一个声明性界面框架,使用更少的代码、强大的工具和直观的 Kotlin API
  2. 抛弃了原有安卓view的体系,完全重新实现了一套新的UI体系
  3. 使用可组合函数来替换view构建UI界面,只允许一次测量,避免了布局嵌套多次测量问题,从根本上解决了布局层级对布局性能的影响

3.1 Compose和View的对比

3.1.1 直观的可视化对比

        既然Compose和View都是来处理视图的,那么首先想到从布局层次角度来看,两者有什么区别。但是在看布局层次之前,我们需要简要了解从Activity到屏幕显示的过程或者结构。

  • Activity:负责管理安卓应用的用户界面
  • PhoneWindow:是Window类的具体实现,可以通过该类去绘制窗口
  • DecorView:最顶层的View,即是所有应用窗口的根 View
  • TitleView:标题导航栏
  • ContentView:Activity对应的XML布局,通过setContentView设置到DecorView中
        言归正传,我们可以通过Android Studio自带的 Layout Inspector工具,来查看页面的布局层次。

 

        对于以上同一个列表布局,分别使用了View(左侧)、Compose(右侧)进行了实现。我们可以发现,通过View实现的布局,可以一直追溯到每一个按钮,而通过Compose实现的布局,则只能追溯到ComposeView,而不是Compose内部的各个Composable,说明Composable并没有被转化为View。

3.1.2 继承与组合

        经过多年的发展,Android的界面显示仍然是那一套继承自View的组件体系。View.java本身已经变得臃肿不堪(3万行代码),并且View体系下的组件都是继承关系。比如Button就是继承自TextView,但这是不合理的,父类中可能有子类不适用的功能被一并继承下来,比如带粘贴板的按钮。
开发者需要花费大量的精力去确保各组件之间状态的一致性,这也是造成命令式 UI 代码复杂度高的根本原因
——《Jetpack Compose 从入门到实战》
        其实,对于面向对象设计模式中, 组合(Has a)优于继承(Is a),以自定义按钮的实现为例:
        配合Kotlin的DSL和扩展函数特性,Compose将UI组件变为函数,就可实现简单、强大、高可读性和高扩展性的UI组件和页面实现。

3.1.3 命令式UI和声明式UI

命令式用命令的方式告诉计算机如何去做事情( how to do),计算机通过执行命令达到结果,而声明式直接告诉计算机用户想要的结果( what to do),计算机自己去想自己该去怎么做。
——《Jetpack Compose 从入门到实战》
        Android View就属于命令式UI,它需要从View树中找到确定的控件,一步一步完成确定的UI更新事件;声明式编程是给出最终的几个界面状态,根据数据的变化完成不同界面的显示。
        命令式编程是指定一个人给我买杯咖啡,声明式编程是我想要杯咖啡,并不关心是谁买的。

3.1.4 Moidier和XML

        在Android View中,开发者使用XML文件或者直接通过代码对诸如边距、文字、字号、偏移位置等组件样式进行设置或更改。而在Compose中,开发者则可以通过设计精妙的Modifier完成对组件样式的设置。由于 Modifier是通过链式调用进行组合的,而 调用顺序不同会产生不同的Modifier链,Compose会根据Modifier链的顺序完成页面的测量布局和渲染。 Compose遍历整个Modifier链的时候就像剥洋葱一样从外(outer)到内(inner)一层层进行访问
Modifier实质上是一个接口,包含三个具体实现:
  • Modifier伴生对象:Modifier链最开头的Modifier
  • Modifier.Element:代表具体的修饰符
  • CombinedModifier:连接每个Modifier,连接的两个Modifier分别存储在outer和inner中
对于如下的示例代码,就会生成下图中的Modifier链
Modifier.size(100.dp) // 设置大小 
    .background(Color.Red) // 设置背景颜色
    .padding(10.dp) // 设置边距

3.1.5 视图树与渲染流程

        Android View和Compose的视图树类似,只不过View树是由View/ViewGroup作为节点,Compose树是由LayoutNode作为节点。
        Android组件渲染主要分为三个步骤,经过这些步骤后Android就会在手机屏幕上显示出开发者想要展示的内容。

3.1.5.1 对于Android View

  1. 测量:为测量宽高过程,如果是ViewGroup还要在onMeasure中对所有子View进行measure操作
  2. 布局:用于摆放View在ViewGroup中的位置,如果是ViewGroup要在onLayout方法中对所有子View进行layout操作
  3. 绘制:往View上绘制图像

3.1.5.2 对于Compose

  1. 组合:执行Composable函数体,生成LayoutNode视图树
  2. 布局:对视图树中每个LayoutNode完成测量并指定摆放位置
  3. 绘制:将所有LayoutNode实际绘制到屏幕上

3.1.5.3 Compose只允许一次测量

        Android View在第一次执行scheduleTraversals会调用measure两次,而且都在layout之前执行的,会导致实际对同一个View进行两次测量,这样随着View树深度的增加,测量次数会发生 指数爆炸。Compose只允许对每个LayoutNode完成一次测量,多次测量会抛异常
暂时无法在飞书文档外展示此内容

private fun LayoutNode.trackMeasurementByParent() {
    val parent = parent
    if (parent != null) {
        check(
            measuredByParent == LayoutNode.UsageByParent.NotUsed ||
                @Suppress("DEPRECATION") canMultiMeasure
        ) {
            "measure() may not be called multiple times on the same Measurable. Current " +
                "state $measuredByParent. Parent state ${parent.layoutState}."
        }
        ...
    } else {
        ...
    }
}
private fun LayoutNode.trackLookaheadMeasurementByParent() {
    // when we measure the root it is like the virtual parent is currently laying out
    val parent = parent
    if (parent != null) {
        check(
            measuredByParentInLookahead == LayoutNode.UsageByParent.NotUsed ||
                @Suppress("DEPRECATION") canMultiMeasure
        ) {
            "measure() may not be called multiple times on the same Measurable. Current " +
                "state $measuredByParentInLookahead. Parent state ${parent.layoutState}."
        }
        ...
    } else {
        ...
    }
}
        以上两个方法都是在measure()中调用的,也就是说 Compose在测量的过程中就不允许多次测量

3.1.6 布局加载流程

        不论是View还是Compose,其视图加载流程的 前半部分比较相似:通过Window创建DecorView(内部包含ContentView); 后半部分就有所不同:Android View需要通过WindowManager将DecorView加载到PhoneWindow中,并创建ViewRootImpl实例,关联DecorView和ViewRootImpl后,通过ViewRootImpl.performTraversals()方法开始绘制View树;Compose则是在ComposeView中生成AndroidComposeView作为Compose视图树的持有者,同时它也是ViewGroup,连接了View和Compose。

3.1.6.1 setContentView

3.1.6.2 setContent

        使用Compose的过程中我们发现,setContent方法替代了原来的setContentView方法。setContent方法大致流程如下:

        我们编写的可组合函数就是在setContent时添加进布局的,结合之前的Layout Inspector中观察到的层级,我们可以认为Compose没有把微件函数转换成View。
我们来看一下源码:
public fun ComponentActivity.setContent(
    parent: CompositionContext? = null,
    content: @Composable () -> Unit
) {
    // decorView的第一个子view如果是ComposeView就直接用,否则就创建一个ComposeView加载到根布局
    val existingComposeView = window.decorView
        .findViewById<ViewGroup>(android.R.id.content)
        .getChildAt(0) as? ComposeView

    if (existingComposeView != null) with(existingComposeView) {
        setParentCompositionContext(parent)
        setContent(content)
    } else ComposeView(this).apply {
        // Set content and parent **before** setContentView
        // to have ComposeView create the composition on attach
        setParentCompositionContext(parent)
        setContent(content)
        // Set the view tree owners before setting the content view so that the inflation process
        // and attach listeners will see them already present
        setOwners()
        setContentView(this, DefaultActivityContentLayoutParams)
    }
}

3.1.7 Compose与View的兼容

        Android从出现一直到发展到如今,用了十几年的时间,已经发展出庞大的生态和巨量的项目。那么,Compose作为一种全新的UI体系,是否能兼容现有的项目呢? 答案是肯定的。不仅在原有的Android项目中可以调用Compose,也可以在Compose中调用基于原生Android的各种控件,包括原生控件和自定义控件。

Compose和AndroidView相互调用

在AndroidView中使用Compose

xml文件中
<androidx.compose.ui.platform.ComposeView
    android:id="@+id/acv"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />
kotlin文件中
findViewById<ComposeView>(R.id.acv)
    .setContent {
        Text(text = "ComposeView")
    }

在Compose中使用AndroidView

@Composable
fun AndroidViewExample() {
    Column {
        Text(text = "top")
        AndroidView(
            factory = { context ->
                TextView(context).apply{
                    setBackgroundColor(android.graphics.Color.GRAY)
                }
            },
            modifier = Modifier.warpContentSize(),
            update = { textView ->
                textView.apply {
                    text = "123"
                    textSize = 16f
                    setTextColor(android.graphics.Color.BALCK)
                }
            }
        )
    }
}

延申一点

        在AndroidView()方法中,是将AndroidView包装成为一个LayoutNode,随后加载到Compose树中。由于View下继承的控件非常多,因此采用了 工厂模式将具体的View构建交给开发者。
@Composable
@UiComposable
fun <T : View> AndroidView(
    factory: (Context) -> T,
    modifier: Modifier = Modifier,
    update: (T) -> Unit = NoOpUpdate
) {
    ...
    ComposeNode<LayoutNode, UiApplier>(
        // 使用
        factory = createAndroidViewNodeFactory(factory, dispatcher),
        update = {
            updateViewHolderParams<T>(
                modifier = materializedModifier,
                density = density,
                lifecycleOwner = lifecycleOwner,
                savedStateRegistryOwner = savedStateRegistryOwner,
                layoutDirection = layoutDirection
            )
            set(update) { requireViewFactoryHolder<T>().updateBlock = it }
        }
    )
}
@Suppress("NONREADONLY_CALL_IN_READONLY_COMPOSABLE", "UnnecessaryLambdaCreation")
@Composable inline fun <T : Any, reified E : Applier<*>> ComposeNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater<T>.() -> Unit
) {
    if (currentComposer.applier !is E) invalidApplier()
    currentComposer.startNode()
    if (currentComposer.inserting) { // 插入新的LayoutNode
        currentComposer.createNode { factory() }
    } else { // 复用原来的LayoutNode
        currentComposer.useNode()
    }
    Updater<T>(currentComposer).update()
    currentComposer.endNode()
}
        而createNode{}方法实际上就是一个接口,具体实现由开发者决定,也就是AndroidView()方法中的factory参数。
        createAndroidViewNodeFactory()方法有 两个最主要的任务:
  • 生成上下文Context并交给View
  • 返回ViewFactoryHolder实例转换后的LayoutNode(ViewFactoryHolder类最终继承的就是ViewGroup)
@Composable
private fun <T : View> createAndroidViewNodeFactory(
    factory: (Context) -> T,
    dispatcher: NestedScrollDispatcher
): () -> LayoutNode {
    val context = LocalContext.current
    ...
    return {
        ViewFactoryHolder<T>(
            context = context,
            factory = factory,
            parentContext = parentReference,
            dispatcher = dispatcher,
            saveStateRegistry = stateRegistry,
            saveStateKey = stateKey
        ).layoutNode
    }
}

Compose的继承体系

        Compose继承自ViewGroup,即Android原本的UI体系。 AbstractComposeView本质是一个中间层,连接了新架构(Compose)与旧架构(View/ViewGroup)

3.2 小结(总体对比)

Android View
Jetpack Compose
类职责不单一,继承关系不合理
函数式编程思想,规避了面向对象的各种弊病
依赖系统版本,问题修复不及时
独立迭代,良好的系统兼容性
命令式编程,开发效率低下
声明式编程,DSL的开发效率更高
多次测量,影响性能
单次测量,提高性能

3. 怎么用Compose

重要的事情说三遍: 多加应用!多加应用!多加应用!
官方: 谷歌中文
《Jetpack Compose 从入门到实战》《Jetpack Compose Android全新UI编程》
知乎、简书、CSDN
郭霖、扔物线、霍丙乾bennyhuo
《代码整洁之道》

4. 相关引用

Jetpack Compose

Android架构

设计模式

视图加载与渲染

Activity相关

DSL相关

Android工具相关

Material Design 设计原则

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值