每日一题 Flutter#1 | 说说你对声明式 UI 的理解

856 阅读7分钟

最近在着手开发我的 《匠心星问》 ,它定位是一款 题库 应用,将集题目浏览、发布、解答、做题为一体。打算第一步先以 Flutter 为核心,准备题库资源。于是诞生《每日一题》 系列,准备精心设计一些 Flutter 的问题与解答,作为题库的养料。本文的焦点是探讨:

说说你对声明式 UI 的理解

声明式 UI 是现代前端开发的重要趋势,能显著提升效率和代码可维护性。理解其本质,有助于掌握 Flutter框架的设计理念,写出更简洁、易维护的代码,是成为优秀 Flutter 工程师的基础。

--

前言

在移动端跨平台开发技术中,Flutter 无疑是近年来最受欢迎的框架之一。而它的核心理念之一,就是 声明式 UI(Declarative UI)。很多初学者在接触 Flutter 时,都会听到一句话:

Flutter 是一个声明式 UI 框架。

如今各个前端平台几乎都有了自己的声名式 UI 开发,比如

平台框架
AndroidJetpack Compose
iOSSwiftUI
鸿蒙ArkUI
WebReact、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 的世界会发生什么:

《Flutter极限测试 - 连续 setState 1000000 次会怎么样?》


四、声明式 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 站 。