Widget 体系
Material/Cupertino 与资源。
Widget 体系
这一课你将能做到
- 区分 StatelessWidget 和 StatefulWidget,知道什么时候用哪个
- 熟练组合 Text / Image / Container / Row / Column / ListView 等基础 Widget 搭出任意 UI
- 理解 Key 的作用,能解决「列表动画错乱」「状态错位」等经典问题
为什么要学
Flutter 里一切都是 Widget——按钮是 Widget、内边距是 Widget、整个 App 也是 Widget。Widget 写不对,UI 一定搭不出来。
人话:Widget 就是乐高积木。小积木(Text、Icon)拼出大积木(Card、AppBar),大积木再拼出整个房子(Scaffold)。
先建立图像
MaterialApp
└── Scaffold
├── AppBar(title: Text('首页'))
├── body: Column
│ ├── Image.network(...)
│ ├── Row
│ │ ├── Icon(Icons.star)
│ │ └── Text('评分 4.9')
│ └── ElevatedButton(...)
└── FloatingActionButton(...)
每个节点都是一个 Widget,Widget 嵌套 Widget。
跟着做
步骤 1:最小可运行骨架
新建 lib/main.dart:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const HomePage(),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Widget 体系')),
body: const Center(child: Text('Hello Widget')),
);
}
}
flutter run 即可看到一个带标题栏和居中文字的页面。
步骤 2:StatelessWidget vs StatefulWidget
StatelessWidget:只依赖构造参数,build 出来的内容不变。
class Greeting extends StatelessWidget {
final String name;
const Greeting({super.key, required this.name});
@override
Widget build(BuildContext context) => Text('你好, $name');
}
StatefulWidget:自己有可变状态(counter、表单输入、动画值)。
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0;
void _inc() => setState(() => _count++); // 改状态必须走 setState
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: _inc,
child: Text('点了 $_count 次'),
);
}
}
经验:默认 Stateless,真的需要内部状态再升级成 Stateful。
步骤 3:常用基础 Widget
// 文本
Text('大标题', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold));
// 图片
Image.network('https://picsum.photos/200');
Image.asset('assets/logo.png'); // 需在 pubspec.yaml 注册
// 容器(盒模型:padding/margin/color/圆角/装饰)
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(12),
boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 4)],
),
child: const Text('卡片'),
);
// 图标 + 按钮
Icon(Icons.favorite, color: Colors.red);
ElevatedButton(onPressed: () {}, child: const Text('确认'));
TextButton(onPressed: () {}, child: const Text('取消'));
步骤 4:布局三剑客 Row / Column / Expanded
class ProfileRow extends StatelessWidget {
const ProfileRow({super.key});
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const CircleAvatar(radius: 28, child: Text('A')),
const SizedBox(width: 12),
Expanded( // 占满剩余宽度
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('Ada', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Text('Flutter 工程师', style: TextStyle(color: Colors.grey)),
],
),
),
const Icon(Icons.chevron_right),
],
);
}
}
- Row 主轴水平,Column 主轴垂直;
mainAxisAlignment 控制主轴,crossAxisAlignment 控制交叉轴。
Expanded / Flexible:分配剩余空间,否则 Row 内容超出会黄黑条溢出。
步骤 5:ListView 长列表
class TodoList extends StatelessWidget {
final List<String> items;
const TodoList({super.key, required this.items});
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (ctx, i) => ListTile(
key: ValueKey(items[i]), // 稳定 Key
leading: const Icon(Icons.check_circle_outline),
title: Text(items[i]),
),
);
}
}
规则:列表项超过一屏就用 ListView.builder(懒加载),不要用 ListView(children: [...]) 一次性渲染。
步骤 6:Key 解决「状态错乱」
反例:没有 Key 的可重排序 StatefulWidget 列表:
class BadList extends StatefulWidget {
const BadList({super.key});
@override
State<BadList> createState() => _BadListState();
}
class _BadListState extends State<BadList> {
final _items = ['A', 'B', 'C'];
void _shuffle() => setState(_items.shuffle);
@override
Widget build(BuildContext context) {
return Column(
children: [
ElevatedButton(onPressed: _shuffle, child: const Text('打乱')),
for (var e in _items)
StatefulColorTile(key: ValueKey(e), label: e), // 关键
],
);
}
}
class StatefulColorTile extends StatefulWidget {
final String label;
const StatefulColorTile({super.key, required this.label});
@override
State<StatefulColorTile> createState() => _StatefulColorTileState();
}
class _StatefulColorTileState extends State<StatefulColorTile> {
late final Color _color =
Colors.primaries[widget.label.hashCode % Colors.primaries.length];
@override
Widget build(BuildContext context) =>
Container(color: _color, padding: const EdgeInsets.all(16), child: Text(widget.label));
}
把 key: ValueKey(e) 删掉再点「打乱」,会发现颜色不跟字母走 —— 这就是 Key 缺失导致的状态错位。
讲清楚要点
- Widget 是配置,不可变:字段必须 final。要变就建一个新 Widget 替换旧的,Flutter 做 diff。
- build() 必须「纯净」:无副作用、无网络请求、快。网络请求放 initState / FutureBuilder / Bloc。
- State 三件套:
initState(一次性初始化)、build(每次重建)、dispose(释放资源,比如 controller)。
- Key 决定 Element 是否复用:列表删除/排序/动画,给 StatefulWidget 一个稳定的
ValueKey/ObjectKey。
- const 越多越好:能 const 的构造都 const,避免无谓 rebuild。
你可能会懵的地方
-
Padding 居然是个 Widget?
Flutter 没有「属性」的概念,所有视觉/布局都是 Widget。Padding(padding:..., child: ...) 是个真实存在的节点。
-
为什么 Image 加载不出来?
Image.asset 必须在 pubspec.yaml 里声明路径:flutter: assets: - assets/。改完要完全重启(不是热重载)。
-
Column 里再嵌 ListView 报错?
两个都无限高。给 ListView 套 Expanded 或 SizedBox(height: ...)。
-
setState 之后 UI 没刷新?
检查是不是在 build 里直接改字段,或在 dispose 之后调用;也可能是改的字段没参与 build。
-
Material 和 Cupertino 怎么选?
MaterialApp 是默认推荐,跨平台一致;要做 iOS 原生风格才用 CupertinoApp。生产项目基本用 Material。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 在 StatelessWidget 里写字段并直接改 | UI 不更新 | 用 StatefulWidget + setState |
setState 里跑耗时操作 | 阻塞 UI | setState 只改数据,逻辑放外面 |
| Column 直接放 ListView | 高度无限冲突 | Expanded(child: ListView(...)) |
| 给所有 Widget 都加 GlobalKey | 性能差且难维护 | 局部用 ValueKey/ObjectKey,GlobalKey 仅在跨树共享状态时用 |
| 在 build 里发网络请求 | 每次 rebuild 都发一次 | initState 发一次,或 FutureBuilder 包 Future |
用 MediaQuery.of(context).size 写死布局 | 不响应屏幕变化 | 用 LayoutBuilder / Expanded |
卡点急救
- 黄黑溢出条纹(RenderFlex overflowed):子组件超宽,套
Expanded 或 Flexible;或加滚动 SingleChildScrollView。
- 图片 asset 404:检查
pubspec.yaml 是否声明,注意缩进;改完要 flutter clean && flutter run。
- setState called during build:把调用挪到按钮回调/
WidgetsBinding.instance.addPostFrameCallback。
- 键盘弹出导致 bottom overflow:
Scaffold(resizeToAvoidBottomInset: false) 或包 SingleChildScrollView。
- Stateful 列表删除后状态错位:每项加
key: ValueKey(item.id)。
面试常问
-
StatelessWidget 和 StatefulWidget 区别?
前者配置不可变,无内部状态;后者有 State 对象,可跨 rebuild 保留数据,setState 触发重建。
-
build() 会被调用多少次?
任意次、随时可能被调用。所以 build 必须纯净无副作用,依赖于此才能高效重建。
-
Key 的作用?
在 Widget 树更新时帮助 Flutter 判断「旧 Element 是否可以复用」。相同位置相同类型 + 相同 Key → 复用;不同 Key → 销毁重建。
-
GlobalKey 用在哪?
跨 Widget 树访问 State(如 FormState.validate()),或在树的不同位置移动同一个 Widget 保留状态。代价高,能不用就不用。
自测三问
- 为什么 Flutter 没有
view.setPadding(16) 这种调用?它是怎么处理 padding 的?
- 写一段代码:一个计数器按钮,点击数字 +1。
- 给一个可重排序的 StatefulWidget 列表加 Key,应该选哪种 Key?为什么?
小结一句话
Widget 就是不可变的 UI 配置,Stateless 用来展示、Stateful 用来持状态,合理组合 + 正确使用 Key 是 Flutter UI 的全部基石。
延伸