2dc84814aec7b72013b4355355355f09
重学安卓:一通百通 “声明式 UI” 扫盲干货

往期回顾专栏目录更新动态优惠政策版权须知

温馨提示:如果这是第一次接触《重学安卓》,可通过上述链接来访问和快速了解《重学安卓》专栏、获取它的目录、试读内容,以及了解它的最新动态 和 发展状况。

截至目前,专栏已对 体系化文章 做了 1970 余次修订,数十位群友告诉我 受专栏的启发 他们也开启了写作之路。群里不定期会有小伙伴讨论适配问题、分享原创的开源库 和 提供内推机会,订阅后可随时进群交流。

·

重要提示

阅读本文的最佳时机是,您已吃过 阅读 “源码” 或 “源码分析文” 时 找不到头绪的苦

您没吃过苦,您先不要着急阅读本文。

在您吃够这方面的苦后,您才有机会发现,本文正是专用于解决 “如何找到正确打开方式” 的困扰。

我们绝不通篇贴源码,而是基于广泛的实践和反思,在累积过大量样本 乃至足以排除掉所有干扰信息后,点到为止地揭露 声明式 UI 框架最为核心的本质,方便您理解其真实的存在意义,乃至可以笃信地将其用在项目中。

前言

Canvas 总共生了两胎,一胎是 View 体系,二胎是 Compose 体系,

上一期《过目难忘 Android GUI 关系梳理》,我们用 “通俗易懂” 的方式解析了 “View 体系” 每个层级工具的 存在意义 及 相互间关系,鉴于 Compose 离 “普及” 还有很长一段距离,这一期我们先来介绍 Compose 框架背后的本质。

声明式 UI 的由来

事实上,React、Flutter、SwiftUI、Jetpack Compose,这些 UI 框架有个共同鼻祖,即一个名为 elm(不是 “饿了么”)的 UI 框架,是它最早确立并推行了 “声明式 UI” 设计理念。

注:“声明式 UI” 这个名称,最初是谁定义的,暂时没有找到来源,目前各大官网都是称其为 “声明式 UI”,基于我个人理解,其更精确的表达是,“数据驱动 UI 框架下的声明式 UI 分支”,当然,最终我们还是简称为 “声明式 UI”。

—— 那么 “声明式 UI” 到底长啥样?为什么要使用 “声明式 UI”?它相比传统 “View” 有何优势?它的本质或者说存在意义又是什么?

—— React、Flutter、SwiftUI、Jetpack Compose,表面上每个都长得不一样,如何透过表象看穿它们的 “流程和机制”,从而能自行领悟代码 该怎么写、往哪写、怎么改?

所以今天我们就来统一解析 “声明式 UI” 背后的本质,相信阅读后能让你醍醐灌顶。

文章目录一览

  • 前言
  • 声明式 UI 的由来
  • 声明式 UI 的本质是 “函数式编程”
    • “纯函数” 是 “函数式编程” 的基石
    • “函数式编程” 引入前的混沌世界
    • “函数式编程” 为什么能 “彻底” 解决这类问题?
    • 引入 “函数式编程” 后的世界
  • 所以为什么会有 “数据驱动 UI 框架”?
    • 声明式 UI 的运作流程是怎样的?
    • 数据驱动 难以替代的好处
  • 函数式编程的局限
  • Note 2020.07.27 加餐:
  • 现有条件下解决 “视图调用一致性问题” 的最优解
    • 1.Java + DataBinding 严格模式
    • 2.Kotlin + ViewBinding
    • 3.Kotlin DSL 动态布局
  • Note 2020.07.31 加餐:
    • 通过 “函数式编程思想” 秒懂 Compose 流程机制
  • 综上

声明式 UI 的本质是 “函数式编程”

声明式 UI、Java8 Stream、RxJava 等,本质上都是函数式编程。

许多文章,从 RxJava 时代起,就照搬了官网的说法,说 RxJava 是一种 “响应式编程” 框架。

事实上,“响应式编程” 是一种 额外发明的称谓,“响应式编程” 这个概念 不仅无助于我们 正确理解事实,反倒徒添困扰 —— 为什么要用响应式编程 —— 没有人真的能够 就这个脱离事实的概念 把事情给你交代清楚

所以,我们不妨回归它最真实的本质 —— 函数式编程,好从根源的根源找寻线索,从而有机会 知其所以然、而顿悟般地 知其然。

“纯函数” 是 “函数式编程” 的基石

老规矩,先讲结论:

声明式 UI 是 “数据驱动 UI 框架” 的一个分支,声明式 UI 的实现方式是函数式编程,且函数式编程的基石是纯函数

函数式编程的存在,主要是为了 从范式层面彻底解决 过程的一致性问题

