CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Flutter 开发入门/课时/架构原则
RawBlame
课时目录12 节

Flutter 开发入门

  1. 01Flutter 环境
  2. 02Dart 语言
  3. 03Widget 体系
  4. 04架构原则
  5. 05状态管理
  6. 06动画
  7. 07网络与本地存储
  8. 08Firebase 集成
  9. 09测试
  10. 10CI/CD 与发布
  11. 11分析与监控
  12. 12综合练习

架构原则

三棵树与 SOLID。

30 分钟

架构原则

这一课你将能做到

  1. 说清楚 Widget 树、Element 树、RenderObject 树三者的关系和职责
  2. 解释 build() 何时被调用、为什么 setState 之后 UI 会刷新
  3. 通过组合(Composition)而不是继承来组织 Flutter 代码,并理解「性能 = 减少不必要的 rebuild」

为什么要学

不理解三棵树,就只能在表象层调代码:为什么 setState 没生效?为什么这个 GlobalKey 报重复?为什么这个 build 跑了 60 次?理解了底层模型,这些问题看一眼就懂。

人话:Widget 是「设计图」,Element 是「工地项目经理」,RenderObject 是「真正干活的工人」。图纸改了,项目经理对比新旧图纸,告诉工人改哪里。

先建立图像

Widget 树(不可变配置)        Element 树(可变实例)       RenderObject 树(实际渲染)
   Text('A')              ←──  StatefulElement        ──→   RenderParagraph
   Container(...)         ←──  StatelessElement       ──→   RenderDecoratedBox
   MyButton(...)          ←──  StatefulElement        ──→   RenderFlex ...

每次 setState,Flutter 都生成一棵新的 Widget 树,与老的 Element 树 diff,能复用就复用,需要更新就更新 RenderObject。

跟着做

步骤 1:观察 build 何时被调用

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) =>
      const MaterialApp(home: Scaffold(body: DemoPage()));
}

class DemoPage extends StatefulWidget {
  const DemoPage({super.key});
  @override
  State<DemoPage> createState() => _DemoPageState();
}

class _DemoPageState extends State<DemoPage> {
  int _a = 0;

  @override
  Widget build(BuildContext context) {
    debugPrint('父 build');
    return Column(
      children: [
        ElevatedButton(
          onPressed: () => setState(() => _a++),
          child: Text('a=$_a'),
        ),
        const Child(),          // const 子
        ChildNonConst(),        // 非 const 子
      ],
    );
  }
}

class Child extends StatelessWidget {
  const Child({super.key});
  @override
  Widget build(BuildContext context) {
    debugPrint('  const Child build');
    return const Text('const 子');
  }
}

class ChildNonConst extends StatelessWidget {
  ChildNonConst({super.key});
  @override
  Widget build(BuildContext context) {
    debugPrint('  非 const Child build');
    return const Text('非 const 子');
  }
}

运行 → 点按钮,控制台输出:

父 build
  非 const Child build

const Child 没有 rebuild —— 因为它的 Widget 实例没变,Element diff 时直接复用。这就是 const 的收益。

步骤 2:用 Element 验证「复用」

class ReuseDemo extends StatefulWidget {
  const ReuseDemo({super.key});
  @override
  State<ReuseDemo> createState() => _ReuseDemoState();
}

class _ReuseDemoState extends State<ReuseDemo> {
  bool _swap = false;

  @override
  Widget build(BuildContext context) {
    final w1 = Container(key: const ValueKey(1), color: Colors.red, width: 60, height: 60);
    final w2 = Container(key: const ValueKey(2), color: Colors.blue, width: 60, height: 60);
    return Column(
      children: [
        ElevatedButton(onPressed: () => setState(() => _swap = !_swap), child: const Text('交换')),
        Row(children: _swap ? [w2, w1] : [w1, w2]),
      ],
    );
  }
}

有 Key 时交换位置,Flutter 会移动 Element 而不是销毁重建。删掉 Key 再观察,行为相同(因为类型 + 位置匹配),但如果改成「StatefulWidget 内部有随机颜色」,没 Key 就会错位。

步骤 3:组合优于继承

Flutter 几乎不用继承写 UI。要复用就包一层:

class PrimaryButton extends StatelessWidget {
  final String label;
  final VoidCallback onPressed;
  const PrimaryButton({super.key, required this.label, required this.onPressed});

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        backgroundColor: Colors.blue,
        padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 14),
        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
      ),
      onPressed: onPressed,
      child: Text(label, style: const TextStyle(fontSize: 16)),
    );
  }
}

// 使用:组合进任何页面
PrimaryButton(label: '登录', onPressed: () {});

写一个 class MyButton extends ElevatedButton 是反模式:ElevatedButton 不是设计来被继承的,你会陷入它的私有逻辑。

步骤 4:把大 build 拆小

反例:

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: Column(
      children: [
        // 200 行嵌套
      ],
    ),
  );
}

正例:拆成 _Header、_Body、_Footer 三个独立 Widget。好处:

  1. 每个 Widget 可独立 const、独立 rebuild
  2. 代码可读,能复用
  3. DevTools 里能精准定位 rebuild 热点
