Compose-概念篇

210 阅读8分钟

什么是Compose

Google于2019年在I/O大会上首次公开发布了Compose。
android 官网 主推UI框架
响应式数据驱动,MVI架构中的UI层框架

特点
  1. 声明式编程模型 - 通过Kotlin代码描述界面应该是什么样子
  2. 基于Kotlin - Compose使用Kotlin语言构建界面,提供了惯用的Kotlin函数和编程方式。
  3. 响应式数据驱动(Remember) - 界面结构和状态保存在可变数据类中,状态变更驱动界面更新。
  4. 增量构建界面 - 通过布局组件的嵌套组合来构建界面层级结构。
  5. 可交互和动画友好 - 内置可组合的动画和手势系统。
  6. 实时预览与热重载 - 支持直接在编辑界面时实时预览,修改即可立即看到效果。
  7. 可在现有项目中使用 - 可以与View系统一起使用,无需全重写即可使用Compose。
  8. 多平台 - 未来可扩展到更多平台,如Android/iOS/Web等
声明式UI

声明式UI可以理解为一种概念或者是一种如何描述布局样式的行为。

下面是Android 布局演变过程

  • 传统命令式代码:
EditText usernameInput = new EditText(context);
usernameInput.setHint("Username");
layout.addView(usernameInput);

缺点:
1、UI复杂时臃肿,不易理解
2、如果需要更新View ,会导致 view对象到处传递
3、不能实时刷新效果

  • XML布局:
    xml 在传统View对象上,通过解析 转换,做了一些优化,加载时,通过读取布局文件,显示UI,在某种意义上XML算准声明式了
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:hint="你好"/>
</LinearLayout>

缺点:
1、XML着重于声明静态的文档结构,而不是应用的动态视图
2、XML需要手动处理视图的更新,不是响应式自动更新的。
3、XML作为标记语言,语义表达能力较弱,不能附加逻辑业务
4、XML编写的界面与应用逻辑严重耦合(需要再Activity里面findViewById)。

  • Compose
@Composable
fun EditeText() {
    var value by remember{
        mutableStateOf("")
    }
    BasicTextField(value = value, onValueChange = {
        value = it
    })
}

1、界面是状态函数的映射关系。
2、界面响应状态自动更新。
3、界面语义表达能力强,语法清晰。
4、界面与逻辑解耦。
5、支持实时预览编辑,可快速配置数据效果。

核心概念-数据重组

在 Jetpack Compose中, UI更新由两个核心完成。
重组机制: 用于维护数据的生命周期与状态变更。

@Composable
fun helloCompose(){
    //重组
    val hello by remember { mutableStateOf("Hello Compose~") }
    //UI
    Text(
        text = hello,
    )
}

对于重组,着重从两个方法切入理解 remember()mutableStateOf()

remember

由于Compose语法的设计,所有组件都是需要被@Composable修饰的方法,但是方法内的变量会随着方法进出栈被重置,所以Compose提供了remember() 方法,用来记住组件内的这些状态。

@Composable
inline fun <T> remember(crossinline calculation: @DisallowComposableCalls () -> T): T =
    currentComposer.cache(false, calculation)

可以看得出来 remembe() 是被inLine修饰的高阶函数,最终将参数内的方法,cache到Composer中。

主要作用:

  1. 记忆化结果-可以记忆计算函数的结果,避免重复计算。
  2. 避免状态值错乱-避免状态在重新组合时被改变
  3. 优化性能-可以跳过无用的重复计算和状态更改引起的重组。
  4. 控制组合生命周期-可以通过使用LaunchedEff(),手动控制组合函数的创建和销毁生命周期。
State

在重组机制中,remember 负责数据与组件之间的映射,而state负责数据的保存和状态维护。

fun <T> mutableStateOf(
    value: T,
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy()
): MutableState<T> = createSnapshotMutableState(value, policy)

可以看得出来 mutableStateOf() 除了需要被维护的数据作为参数以外,还需要一个可变快照的策略的参数,通过两个参数 返回一个MutableState,关于快照系统 后面单独再写一篇文章详细介绍吧,这里就不过多说了。
State主要作用:
1、保存组件的数据状态-State对象用于在组件内部保存数据状态。
2、驱动界面更新-当State中的数据变化时,会触发重新组合流程,使界面保持同步更新。
3、避免重复繁琐的状态管理代码
通过封装State对象,可以避免手动管理状态带来的大量模板代码。
4、实现声明式编程模型
State与UI语义上的声明式 mapping 关系,而不是命令式更新。
5、优化性能
框架可以根据State的变化优化增量更新,而不是全部重绘。
6、线程安全的数据容器
State对象本身是不可变的,可以安全地在不同线程访问。
7、实现Undo/Redo
State的变更记录可以方便实现撤销重做功能。
8、集中管理组件的状态
将多个状态属性封装在一个State对象中,便于统一管理。

