给移动开发者的声明式UI入门手册

ed365cf591beb005086c2ced2013200f.jpeg

/   今日科技快讯   /

三星电子有限公司近日宣布,开始在位于韩国的华城工厂大规模生产3纳米半导体芯片,是全球首家量产3纳米芯片的公司。与前几代使用FinFET的芯片不同,三星使用的 GAA(Gate All Around)晶体管架构,该架构大大改善了功率效率。

/   作者简介   /

明天就是周六啦,提前祝大家周末愉快!

本篇转自hackware的博客,文章主要介绍了声明式UI,相信会对大家有所帮助!

原文地址:

https://mp.weixin.qq.com/s/sdHsOqMyhFdeB9mWD01VzA

/   命令式UI的由来   /

和声明式 UI 相对应的是命令式 UI,也就是我们传统的 UI 编程模式。

在声明式 UI(响应式 UI)这个概念还没出现之前,我们似乎并没有将其称作命令式 UI。它好像是为了甄别两者的不同而凭空造出的概念。

在我看来,命令式 UI 是由面向对象编程思想自然而然地演化出来的。面向对象编程思想讲究封装,继承和多态。这三大特性在命令式 UI 中表现得淋漓尽致。我们来分析一下:

封装


任何 UI 系统的核心职责是测量、布局、绘制和事件反馈。测量是为了计算出 UI 元素的大小,布局是为了计算出 UI 元素在屏幕中的摆放位置。绘制是为了将 UI 元素真正呈现到屏幕上,事件反馈是为了监听来自 UI 系统内部或外部的事件来更新 UI,串联用户的交互流程以完成用户的工作。


我们将界面中的元素封装成 View,让其承担上述四个职责。比如在 Android 中,onMeasure、onLayout、onDraw、onTouchEvent 都是 View 类的方法。


继承


UI 元素的种类是丰富的,比如按钮、文本、图片、进度条、单选框、复选框、SeekBar、下拉刷新、列表等等。我们不可能让一个 View 承担所有的功能,因此我们通过继承 View,重写部分核心职责方法来让不同的 View 承担不同的功能,基本上做到一个 View 只干一件事情。


这里仍然会涉及到封装,因为不同的 View 会有不同的属性。这些属性体现在成员变量上。


多态


多态在 UI 系统中体现得不多,最普遍的场景是对于某个 View,当对它进行测量时,如果它还有子 View,那么子 View 也会递归的被测量,而子 View 的类型可能是多种多样的,因此不同的子 View 对于同样的测量事件,会给予不同的反馈。


总结


当我们将 UI 元素封装成 View 以后,自然而然的会使用 Setter 来更新它的状态,使用 Getter 来获取它的状态:


TextView textView = new TextView();
textView.setText("hello world");
String text = textView.getText();

这就是命令式的,你对 Setter 方法的每一次调用就好像是对 View 发出一个个命令一样。你始终在直接操作承担渲染的 View 对象。这其实就是命令式 UI 和声明式 UI 的本质区别:


命令式 UI 直接操作渲染对象,而声明式 UI 不直接操作渲染对象。大家先记住这个核心结论,我们接着往下分析。实际上远没有这么简单。虽然只是操作对象发生了变化,但这却带来了革命性的转变。


/   什么是声明式UI   /


为了更好的向大家阐释清楚声明式 UI 的原理,我发明了两个词,渲染前端渲染后端


由于声明式 UI 不直接操作渲染对象,而是操作渲染对象的描述,这个描述即 Widget。它是轻量级的 UI 的蓝图。这里的渲染前端就是 Widget 树,而渲染后端则是 View 树(weiV)或 RenderObject 树(Flutter)。渲染后端由渲染前端生成,它负责 UI 元素的测量、布局、绘制、事件反馈。


总结下来就是:


在命令式 UI 中,渲染前端和渲染后端都由 View 树承载。而在声明式 UI 中,渲染前端由 Widget 树承载,渲染后端由 View(RenderObject) 树承载。


我举一个形象的例子:


{
    "nickName": "hackware",
    "realName": "陈方兵",
    "age": 29,
    "sex": "男"
}

这段 JSON 文本是对一个 Person 的描述,它并不是真正的 Person 对象,我们可以使用以下的代码将其转换成真正的 Person 对象:


Person person = new Gson().fromJson(personDesc, Person.class);

这段 JSON 文本就相当于 Widget,而 Person 对象就相当于 View(RenderObject)。懂了吧?


那为什么不直接操作 View,而是操作它的描述 Widget 呢,这样做的好处是什么?


这个问题值得深入的展开讨论,因此我打算在后期的《UI 开发的革命,声明式 UI 到底好在哪里?》这篇文章中来细讲。今天我们只做个初探,先给出最明显的两个好处:


