文章目录
- 摘要
-
- [1、build() 到底是什么](#1、build() 到底是什么)
- [2、build() 什么时候会被调用](#2、build() 什么时候会被调用)
- [3、最小示例:观察 build 频率](#3、最小示例:观察 build 频率)
- [4、初学者最容易踩的 3 个坑](#4、初学者最容易踩的 3 个坑)
-
- [坑 1:在 build 里发网络请求 / 调接口](#坑 1:在 build 里发网络请求 / 调接口)
- [坑 2:在 build 里创建需要释放的对象](#坑 2:在 build 里创建需要释放的对象)
- [坑 3:在 build 里做重计算/重排序/大对象构建](#坑 3:在 build 里做重计算/重排序/大对象构建)
- [5、生产级写法:让 build "再多次也不怕"](#5、生产级写法:让 build “再多次也不怕”)
- [6、自检 Checklist(写完就对照)](#6、自检 Checklist(写完就对照))
摘要
很多初学者第一次写 Flutter 会疑惑:build() 怎么老是执行,是不是性能问题?这篇文章用一个最小可运行示例解释 Flutter 的声明式 UI 思想,梳理 build() 的触发时机、常见误区,以及生产中更稳的写法与自检清单。
1、build() 到底是什么
在 Flutter 里,你写的界面不是"命令式地去操作 UI 控件",而是用代码描述"此刻 UI 应该长什么样"。
可以把 build() 理解为:
-
类似 React 的 render():根据当前 state/输入,返回一棵 Widget 树(UI 描述)
-
build() 被频繁调用是正常设计:Flutter 会在需要更新界面时重新执行它,以得到新的 UI 描述
关键点:build 应该尽量是"纯函数"
同样的输入(state/props)应当得到同样的输出(Widget 树),而不是在 build 里做网络请求、写文件、读数据库等副作用。
2、build() 什么时候会被调用
常见触发来源(记住这些就够用了):
①首次显示页面:创建 widget 时会 build
②调用 setState():标记此 State 需要重建
③父组件重建:父组件 build 了,子组件可能也会跟着 build
④依赖的 InheritedWidget 变化:例如 Theme、MediaQuery(旋转屏幕/字体缩放)、Localizations 等变化,会触发依赖它们的 widget rebuild
⑤动画/帧驱动:某些动画、List 滚动中的特定组件也可能在帧更新中触发构建
结论:不要追求 build "只执行一次",而是追求"build 再多次也没问题"。
3、最小示例:观察 build 频率
新建一个 Flutter 项目,把 main.dart 改成下面这样(可直接运行):
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
void main() {
// 仅 Debug 可用:打印哪些 Widget 在 rebuild
debugPrintRebuildDirtyWidgets = true;
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
debugPrint('MyApp.build');
return MaterialApp(
title: 'Build Demo',
theme: ThemeData(useMaterial3: true),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int count = 0;
@override
Widget build(BuildContext context) {
debugPrint('HomePage.build: count=$count');
return Scaffold(
appBar: AppBar(title: const Text('Build 调用演示')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('点击按钮会触发 setState → rebuild'),
const SizedBox(height: 12),
Text('count = $count', style: const TextStyle(fontSize: 24)),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
setState(() {
count++;
});
},
child: const Text('setState +1'),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () async {
// 故意演示:异步完成后 setState 也会触发 build
await Future.delayed(const Duration(milliseconds: 300));
if (!mounted) return;
setState(() {
count += 10;
});
},
child: const Text('异步后 +10'),
),
],
),
),
);
}
}
运行后你会看到控制台不断打印:

页面初次进入时 build
点击按钮后 setState → build
异步完成后 setState → build
这些都属于正常现象。
4、初学者最容易踩的 3 个坑
坑 1:在 build 里发网络请求 / 调接口
错误示例(不要这样写):
@override
Widget build(BuildContext context) {
fetchUser(); // build 多次 → 请求多次 → 事故
return ...
}
正确做法:
一次性的请求放到 initState()
或放到状态管理层(ViewModel/Controller),让 UI 只负责展示
坑 2:在 build 里创建需要释放的对象
例如 TextEditingController、AnimationController、FocusNode 等。
这些对象应该:
-
在 initState() 创建
-
在 dispose() 释放
坑 3:在 build 里做重计算/重排序/大对象构建
例如对大列表做 map/sort、解析大 JSON、同步 IO 等。
原则:把重工作移出 build(缓存结果、提前计算、异步处理)。
5、生产级写法:让 build "再多次也不怕"
你可以把下面这几条当作日常规范:
①build 只做 UI 描述:不做副作用
②能 const 就 const:减少无意义 rebuild 成本,也更清晰
③拆小 Widget:把变化范围缩小
-
变化的内容(例如 count 文本)单独做一个 widget
-
不变的内容尽量 const
④把状态边界写清楚:谁变、谁重建,控制在最小范围
// 不变的部分独立成 const Widget
class StaticHint extends StatelessWidget {
const StaticHint({super.key});
@override
Widget build(BuildContext context) {
return const Text('这段永远不变,应该尽量 const');
}
}
6、自检 Checklist(写完就对照)
-
build 里没有网络请求/数据库操作/写日志到文件等副作用
-
controller/focus node 等对象不在 build 里 new
-
大计算、大列表处理不在 build 内同步执行
-
能 const 的 widget 都 const 了
-
页面卡顿时先用日志/DevTools 定位,而不是"猜 build 太多"
下一篇我建议接 "Stateless vs Stateful:不要从名字理解,从状态归属理解",把"状态在哪里"这件事讲透,你后面写布局、路由、网络、状态管理都会顺。