数据驱动 UI 框架 主要是为了解决 视图调用的一致性问题

如果光是阅读了以上三点,你还是不理解的话,那接下来我就分别介绍 99% 的网文都不曾介绍的真实状况,来方便你迅速地建立起感性的认识。

“函数式编程” 引入前的混沌世界

以下是我们最常见的用法:

通过 findViewById 拿到 TextView 实例,使其作为 Activity 内部的共享成员变量,为多个方法所调用,来改变 TextView 的状态。

这造成了什么问题呢?

一旦 TextView 成为共享变量,被分散到各个方法中,后续就不可控了,因为当执行方法 B 时,方法 A 是无法知道 方法 B 中对共享变量做了什么,却无差别地承受 共享变量被修改 所带来的影响,

比如当 方法 B 将 TextView 置空,那么 方法 A 调用 TextView 实例时 将面临 null 安全问题。

可能有人会问,这种问题 通过手动判空 不就可以了?

事实上,在软件工程的背景下,任何微小的隐患 都可能被 “指数级” 地放大

一个软件的页面可能有数十个,每个页面的控件也可能多达十数个,而每个控件都可能分散在多个方法中,这种情况下,一味地寄希望于手动判空,是成本极高 且存在 一致性风险 的 —— 总会有疏忽的时候,总会有 方法 A 记得判空,而方法 B 忘记的时候。

“函数式编程” 为什么能 “彻底” 解决这类问题?

因为函数式编程基于纯函数。

什么是纯函数?为什么纯函数最终能解决这个问题?

简单来说,纯函数相比普通函数的特征是:

只有一个入口 & 只有一个出口

啥意思呢,就是说:

函数只从 参数列表 这唯一入口 接收外来的初值,

并且只从 返回值 这唯一出口 返回结果数据。

除此之外:

不在函数内部执行与运算本身无关的其他操作,

不在函数内部调用外部变量、不修改从外部传进来的变量,

抛开上述 TextView 的案例,我们先来举个例子看看纯函数本身:

如此一来,在调用该函数时,关注点就只有 入口和出口 这两处,而不至于蔓延到整个程序,从而 不可预期情况发生的概率 从 99.9% 骤减为 0 —— 调用者无须了解细节 即可放心调用

引入 “函数式编程” 后的世界

函数式编程,除了单个纯函数,也可以是多个纯函数的链式编程,

即,上一个函数的输出 作为下一个函数的输入

整个链同样只有 开头的入参 这一个入口,和 末尾的回调 这一个出口,

至此,我们得以顿悟般地理解:为什么 RxJava 或 Java8 Stream 是这样书写、为什么会和常规的 “侵入式” 思维发生 “别扭” …

是的,正因为人们无意识地习惯了 “自由散漫” 的编程,而从未意识到还有 “集中管理” 的范式的存在,于是在遇到 RxJava 链式编程的第一感觉就是,“自由散漫” 在此行不通了,

而这也恰恰是 函数式编程 的存在意义:从范式层面彻底解决 过程的一致性问题 —— 有怎样的输入,就有且只有怎样的输出,关注点只有这两处,因集中管理 而从根本上杜绝了不可预期的结果

划重点 👆 👆 👆

所以为什么会有 “数据驱动 UI 框架”?

将 “视图系统” 设计为 “数据驱动”,反映了源码设计者 对 彻底解决软工安全问题 的不懈追求。

正是由于这锐意进取的 "死磕精神",使得软件开发得以不断优化和改进。

通过 "混沌世界" 一节的分析,我们已确知,在 “自由散漫” 的环境下,因实例的分散,而使不可预期的风险被大幅扩散

加上我们在 《从被误解到 “真香” 的 Jetpack DataBinding》 一文中提到的,"横竖屏布局的控件存在差异" 这一经典案例,如采用 "自由散漫" 的方式,同样会埋下视图调用 的一致性问题。

那怎么办呢?

声明式 UI 的运作流程是怎样的?

很简单 —— 使用 基于函数式编程 的 数据驱动 UI 框架

Tip:非 DataBinding。DataBinding 是另一种方式 —— 通过 “自动化生成中间代码”,来规避人工调用视图实例造成的一致性隐患,但既然是额外的工具,便没能迫使 原视图系统 遵循函数式编程。

其存在意义主要就是为了解决 视图调用的一致性问题

从唯一入口输入 “声明树”,在内部通过对比新旧声明的差异,自行完成 “视图实例树” 的构建或调整,并输出最终结果到屏幕,使得开发者 没有机会 基于入侵式思维 直接对视图实例本身 进行调用和赋值,任何改变,只能从唯一入口注入 —— 用数据 来驱动整个过程的改变

top Created with Sketch.