Flutter 声明式 UI:为什么 build 会被反复调用?

文章目录

  • 摘要
    • [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:不要从名字理解,从状态归属理解",把"状态在哪里"这件事讲透,你后面写布局、路由、网络、状态管理都会顺。

相关推荐
ujainu小8 小时前
Flutter动画提效实战:animations 2.1.1 官方包全解析,4种Material动画开箱即用
flutter·animations
Web3VentureView8 小时前
Synbo观察|新西兰计划2026年将区块链纳入基础教育
人工智能·区块链
xuguiyi1008 小时前
区块链智能合约之MetaMask插件安装
区块链·智能合约
巴拉巴拉~~8 小时前
深入探索Flutter自定义绘制:从零到一实现炫酷仪表盘
flutter·ui
ujainu小8 小时前
Flutter 结合 path_provider 2.1.5 实现跨平台文件路径管理
flutter·path_provider
ujainu小8 小时前
Flutter image_picker 1.2.1 插件:图片与视频选择全攻略
flutter
鼎道开发者联盟8 小时前
鼎道AIGUI元件体系如何让DingOS实现“积木”式交互
人工智能·ui·ai·aigc·交互·gui
巴拉巴拉~~8 小时前
Flutter 通用列表项组件 CommonListItemWidget:全场景布局 + 交互增强
flutter·php·交互
speedoooo17 小时前
在现有App里嵌入一个AI协作者
前端·ui·小程序·前端框架·web app