Compose Multiplatform 之旅—声明式UI

从Android 开发,踏入到Compose 之旅的时候,我相信有很多小伙伴和我一样,从命令式UI 转化到声明式UI,总是不太习惯。但像Flutter、Swift、ArkUI、Compose 这些声明式UI 逐渐走向了主流。今天我们就一起聊聊Compose 的声明式UI,走进声明式UI大门。

想了解更多Compose Multiplatform项目的小伙伴,可以看看之前的文章

差异

命令式UI

  • 开发者手动操作 UI 控件,每次状态改变都需要显式更新,强调"如何做"。
  • UI是View树,每个节点都是View 和ViewGroup对象
  • 依赖View系统模块化较差,需要主要更新UI,代码量相等较多

声明式UI

  • 开发者只需声明 UI 与状态的关系,状态改变时系统会自动更新,强调"应该是什么"
  • UI是Compose UI 树,Composable 函数生成的节点组成。这些节点描述了 UI 的结构、状态和行为。
  • 高度模块化,函数易于重用,更少的代码

命令式 UI 就像是手动画画,你一笔一笔画,控制每个细节。 声明式 UI 就像描述一幅画的内容,然后系统自动画出来。

思维转变

  • 忘掉 View 引用:在 Compose 中,不再需要手动获取和操作 View。
  • 描述 UI 状态:将 UI 视为状态的结果,而不是逐步修改的对象。
  • 状态管理驱动:UI 根据状态变化自动更新,你只需管理好状态。
  • 关注数据流向:数据 -> 状态 -> UI。

核心思想

Compose 核心思想:UI 是状态的函数,即 UI = f(状态)。

函数式编程

Compose 之所以能够如此简便,得益于Kotlin的各种语法糖,像常见的回调函数,我们就可以省略无关的逻辑,直接通过大括号的形式进行回调。

kotlin 复制代码
fun doSomething(onComplete: () -> Unit) {
    println("Doing something...")
    onComplete() // 调用回调
}

fun main() {
    doSomething { 
        println("Task completed!") 
    }
}

有了这样方便的简写,就可以很直观明了的写出UI结构了,和原来的xml 的层级一样。

kotlin 复制代码
@Composable  
fun Greeting(msg: String){  
    Column {  
        Text("一级1")  
        Text("一级2")  
        Column {  
            Text("二级1")  
            Text("二级2")  
        }  
    }
}


@Composable  
inline fun Column(  
    ... 
    content: @Composable ColumnScope.() -> Unit  
) {  
   ...
}

之前第一次接触到这种形式的时候,还蛮好奇的。仔细研究,发现Column 的{} 里面其实就是一个@Composable ColumnScope.() -> Unit的函数参数,使用Kotlin的语法糖之后变得十分简洁。 框架会在初始,和合理的时机调用这个函数,获取到UI,去进行展示。

@Composable 注解

看到上面的代码,是不是很好奇@Composable注解到底有什么用。和常规JAVA的注解不一样,用于APT生成代码。这里是会在编译器编译时,对函数进行亿点点操作。

kotlin 复制代码
//原始代码
@Composable  
fun Greeting(msg: String){  
    Text("Hello $msg")  
}  

//编译器亿点点操作后
fun Greeting(msg: String,composer: Composer,key: Int){  
    composer.startRestartGroup(key)
    if (composer.changed) {
        Text("Hello, $msg", composer, 0)
    }
    composer.endRestartGroup() 
}

看上面编译后的代码,可以看到Composable注解的函数会有一个composer: Composer的参数,以及一个key。

渲染时,系统会执行一个树的遍历,从顶层的 Composable 函数开始,逐步递归地构建出由以下类型组成的树:

  • 组合树:表示 @Composable 函数的调用关系,描述 UI 的逻辑结构。
  • 布局树:由 Modifier 和布局约束生成,描述实际的布局结构。
  • UI 节点树:最终转换成显示在屏幕上的 View。

构建出来的树,不但能表示UI,还能感知状态的变化,当状态发生改变时,只通知和状态对应的树,从而避免无效的渲染提升性能。

不过这里生成的树的数据结构,不是简单的TreeNode,而是专门设计的一个SlotTable。内存占用小:SlotTable 是线性存储,避免了传统树形结构的指针开销;重组效率高:线性结构结合组层次信息,可以快速跳过未变的部分;动态性强:支持高效的增删操作,而传统树结构需要频繁调整子结点指针。这个数据结构比较复杂,感兴趣的伙伴可以去查询下相关的资料。

MutableState

上面说到的状态变化,通知UI是如何做到的呐,本质上就是一个观察者模式。

csharp 复制代码
var msg by remember { mutableStateOf("hello") }

在创建compose 需要关注变化的变量,我们会用到mutableStateOf方法,他其实就是返回了一个MutableState 类型的对象。

MutableState 本质上是一个带有观察者功能的对象,可以简化理解为:

kotlin 复制代码
class MutableState<T>(private var value: T) {
    private val listeners = mutableListOf<() -> Unit>()

    fun getValue(): T = value

    fun setValue(newValue: T) {
        if (value != newValue) {
            value = newValue
            // 通知所有监听者
            listeners.forEach { it.invoke() }
        }
    }

    fun addListener(listener: () -> Unit) {
        listeners.add(listener)
    }
}

读取值时:Compose 会自动注册一个监听器。 修改值时:setValue 会通知所有监听该状态的监听器,比如调用@Composable 相关的重组逻辑。

重组

本质上就是重新调用一下函数,绘制UI。就是在创建Comopose节点树的过程中,创建了MutableState观察者,当MutableState监听到值的变化,就会重新调用和这个状态相关的函数,重新绘制UI。

UI = State + Composable

通过Composable注解背后,做的亿点点操作,就能高效的实现根据状态返回UI。写代码时,只需牢牢的记住,UI 是状态的函数,根据状态,写好对应的UI就行。

结语

Compose 的这套机制很复杂,我这里也只是讲了一些皮毛,希望大家先有个基本的认识,一些细节后续再一起探讨。

相关推荐
Jerry说前后端1 小时前
Android 数据可视化开发:从技术选型到性能优化
android·信息可视化·性能优化
Meteors.2 小时前
Android约束布局(ConstraintLayout)常用属性
android
alexhilton3 小时前
玩转Shader之学会如何变形画布
android·kotlin·android jetpack
whysqwhw7 小时前
安卓图片性能优化技巧
android
风往哪边走7 小时前
自定义底部筛选弹框
android
Yyyy4828 小时前
MyCAT基础概念
android
Android轮子哥8 小时前
尝试解决 Android 适配最后一公里
android
雨白9 小时前
OkHttp 源码解析:enqueue 非同步流程与 Dispatcher 调度
android
风往哪边走10 小时前
自定义仿日历组件弹框
android
没有了遇见10 小时前
Android 外接 U 盘开发实战:从权限到文件复制
android