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

前言

我会用两篇文章来讲透声明式 UI,分别是《给移动开发者的声明式 UI 入门手册》,《UI 开发的革命,声明式 UI 到底好在哪里?》,今天这篇文章是第一篇,第二篇什么时候能发出还不确定,欢迎关注本公众号:FlutterFirst 以及时接收推送。

命令式 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")
        }
    }
}
weiV_Counter.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 = "偶数")
            }
        }
    }
}
flexible.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 有所帮助。以上仅仅代表我个人的理解,它不权威也可能存在谬误,还望指正。

我是中国第一位 Android & Flutter 双料 GDFE,关注我的公众号:FlutterFirst,带你起飞!我们下期见。

过去几十年以来,硬件的性能每 18 个月翻一倍,但软件的进步却慢得多,声明式 UI 是在 UI 开发这个领域难得的一次革命性的飞跃。---- 尼古拉斯 · 方兵

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容