不再需要 findViewById


由于你始终操作的是 UI 的描述,每当需要更新 UI 时,只需重新生成一份新的描述(一颗新的 Widget 树)即可。新的 Widget 树会和旧的 Widget 树作比对(Virtual DOM Diff)并只把变化的部分同步到渲染后端。


以 weiV Counter 为例:


class WeiVCounterKotlinActivity : WeiVActivity() {
    private var count = 0
    private val maxCount = 5
    private val minCount = 0

    override fun build(buildCount: Int) = WeiV {
        Flex {
            it.orientation = FlexDirection.VERTICAL

            Button(text = "Add count", enable = count < maxCount, onClick = {
                setState {
                    count++
                }
            })

            Button(text = "Sub count", enable = count > minCount, onClick = {
                setState {
                    count--
                }
            })

            Text(text = "count = $count")
        }
    }
}

43ef8974a9ea03a7d3a17e99c4ab135e.gif

点击 Add count 或 Sub count 按钮时 Text 的文本就会发生变化,这里并没有 findViewById 和 setText。

调用 setState 方法会先执行 Lambda 表达式将数据改变,这里的数据称为 State。而后 build 方法会重新执行以生成新的 Widget 树,新旧 Widget 树做比对并对 Text 所对应的 TextView 调用 setText。当 count 达到最大值时,比对会导致 Add count 按钮被调用 setEnable(false),当 count 达到最小值时,比对会导致 Sub count 按钮被调用 setEnable(false)。

极其灵活的组织子View


在 Android 中,在 XML 里只能静态的组织子 View。虽然 DataBinding 出现以后我们可以在 XML 使用简单的表达式,但仍不够灵活。我们先来看看声明式 UI 下组织子 View 的灵活性吧:

class WeiVCounterKotlinActivity : WeiVActivity() {
    private var count = 0
    private val maxCount = 5
    private val minCount = 0

    override fun build(buildCount: Int) = WeiV {
        Flex {
            it.orientation = FlexDirection.VERTICAL

            Button(text = "Add count", enable = count < maxCount, onClick = {
                setState {
                    count++
                }
            })

            Button(text = "Sub count", enable = count > minCount, onClick = {
                setState {
                    count--
                }
            })

            Text(text = "count = $count")

            repeat(count) {
                Text(text = "$it")
            }

            if (count % 2 == 0) {
                Text(text = "偶数")
            }
        }
    }
}

b2e4a0be49e3a01e34c61d38fae5b681.gif

这个 Demo 是不是很神奇,你可以使用通用编程语言的任何语法来组织子 View。我想不需要我再做解释了吧。

当然声明式 UI 的好处还不止这些,我们后面再深入探讨,接下来我们简单讲一下声明式 UI 的原理。

/   声明式UI的原理   /

回到声明式 UI这个词本身,现在你也许对它的概念已经明朗了。

我们不再使用 Setter 来直接更新 UI,而是在需要更新 UI 时,创建一颗完整(或部分)的 Widget 树来声明出 UI 该是什么样子。这就是声明式 UI 的本质。

声明式 UI 的原理可以简单概括为一个公式:

UI = F(State)

和 UI 相关的数据被称为状态,UI 总是根据状态生成。

声明式 UI 的核心运行原理就在于公式中的 F 函数,主要是 Virtual DOM Diff 算法,大家有兴趣可以去看看 Flutter 或 weiV 的 Diff 算法(只有 240 行代码 )

Virtual DOM Diff 的核心流程(同级 Diff)如下:

  1. 新旧 Widget 都不为 null 时,如果新旧 Widget 类型和 Key 相同,则使用新的 Widget 中的数据更新旧的渲染对象

  2. 新旧 Widget 都不为 null 时,如果新旧 Widget 类型或 Key 不同,则移除旧的渲染对象,创建新的渲染对象

  3. 如果旧的 Widget 为 null,新的 Widget 不为 null,则创建新的渲染对象

  4. 如果旧的 Widget 不为 null,新的 Widget 为 null,则删除旧的渲染对象


/   结束语   /


好了,洋洋洒洒两千多字,希望能对你理解声明式 UI 有所帮助。以上仅仅代表我个人的理解,它不权威也可能存在谬误,还望指正。

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

Android筑基,Kotlin扩展函数详解

我将自定义 ClassLoader 的坑都踩了一遍

欢迎关注我的公众号

学习技术或投稿

f65a4b5f4ee6191b4779995f5287465d.png

53ea62d211f9fcdbbeca714f87d93233.jpeg

长按上图,识别图中二维码即可关注

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值