核心概念-组件UI

上面笼统的讲了一下Compose中数据的维护,那作为UI框架,最重要的一定是绘制,布局。
由于Compose设计的初衷是为了跨平台的,所以UI层的绘制,会和部分原生布局系统有关,但相对是隔离的,因为没看过其他平台代码,以下只针对Android-Compose的部分类,接口介绍,由于整个UI架构比较特殊,短短篇幅不能完全介绍完,后面会在单独出一篇文章,系统性的来介绍一下Compose是如何跨平台的。
对于传统XML,整个布局系统是一棵树,而Compose是通过各个Node进行连接,由于子节点可以被多个父节点共享,所以不是严格的树结构。

构成整个UI页面一共有几个关键接口ApplierComposeUiNode,Modifier

  • Applier 负责持有组件树的根节点,并负责调度组件树的组合工作。
  • ComposeUiNode 作为组件树的节点,包含了单个组件的状态信息和内容。
  • Modifier 被组件持有,用来声明性地配置一个组件的属性、布局等信息
Applier 接口

在Android UI系统里,Applier的具体实现类是UiApplier,主要负责连接组合系统和绘制系统的关键类,负责整个UI的组合、更新以及绘制控制。

  1. 持有组件树:UiApplier中持有组成UI的组件树以及其根节点。
  2. 分发组合:UiApplier负责调用组件树的根节点进行组合,将组合工作分发到整棵组件树。
  3. 收集组合结果:组合完成后,UiApplier会收集到组件树的组合结果,也就是组件的状态、内容等信息。
  4. 生成绘制列表:UiApplier会根据组件树的组合结果,生成用于绘制的绘制指令列表。
  5. 触发重组:当检测到需要重组时,UiApplier会清理旧的组件树并重新触发组合工作。
  6. 控制绘制:最终UiApplier会根据绘制列表输出内容到目标绘制界面。
ComposeUiNode 接口

ComposeUiNode 接口的具体实现类是LayoutNode, 用于存储布局中主要信息,例如modifier、layoutDirection、density等

  1. 保存布局参数:LayoutNode会保存组件的对齐方式、边距、尺寸、位置等布局参数。
  2. 实现布局逻辑:不同的LayoutNode会有不同的布局排列逻辑,如线性布局、约束布局等。
  3. 提供布局上下文:LayoutNode作为一个布局节点,关联所有子元素并提供布局过程中的上下文。
  4. 协调布局流程:LayoutNode会负责调用子节点的测量和布局逻辑,协调其布局流程。
  5. 计算子元素布局:LayoutNode根据布局逻辑、约束条件计算出子元素的位置和大小。
  6. 保存布局结果:LayoutNode在布局完成后会保存子元素的布局相关位置信息。
  7. 标记脏节点:在参数变更时LayoutNode会标记脏以请求重布局。
  8. 控制绘图:LayoutNode根据布局结果控制对应UI元素的绘制。
  9. 响应参数变化:LayoutNode会在参数变化时通知父节点请求重新布局。 所以LayoutNode是布局系统中的关键单元,负责具体的布局逻辑实现和过程控制。
Modifer 接口
ConstraintLayout(modifier = Modifier
    .padding(12.dp)
    .fillMaxWidth()
    .background(color = Color.White, shape = RoundedCornerShape(size = 16.dp))
    .padding(12.dp)
    .clickable {
    }) {
    }

Modifer 在整个系统里面,有很多的实现类,这里就不详细讲解某一个了。
对于Node而言,Modifier的存储会更加具体,Node 针对整个布局节点的配置,其中包含Modifier, 而Modifier 会更加细节,例如:颜色,size,背景,边距等。
主要作用:

  1. 修改组件属性:可以通过 Modifier 来修改组件的大小、边距、填充、边框等显示属性。
  2. 应用转换:可以通过 Modifier 应用调整组件的缩放、旋转、偏移等转换。
  3. 设置绘制层级:可以通过 Modifier 改变组件的绘制层级。
  4. 添加交互:可以使用 Modifier 给组件添加点击、长按、拖拽等交互。
  5. 应用约束:可以通过 Modifier 给组件添加各种布局约束条件。
  6. 应用动画:可以通过 Modifier 给组件设置动画。
  7. 创建自定义Modifier:可以继承 BaseModifier 类来创建自定义的 Modifier。
  8. 组合修改器:可以使用 Modifier.compose() 来合并多个 Modifier 的效果。
  9. 改变绘制顺序:可以通过 Modifier 改变组件的绘制顺序。