class Page extends StatelessWidget {
  const Page({super.key});
  @override
  Widget build(BuildContext context) =>
      const Scaffold(body: Column(children: [_Header(), _Body(), _Footer()]));
}

步骤 5:用 DevTools 看 rebuild

flutter run
# 终端里按 v 打开 DevTools,或者
flutter pub global run devtools

打开 Performance 标签 → 勾选 "Track Widget Builds",操作 App,能直接看到每个 Widget 的 rebuild 次数。这是定位性能问题的第一站。

讲清楚要点

  1. Widget 是不可变配置:所有字段 final。便宜,可以重建无数次。
  2. Element 是可变的管理者:持有 State、记录父子关系、复用 RenderObject。Element 树才是 Flutter 真正的「虚拟 DOM」。
  3. RenderObject 负责布局/绘制:计算尺寸、位置、画到屏幕上。数量比 Element 少(Stateless/Stateful Element 没有自己的 RenderObject)。
  4. 更新流程:setState → 标脏 Element → 下一帧调用 build 生成新 Widget 树 → 与 Element diff → 复用 / 新建 / 销毁 → RenderObject 重新 layout + paint。
  5. 性能 = 缩小 rebuild 范围 + 让子 Widget 复用:const、拆 Widget、用 ListView.builder、把变化局部化。

你可能会懵的地方

  • 「一切都是 Widget」是不是低效? 不是。Widget 本身只是数据;真正贵的是 layout 和 paint,Flutter 复用 Element/RenderObject 来避免重复干活。

  • BuildContext 是什么? 就是 Element 的句柄。context.read / Theme.of(context) 都是通过 Element 沿树向上找。

  • setState 之后整个 App 都 rebuild 吗? 不。只有被标脏的 Element 及其子树。所以从根 setState 会重建整个 App,要尽量避免。

  • RenderObject 树我能写吗? 可以,但除非写自定义布局/绘制(比如自定义图表),一般用不到。

  • Flutter 跟 React 有什么区别? 模型几乎一样:不可变描述 + diff。差别:React 用 VDOM 在 JS 层,Flutter 直接控制 RenderObject,无浏览器开销。

错法对照

错法问题正解
class MyBtn extends ElevatedButton踩进父类私有逻辑用组合:包一个 StatelessWidget
在顶层 setState 控制所有 UI全树 rebuild,卡顿把状态下沉到最小的 StatefulWidget
把一个 300 行 build 写在一个方法里无法 const,无法精确 rebuild拆成多个小 Widget
在 build 里 print(DateTime.now()) 当状态用数据来自副作用,rebuild 不稳定状态放 State 字段,build 只读取
用 GlobalKey 当局部 Key 用性能差、跨树难维护默认 ValueKey / ObjectKey

卡点急救

  1. setState 没生效:可能在错误的 State 上调用;或被 const 阻断;用 DevTools 看哪个 Element 被标脏。
  2. GlobalKey 重复:同一个 GlobalKey 被两个 Widget 同时使用,通常是列表项里复用了 GlobalKey。换成 ValueKey。
  3. 页面卡顿(jank):打开 Performance Overlay(MaterialApp(showPerformanceOverlay: true)),看 UI 线程是否超 16ms。多半是大 Widget 反复 rebuild 或同步耗时操作。
  4. 父 build 调了但子没刷新:子的 build 取决于传入的参数,参数没变就不刷;要么参数改,要么用 Key 强制重建。

面试常问

  • Flutter 的三棵树分别是什么?为什么需要三棵? Widget(不可变配置)、Element(可变实例+挂载点)、RenderObject(布局绘制)。三棵分离是为了让 Widget 重建便宜、Element 复用、RenderObject 少动。

  • setState 之后发生了什么? 当前 Element 标脏,加入下一帧的 dirty list;frame 到来时对该 Element 重新调用 build 得到新 Widget 子树,diff 后更新或新建 Element,再触发布局和绘制。

  • BuildContext 的本质? 就是 Element。context.ancestorStateOfType 之类的 API 都是从 Element 沿父链查找。

  • 如何提高 Flutter 应用性能? 缩小 rebuild 范围(拆 Widget、const、状态下沉)、懒加载(ListView.builder)、避免在 build 做耗时操作、用 RepaintBoundary 隔离重绘、profile/release 模式下用 DevTools 分析。

自测三问

  1. Widget、Element、RenderObject 哪个是不可变的?哪个持有 State?
  2. 为什么把 build 拆小能提升性能?写出至少两点理由。
  3. const Widget 为什么能减少 rebuild?背后的判断条件是什么?

小结一句话

理解「Widget 是配置,Element 是管理者,RenderObject 是干活的」这一层模型,你就看懂了 Flutter 渲染管线的本质,性能优化也有了方向。

延伸

  • 官方架构介绍:<https://docs.flutter.dev/resources/architectural-overview>
  • 推荐视频:搜索 "Flutter Rendering Pipeline"(Flutter 官方 YouTube)
  • 下一课:状态管理,把数据流真正管起来,告别满屏 setState。
加载本节练习…
← 上一课Widget 体系下一课 →状态管理