码林鼠头像
关注

Flutter之Widget/State 入门文档

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

前置知识:已阅读 dart_tutorial.md。
核心心法:Flutter ≈ React 的思想 + 原生的渲染。你会 React/Vue,Flutter 的心智模型一天就能建立。


目录

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

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

Web 前端Flutter
UI 描述方式JSX / 模板(声明式)Widget 组合(声明式,思想同 React)
渲染目标浏览器 DOM自绘引擎(Skia/Impeller),不依赖原生控件
布局语言CSS FlexboxRow/Column/Flex(概念同 Flexbox,写法是 Widget 参数)
样式CSSWidget 的构造参数(padding、decoration…)
事件处理onClick 属性onPressed / GestureDetector 回调
状态管理useState / Redux / PiniasetState / 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 树:真正负责布局和绘制
// Widget 就是层层嵌套的"配置对象",读法:由内向外 or 由外向内都行
Center(                    // 水平垂直居中容器
  child: Text('Hello'),    // 文本
)

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


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

下面是本项目的 lib/main.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 身份标识

  
  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(不可变配置)

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

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

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

  
  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):编译器能从上下文推断类型时,可以省略类型前缀:

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

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

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

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

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

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

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

5. setState ≈ useState

// 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)

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

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

  
  Widget build(BuildContext context) { ... }

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

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

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

// 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 速查

// ── 文本与图片 ──
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 里:

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

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

CSSFlutter
padding: 16pxPadding(padding: EdgeInsets.all(16)) 或 Container(padding:...)
border-radius: 8pxBoxDecoration(borderRadius: ...) 或 ClipRRect
color: redContainer(color: ...) / BoxDecoration(color:...)
display: flexRow / Column
position: absoluteStack + Positioned
overflow-y: autoSingleChildScrollView / ListView
border: 1px solidBoxDecoration(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 节点引用)。用它向上查找祖先提供的数据:

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

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

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

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

11. 页面导航 ≈ Router

// 命名路由(简单场景)
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 生态的演进):

层级FlutterReact 对应
组件内setStateuseState
父子传递构造参数 + 回调props + callback
跨层共享InheritedWidget / ProviderContext API
复杂应用Riverpod / BLoCRedux / Zustand
服务端状态—React Query

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


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

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

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)
propsfinal 字段 + 构造参数(命名参数)
stateState 类字段 + setState
useStatesetState
useEffect(fn, [])initState()
cleanupdispose()
Context APIInheritedWidget / Provider
childrenchild / children
divContainer
flexRow / Column + Expanded
position: absoluteStack + Positioned
<img>Image.network / Image.asset
<input>TextField + TextEditingController
虚拟滚动ListView.builder
onClickonPressed / GestureDetector
React RouterNavigator / go_router
toastSnackBar
modalshowDialog
CSS无,Widget 参数 + BoxDecoration
media queryMediaQuery.of(context)
keyKey / ValueKey
HMRHot Reload(r)

17. 动手练习

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

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

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

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

  
  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。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/weixin_41254345/article/details/167223282

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--