Flutter之Widget/State 入门文档

Flutter 教程 ------ 写给前端开发者

前置知识:已阅读 dart_tutorial.md。

核心心法:Flutter ≈ React 的思想 + 原生的渲染。你会 React/Vue,Flutter 的心智模型一天就能建立。


目录

  1. [Flutter 是什么?和 Web 前端的关系](#Flutter 是什么?和 Web 前端的关系)
  2. [最核心的心智模型:一切都是 Widget](#最核心的心智模型:一切都是 Widget)
  3. [解读本项目的 main.dart(逐行)](#解读本项目的 main.dart(逐行))
  4. [StatelessWidget vs StatefulWidget](#StatelessWidget vs StatefulWidget)
  5. [setState ≈ useState](#setState ≈ useState)
  6. [StatefulWidget 生命周期(对比 React)](#StatefulWidget 生命周期(对比 React))
  7. [布局系统:Row / Column / Stack ≈ Flexbox](#布局系统:Row / Column / Stack ≈ Flexbox)
  8. [常用 Widget 速查](#常用 Widget 速查)
  9. [样式思维转变:没有 CSS,一切都是 Widget](#样式思维转变:没有 CSS,一切都是 Widget)
  10. [BuildContext 与 InheritedWidget ≈ Context API](#BuildContext 与 InheritedWidget ≈ Context API)
  11. [页面导航 ≈ Router](#页面导航 ≈ Router)
  12. 状态管理
  13. [Key:列表 key 的知识可以直接迁移](#Key:列表 key 的知识可以直接迁移)
  14. 工程结构与常用命令
  15. [热重载 Hot Reload](#热重载 Hot Reload)
  16. 前端概念对照速查表
  17. 动手练习

1. Flutter 是什么?和 Web 前端的关系

Web 前端 Flutter
UI 描述方式 JSX / 模板(声明式) Widget 组合(声明式,思想同 React)
渲染目标 浏览器 DOM 自绘引擎(Skia/Impeller),不依赖原生控件
布局语言 CSS Flexbox Row/Column/Flex(概念同 Flexbox,写法是 Widget 参数)
样式 CSS Widget 的构造参数(padding、decoration...)
事件处理 onClick 属性 onPressed / GestureDetector 回调
状态管理 useState / Redux / Pinia setState / Provider / Riverpod / BLoC
跨平台 浏览器 iOS / Android / Web / 桌面 一套代码

关键认知:Flutter 不用原生控件,也不转 Webview,而是像游戏引擎一样自己画每一帧。 所以它不需要 CSS/DOM,性能和一致性都更高,代价是"样式"全用代码写。


2. 最核心的心智模型:一切都是 Widget

  • Widget = UI 的不可变配置(类似 React 元素 / Vue 的 vnode 描述)。
  • UI = Widget 树嵌套组合,build() 方法就是你的 render 函数。
  • 三棵树(了解即可,面试常问):
    • Widget 树:不可变的配置描述(每次 rebuild 都重新创建,很轻量)
    • Element 树:Widget 的实例化,管理生命周期和状态(≈ React Fiber)
    • RenderObject 树:真正负责布局和绘制
dart 复制代码
// Widget 就是层层嵌套的"配置对象",读法:由内向外 or 由外向内都行
Center(                    // 水平垂直居中容器
  child: Text('Hello'),    // 文本
)

和 JSX 对比:JSX 用 <div><span></span></div>,Flutter 用 Container(child: Text(...))。子组件统一叫 child(单个)或 children(多个),写多了形成肌肉记忆。


3. 解读本项目的 main.dart(逐行)

下面是本项目的 lib/main.dart 结构(已省略注释),每一行都值得认识:

dart 复制代码
import 'package:flutter/material.dart';  // ① Material Design 组件库(还有 cupertino = iOS 风格)

void main() {
  runApp(const MyApp());   // ② runApp:把根 Widget 挂载到屏幕,≈ ReactDOM.render
}

class MyApp extends StatelessWidget {          // ③ 无状态根组件
  const MyApp({super.key});                    // ④ 构造函数语法糖,key 是 Widget 身份标识

  @override
  Widget build(BuildContext context) {         // ⑤ build ≈ render 函数
    return MaterialApp(                        // ⑥ 应用级配置:路由/主题/国际化
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: .fromSeed(seedColor: Colors.deepPurple),  // ⑦
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'), // ⑧ 首页
    );
  }
}

class MyHomePage extends StatefulWidget {      // ⑨ 有状态组件的"配置类"
  const MyHomePage({super.key, required this.title});

  final String title;                          // ⑩ Widget 字段全部 final(不可变配置)

  @override
  State<MyHomePage> createState() => _MyHomePageState();  // ⑪ 创建状态对象
}

class _MyHomePageState extends State<MyHomePage> {   // ⑫ 状态类(私有,_ 开头)
  int _counter = 0;                            // ⑬ 这里才放可变状态

  void _incrementCounter() {
    setState(() {                              // ⑭ ≈ useState 的 setter
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {         // ⑮ setState 后重新执行这里
    return Scaffold(                           // ⑯ 页面脚手架:appBar+body+FAB 布局
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),             // ⑰ widget.xxx 访问外部传入的配置
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: .center,          // ⑱
          children: [
            const Text('You have pushed the button this many times:'),
            Text('$_counter', style: Theme.of(context).textTheme.headlineMedium),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,          // ⑲ 点击回调
        child: const Icon(Icons.add),
      ),
    );
  }
}

你注意到的两个奇怪写法

⑦ .fromSeed(...) 和 ⑱ .center ------ 这是 Dart 3.10+ 的点简写(dot shorthand):编译器能从上下文推断类型时,可以省略类型前缀:

dart 复制代码
colorScheme: .fromSeed(seedColor: Colors.deepPurple)
// 等价于
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)

mainAxisAlignment: .center
// 等价于
mainAxisAlignment: MainAxisAlignment.center

JS 里没有对应物,看到 .xxx 开头就是这种简写。

其他关键点

  • ⑨⑫ StatefulWidget 被拆成两个类:Widget 类(不可变配置)+ State 类(可变状态)。这是 Flutter 最特殊的模式,先记住,第 4 节解释为什么。
  • ⑰ widget.title :在 State 类里通过 widget 属性访问外部传入的配置(因为 title 不在 State 里)。
  • ⑯ Scaffold:Material 页面脚手架,提供 appBar / body / 底部导航 / FAB 等标准布局坑位。

4. StatelessWidget vs StatefulWidget

StatelessWidget StatefulWidget
有无可变状态 无 有(存在 State 对象里)
React 对应 纯函数组件 class 组件 / useState 组件
何时用 纯展示、props 进 UI 出 计数器、表单、动画、网络数据

为什么拆成两个类? Widget 是不可变的(immutable),每次重建都新建 Widget 实例;而 State 是持久的,挂在 Element 树上不随 rebuild 销毁。这样 diff 时用"新 Widget 配置 + 旧 State"重建 UI,状态得以保留。

dart 复制代码
// 标准三件套模板(背下来,天天写)
class Counter extends StatefulWidget {
  const Counter({super.key});

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return TextButton(
      onPressed: () => setState(() => count++),
      child: Text('$count'),
    );
  }
}

5. setState ≈ useState

dart 复制代码
// React:
// const [count, setCount] = useState(0);
// setCount(count + 1);

// Flutter:
int count = 0;

void increment() {
  setState(() {          // 传入一个回调,在回调里改状态
    count++;
  });
}

三条铁律:

  1. 改状态必须包在 setState 里 ,直接改 count++ 不会触发重绘(不报错但 UI 不更新,新手第一大坑)。
  2. setState 的回调要保持轻量------只做状态修改;重活放在外面做。
  3. 频繁重建时用 const Widget(如 const Text(...))标记不变的部分,Flutter 会跳过它们。

6. StatefulWidget 生命周期(对比 React)

dart 复制代码
class _MyState extends State<MyState> {
  // ≈ useEffect(() => {...}, [])  ------ 挂载时执行一次
  @override
  void initState() {
    super.initState();
    // 初始化状态、订阅事件、发起网络请求
  }

  // ≈ useEffect(() => { return cleanup }, [deps])
  @override
  void didUpdateWidget(MyState old) {
    super.didUpdateWidget(old);
    // 父组件传入了新配置
  }

  @override
  Widget build(BuildContext context) { ... }

  // ≈ useEffect 的 cleanup ------ 卸载时执行
  @override
  void dispose() {
    // 取消订阅、销毁 controller(**必须**,否则内存泄漏)
    super.dispose();
  }
}
React Flutter
useEffect(fn, []) initState()
useEffect cleanup dispose()
useEffect(fn, [dep]) didUpdateWidget() / didChangeDependencies()
useRef TextEditingController 等各类 controller
组件销毁 dispose()

7. 布局系统:Row / Column / Stack ≈ Flexbox

没有 CSS,布局就是嵌套 Widget:

dart 复制代码
// Column = flex-direction: column
Column(
  mainAxisAlignment: MainAxisAlignment.spaceAround, // 主轴 ≈ justify-content
  crossAxisAlignment: CrossAxisAlignment.center,    // 交叉轴 ≈ align-items
  children: [ Text('A'), Text('B') ],
)

// Row = flex-direction: row(主轴变成水平)
Row(
  children: [
    Text('左'),
    Spacer(),                 // ≈ flex-grow 占位弹簧
    Text('右'),
  ],
)

// Stack = position: relative + absolute
Stack(
  children: [
    Image.network(url),
    Positioned(right: 8, top: 8, child: Badge()),   // ≈ absolute 定位
  ],
)

// 常用"原子"布局 Widget
Container(...)    // div:颜色/边距/圆角/装饰全能干(但成本高,能用专用的就别用)
Padding(...)      // ≈ padding
Align(...)        // ≈ text-align / margin auto
Expanded(flex: 1, child: ...)   // ≈ flex: 1
Flexible(...)                   // ≈ flex-shrink
SizedBox(width: 10, height: 10) // 固定尺寸 / 占位间距(≈ margin 的常用替代)

主轴/交叉轴记忆法 :Column 主轴是竖直,Row 主轴是水平;mainAxis 沿着排列方向,crossAxis 垂直于它。

常见布局坑

  • Row/Column 里放没有约束的内容会溢出(屏幕右侧黄黑条纹 = overflow),用 Expanded / Flexible 包住。
  • ListView(可滚动列表)里嵌 Column 时注意滚动方向冲突。
  • 文本换行:Text 默认不换行,需要 TextOverflow.ellipsis 或外层 Flexible。

8. 常用 Widget 速查

dart 复制代码
// ── 文本与图片 ──
Text('hello', style: TextStyle(fontSize: 16, color: Colors.red, fontWeight: FontWeight.bold))
Image.network('https://...')        // 网络图(≈ img src)
Image.asset('assets/logo.png')      // 本地图(需在 pubspec.yaml 声明)
Icon(Icons.home, size: 24)          // 图标(内置 Material 图标集)

// ── 容器 ──
Container(
  padding: EdgeInsets.all(16),      // ≈ padding(EdgeInsets 有 all/symmetric/only)
  margin: EdgeInsets.only(top: 8),
  decoration: BoxDecoration(        // ≈ background + border-radius + box-shadow
    color: Colors.blue,
    borderRadius: BorderRadius.circular(8),
    boxShadow: [BoxShadow(blurRadius: 4)],
  ),
  child: Text('content'),
)

// ── 按钮 ──
FilledButton(onPressed: () {}, child: Text('确定'))    // ≈ 主按钮
TextButton(onPressed: () {}, child: Text('取消'))      // ≈ 文字按钮
OutlinedButton(onPressed: () {}, child: Text('次要'))  // ≈ 描边按钮
IconButton(onPressed: () {}, icon: Icon(Icons.add))

// ── 输入(需要 controller,≈ useRef + value/onChange 合体)──
final controller = TextEditingController();
TextField(
  controller: controller,                      // controller.text 读值
  onChanged: (v) => print(v),                  // ≈ onChange
  decoration: InputDecoration(labelText: '用户名'),
)

// ── 列表 ──
ListView(                                       // 简单列表(一次性构建)
  children: [ ListTile(title: Text('item 1')) ],
)
ListView.builder(                               // 懒加载列表(≈ 虚拟滚动,大数据必用)
  itemCount: 1000,
  itemBuilder: (context, index) => ListTile(title: Text('item $index')),
)

// ── 条件渲染:没有 JSX 的 {cond && <A/>},用 Dart 表达式 ──
Column(children: [
  if (isLoading) CircularProgressIndicator(),
  if (error != null) Text(error!) else Text('ok'),
  ...items.map((i) => ListTile(title: Text(i))),   // 列表渲染:展开 map
])

注意列表渲染没有 key 参数,...items.map(...) 生成的列表直接放进 children。对话框/浮层这类命令式 UI 不写在 build 里:

dart 复制代码
showDialog(context: context, builder: (_) => AlertDialog(title: Text('提示')));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('保存成功')));  // ≈ toast

9. 样式思维转变:没有 CSS,一切都是 Widget

CSS Flutter
padding: 16px Padding(padding: EdgeInsets.all(16)) 或 Container(padding:...)
border-radius: 8px BoxDecoration(borderRadius: ...) 或 ClipRRect
color: red Container(color: ...) / BoxDecoration(color:...)
display: flex Row / Column
position: absolute Stack + Positioned
overflow-y: auto SingleChildScrollView / ListView
border: 1px solid BoxDecoration(border: Border.all())
rem/em 逻辑像素(double,已自动处理多倍屏)
媒体查询 MediaQuery.of(context).size
深色模式 Theme.of(context).brightness == Brightness.dark

单位:Flutter 用逻辑像素(类似 rem 的概念),不用操心 DPR。


10. BuildContext 与 InheritedWidget ≈ Context API

BuildContext = Widget 在树中的"位置引用"(≈ React fiber 节点引用)。用它向上查找祖先提供的数据:

dart 复制代码
Theme.of(context)            // 读全局主题(框架内置的 InheritedWidget)
MediaQuery.of(context)       // 读屏幕尺寸等信息

// 自己写跨层传数据:InheritedWidget ≈ React Context.Provider
// 实际开发很少手写,直接用 Provider / Riverpod 包(见下一节)

规则同 React:context 查找只向上(祖先方向) ;build 外使用 context 前先判断 mounted(≈ React 的"组件可能已卸载"):

dart 复制代码
Future<void> fetchData() async {
  final data = await api.load();
  if (!mounted) return;          // async 之后 context 可能已失效,先检查
  setState(() => items = data);
}

11. 页面导航 ≈ Router

dart 复制代码
// 命名路由(简单场景)
MaterialApp(
  initialRoute: '/',
  routes: {'/detail': (ctx) => DetailPage()},
);

Navigator.of(context).pushNamed('/detail');      // ≈ navigate('/detail')
Navigator.of(context).pop();                     // ≈ navigate(-1)

// 直接 push Widget(原型阶段常用)
Navigator.of(context).push(
  MaterialPageRoute(builder: (_) => DetailPage(id: 42)),
);

// 带参数返回(≈ resolve 后拿到结果)
final result = await Navigator.of(context).push(...);

复杂 App 用 go_router(官方推荐的声明式路由,≈ React Router:路径、嵌套路由、守卫、深链)。


12. 状态管理

演进路线(≈ React 生态的演进):

层级 Flutter React 对应
组件内 setState useState
父子传递 构造参数 + 回调 props + callback
跨层共享 InheritedWidget / Provider Context API
复杂应用 Riverpod / BLoC Redux / Zustand
服务端状态 --- React Query

入门先掌握 setState + Provider;Riverpod 是当前社区主流(编译安全、不用 context、支持异步),需要时再学。


13. Key:列表 key 的知识可以直接迁移

和 React 一样,列表项有身份语义时要给 key,帮助框架 diff:

dart 复制代码
ListView(children: [
  for (final item in items)
    ListTile(key: ValueKey(item.id), title: Text(item.name)),
])

常用:ValueKey(值身份)、ObjectKey、UniqueKey(≈ 强制重建)。默认隐式 key 的 diff 规则和 React 类似------同类型、同位置会复用 State,这也是"同类列表增删时状态串位"问题的同源原因。


14. 工程结构与常用命令

复制代码
flutter_application_1/
├── lib/              # ← 你的代码全在这(≈ src/)
│   └── main.dart
├── android/          # Android 壳工程(一般不改)
├── ios/              # iOS 壳工程(一般不改)
├── test/             # 单元/组件测试(≈ *.test.ts)
├── pubspec.yaml      # ≈ package.json(依赖+资源声明)
└── analysis_options.yaml  # ≈ eslint 配置
命令 作用
flutter run 运行到模拟器/真机(≈ npm run dev)
flutter run -d chrome 跑到浏览器
flutter devices 列出可用设备
flutter create <name> 新建项目
flutter pub get 安装依赖(≈ npm i)
flutter analyze 静态检查(≈ lint + tsc)
flutter test 跑测试
flutter build apk / build ios 打包发布

15. 热重载 Hot Reload

flutter run 运行后,终端快捷键:

按键 作用 ≈ Web
r 热重载 :毫秒级刷新 UI,保留页面状态 HMR
R 热重启:重置状态、重新执行 main 刷新页面
p 显示渲染边界线框(调试布局神器) DevTools 的 layers
q 退出 ---

改代码保存 → 自动热重载(IDE 支持)。注意:改了 initState/全局变量/main() 等初始化逻辑时,热重载不会生效,需要 R 热重启。


16. 前端概念对照速查表

Web 前端 Flutter
组件 Widget
render / 模板 build(BuildContext context)
props final 字段 + 构造参数(命名参数)
state State 类字段 + setState
useState setState
useEffect(fn, []) initState()
cleanup dispose()
Context API InheritedWidget / Provider
children child / children
div Container
flex Row / Column + Expanded
position: absolute Stack + Positioned
<img> Image.network / Image.asset
<input> TextField + TextEditingController
虚拟滚动 ListView.builder
onClick onPressed / GestureDetector
React Router Navigator / go_router
toast SnackBar
modal showDialog
CSS 无,Widget 参数 + BoxDecoration
media query MediaQuery.of(context)
key Key / ValueKey
HMR Hot Reload(r)

17. 动手练习

练习目标 :改本项目的 lib/main.dart,做一个"待办清单"页面,覆盖:StatefulWidget、setState、列表渲染、输入框、条件渲染、导航。

dart 复制代码
// 把 MyHomePageState 换成下面内容即可体验(AppBar/body 相应调整)
class _MyHomePageState extends State<MyHomePage> {
  final _controller = TextEditingController();
  final _todos = <String>[];

  @override
  void dispose() {
    _controller.dispose();            // 别忘了销毁 controller
    super.dispose();
  }

  void _addTodo() {
    final text = _controller.text.trim();
    if (text.isEmpty) return;
    setState(() {
      _todos.add(text);
      _controller.clear();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(12),
            child: Row(
              children: [
                Expanded(child: TextField(controller: _controller)),
                IconButton(onPressed: _addTodo, icon: const Icon(Icons.add)),
              ],
            ),
          ),
          Expanded(
            child: _todos.isEmpty
                ? const Center(child: Text('暂无待办'))
                : ListView.builder(
                    itemCount: _todos.length,
                    itemBuilder: (context, i) => ListTile(
                      leading: const Icon(Icons.check_circle_outline),
                      title: Text(_todos[i]),
                      trailing: IconButton(
                        icon: const Icon(Icons.delete_outline),
                        onPressed: () => setState(() => _todos.removeAt(i)),
                      ),
                    ),
                  ),
          ),
        ],
      ),
    );
  }
}

进阶学习路线

  1. 布局实战:仿写一个设置页(ListTile + Switch + 分组卡片)
  2. 网络请求 :http 包 + FutureBuilder(≈ Suspense 思路)
  3. 状态管理:Provider → Riverpod
  4. 路由:go_router
  5. 官方教程:https://docs.flutter.dev/get-started/codelab (第一个 Flutter 应用 codelab,强烈推荐)

本教程基于 Flutter(Material 3)+ Dart 3.x,示例均适配本项目 SDK 环境下的 main.dart。

相关推荐
恋猫de小郭1 小时前
Dart 4.0 要彻底移除 dart:mirrors,Augmentations 应该要来了
android·前端·flutter
blanks20201 小时前
flutter 原生平台插件学习记录之 android 篇
flutter
m0_738185825 小时前
Flutter 鸿蒙化实战:qrcode_flutter 适配 OpenHarmony,二维码生成与识别
数码相机·flutter·华为·harmonyos·鸿蒙
传奇开心果编程5 小时前
【声明式UI开发实用技术学与练】第8课 状态提升与下放
学习·flutter·react native·ui·swiftui·composer
m0_738185827 小时前
Flutter 鸿蒙化实战:open_app_settings 适配 OpenHarmony,一键跳转系统设置
flutter·华为·harmonyos·鸿蒙
m0_738185827 小时前
Flutter 鸿蒙化实战:qr_code_scanner 适配 OpenHarmony,相机扫码实时识别
数码相机·flutter·华为·harmonyos·鸿蒙
传奇开心果编程1 天前
【现代声明式UI学与练】第4课 列表渲染与 key——如何高效渲染列表、key 的作用、列表重排时的状态保持
学习·flutter·react native·ui·swiftui·android jetpack
m0_738185821 天前
Flutter 鸿蒙化实战:media_info 适配 OpenHarmony,媒体信息与缩略图
flutter·华为·harmonyos·鸿蒙·媒体
m0_738185821 天前
Flutter 鸿蒙化实战:just_audio 适配 OpenHarmony,功能强大的播放器
flutter·华为·harmonyos·鸿蒙