最近在着手开发我的 《匠心星问》 ,它定位是一款 题库 应用,将集题目浏览、发布、解答、做题为一体。打算第一步先以 Flutter 为核心,准备题库资源。于是诞生《每日一题》 系列,准备精心设计一些 Flutter 的问题与解答,作为题库的养料。本文的焦点是探讨:
说说你对声明式 UI 的理解
声明式 UI 是现代前端开发的重要趋势,能显著提升效率和代码可维护性。理解其本质,有助于掌握 Flutter框架的设计理念,写出更简洁、易维护的代码,是成为优秀 Flutter 工程师的基础。
- | - |
---|---|
前言
在移动端跨平台开发技术中,Flutter 无疑是近年来最受欢迎的框架之一。而它的核心理念之一,就是 声明式 UI(Declarative UI)。很多初学者在接触 Flutter 时,都会听到一句话:
Flutter 是一个声明式 UI 框架。
如今各个前端平台几乎都有了自己的声名式 UI 开发,比如
平台 | 框架 |
---|---|
Android | Jetpack Compose |
iOS | SwiftUI |
鸿蒙 | ArkUI |
Web | React、Vue |
跨平台 | Flutter、React Native 等 |
但声明式到底意味着什么?和命令式有什么不同?Flutter 是如何实现声明式 UI 的?这篇文章将结合个人理解,来聊一聊这个话题。
一、 什么是声明式 UI?
声明式 UI 的本质,是 描述你想要的界面样子。而不是一步步命令计算机如何构建它。
举个例子,假设要显示一段文本 张风捷特烈
,并让它居中。在声明式 UI 中,可以是如下代码 :
@override
Widget build(BuildContext context) {
return Center(
child: Text('张风捷特烈'),
);
}
你只需 声明 界面上应该有哪些组件(Text、Center),Flutter 框架负责把这段声明变成屏幕上的实际 UI。
而在命令式 UI(例如早期的 Android 原生开发)中,写法如下: 你必须手动创建组件,设置属性,然后放入布局中。
TextView tv = new TextView(context);
tv.setText("张风捷特烈");
tv.setGravity(Gravity.CENTER);
layout.addView(tv);
总结一句话:
命令式 UI:告诉系统怎么做;
声明式 UI:告诉系统要什么。
打个比方: 假设你正在装修房子,你告诉装修工:
命令式的说法是:“先把这面墙粉刷白色,然后装上这扇门,之后在这放一个沙发……”
声明式的说法是:“我要一个现代简约风格的客厅,这里要有一个白色背景墙、一个深灰色沙发和一盏落地灯。”
前者是一些列的 动作指令集合
,而后者是一系列的 描述数据集合
。总而言之,声明式 UI 让你专注 描述界面应该是什么样子,而不是 怎么把它一步步做出来。
二、声明式 UI 的好处
声明式编程的最大好处就是 状态数据驱动界面更新
。Flutter 鼓励我们将 UI 视为状态的函数:
UI 是状态的函数: UI = f(state)
比如下面,当 isLoggedIn 状态改变时,调用重新构建的方法后,界面自动更新。我们控制组件的配置数据,无需控制其行为。
Widget build(BuildContext context) {
return Text(isLoggedIn ? '欢迎回来' : '请先登录');
}
相比之下,命令式 UI 往往需要手动调用 setText()、invalidate()、notifyDataSetChanged() 等方法来刷新界面。声明式将有以下优势:
更易维护
: UI 界面由状态数据驱动,表现效果只需专注于数据内容。开发效率更高
: 组件可拆分和组合,复用性高,构建界面像搭积木一样灵活。框架内部有高效的 diff 算法和局部刷新机制。更易测试和调试
: 状态逻辑可单独测试,Widget 可进行快照测试(Widget Test)更好的语义性
:组件属于界面的抽象描述,摆脱平台的差异,更易于阅读(前提是好的拆分)
三、声明式 UI 与界面更新
Flutter 界面是通过一个 Widget 树构建起来的。build
方法就像一个模板引擎,每次状态变化时都会重新执行这个方法,返回新的 Widget 树。其中 Widget 就是声名式 UI 中的描述信息。
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('You have pushed the button this many times:'),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
}
这个过程听起来像是每次都在重建 UI,其实 Flutter 并不是每次都 重绘
整个屏幕,它内部维护了三棵树:
- Widget Tree:你写的代码
- Element Tree:根据 Widget 生成的实际挂载的元素,支持 diff 比较
- Render Tree:真正决定画面显示的节点
当 build() 被调用时,Flutter 会通过 diff 算法和标脏的机制,来高效地对比前后的 Widget 树,只更新必要的部分。因此,即便你频繁地调用 setState() 重建界面,Flutter 也能保持良好的性能。在下面的视频中介绍了连续 100 万次 setState 时,Flutter 的世界会发生什么:
四、声明式 UI 的挑战
世上没有什么十全十美之物,得到了什么总得付出点什么。声明式 UI 也面临着一些挑战。
1. 状态管理复杂度上升
无论什么声名式 UI 框架,状态管理都是无法绕开的话题。毕竟 状态数据直接决定着界面的表现
。虽然 UI = f(state) 简化了界面更新,但在大型项目中,状态本身就变得复杂:
- 如何管理多个组件共享的状态?
- 如何分析需求、拆分组件、整合逻辑
- 初学者很容易将构建逻辑和业务逻辑耦合,容易代码混乱。
- 在复杂状态管理下,如果处理不好,容易导致不必要的重建;
2.思维方式转换困难
传统 UI 开发者习惯了命令式思维,一步一步修改 UI ,切换到声明式范式需要脑回路重塑。开发者可能会陷入“build 重不重要?要不要缓存?这次 setState() 会不会引发不必要的刷新?” 等思维混乱。
- 需要时间适应“状态驱动 UI”的思维;
- 不熟悉新范式容易写出低效甚至错误的代码。
- 对创建组件对象,容易产生顾虑,
3. 框架做的太多,自己知道的越少
Flutter 框架内部封装了很多内容,对初学者来说,内部的机制犹如黑箱,初期很难理解 build 的重建机制、 State 的生命周期回调。这些魔法般的表现,可能引起疑惑或者不确定性
- 难以精确控制组件生命周期;
- 副作用不易察觉,可能引发重复创建资源、内存泄漏等问题。
这些问题其实并不是声名式 UI 的问题,而是对工程化的经验把握、对 Flutter 框架的理解深度。这些都可以随着项目实践的深入得到缓解最后,我在这里给出这道题的简要回答,你有什么更好的见解,欢迎在评论区留言 ~
Q:说说你对声明式 UI 的理解
A: 声明式 UI 是指开发者只需描述在不同状态下界面应有的样子,框架会根据状态自动渲染 UI。与命令式 UI 不同,声明式 UI 不需要手动操作界面元素的增删改,而是通过状态数据驱动界面变化。Flutter、React 等都采用声明式 UI,优点是代码更简洁、易维护、易测试,缺点是对初学者理解有一定门槛。
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。