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

相关推荐
王家视频教程图书馆3 分钟前
请求go web后端接口 java安卓端播放视频
android·java·前端
mmsx1 小时前
android opencv导入进行编译
android·人工智能·opencv
truemi.732 小时前
flutter --no-color pub get 超时解决方法
android·flutter
wwwsmef13 小时前
android源码下载
android
无泪真伤4 小时前
Android自定义控件—一个简单的播放暂停播放按钮
android
Henry_He4 小时前
修改壁纸后进桌面一直黑屏问题分析
android
花生糖@4 小时前
Android XR 是什么?解释它的功能、设备、开发工具等
android·xr
zhangphil4 小时前
Android基于Path的addRoundRect,Canvas剪切clipPath简洁的圆角矩形实现,Kotlin(1)
android·kotlin
二流小码农5 小时前
鸿蒙元服务项目实战:终结篇之备忘录搜索功能实现
android·ios·harmonyos