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

相关推荐
方向研究9 小时前
集运指数欧线EC
区块链
I'm Jie18 小时前
Swagger UI 本地化部署,解决 FastAPI Swagger UI 依赖外部 CDN 加载失败问题
python·ui·fastapi·swagger·swagger ui
爱学习的程序媛19 小时前
【Web前端】优化Core Web Vitals提升用户体验
前端·ui·web·ux·用户体验
爱学习的程序媛20 小时前
【Web前端】前端用户体验优化全攻略
前端·ui·交互·web·ux·用户体验
紫丁香20 小时前
Selenium自动化测试详解1
python·selenium·测试工具·ui
GISer_Jing20 小时前
前端组件库——shadcn/ui:轻量、自由、可拥有,解锁前端组件库的AI时代未来
前端·人工智能·ui
小白学鸿蒙20 小时前
使用Flutter从0到1构建OpenHarmony/HarmonyOS应用
flutter·华为·harmonyos
不爱吃糖的程序媛1 天前
Flutter OH 框架介绍
flutter
软件工程小施同学1 天前
区块链论文速读 CCF A--CCS 2025 (2) 附pdf下载
网络·pdf·区块链
ljt27249606611 天前
Flutter笔记--加水印
笔记·flutter