Flutter 常用 Widget 入门
Flutter 的核心思想是:一切皆 Widget。文字、图片、按钮、页面结构、布局、间距、滚动、动画、手势,几乎都由 Widget 组合出来。
1. Widget 先分成几类理解
| 类别 | 常用 Widget | 解决什么问题 |
|---|---|---|
| 应用入口 | MaterialApp、CupertinoApp | 搭建应用外壳、主题、路由 |
| 页面骨架 | Scaffold、AppBar、SafeArea | 标准页面结构 |
| 基础显示 | Text、Icon、Image | 显示文字、图标、图片 |
| 布局容器 | Container、Padding、Center、SizedBox | 控制尺寸、间距、装饰、位置 |
| 线性布局 | Row、Column、Flex | 横向或纵向排列 |
| 弹性布局 | Expanded、Flexible、Spacer | 分配剩余空间 |
| 层叠布局 | Stack、Positioned | 元素重叠、角标、浮层 |
| 流式布局 | Wrap | 自动换行标签、按钮组 |
| 滚动列表 | ListView、GridView、SingleChildScrollView | 长列表、宫格、普通滚动页 |
| 表单输入 | TextField、Form、TextFormField | 输入框、表单校验 |
| 按钮 | ElevatedButton、TextButton、OutlinedButton、IconButton | 用户操作 |
| 卡片列表 | Card、ListTile、Divider | 列表项、设置页、信息卡片 |
| 交互反馈 | SnackBar、Dialog、BottomSheet | 提示、弹窗、底部面板 |
| 状态构建 | FutureBuilder、StreamBuilder、ValueListenableBuilder | 根据异步或状态构建 UI |
| 适配布局 | MediaQuery、LayoutBuilder | 响应式和尺寸判断 |
| 手势 | GestureDetector、InkWell | 点击、长按、拖动 |
| 动画 | AnimatedContainer、AnimatedOpacity、Hero | 简单动画和转场 |
2. StatelessWidget 和 StatefulWidget
2.1 StatelessWidget
StatelessWidget 表示这个组件本身没有内部状态。它依赖外部传入的数据来展示 UI。
class UserName extends StatelessWidget {
final String name;
const UserName({
super.key,
required this.name,
});
@override
Widget build(BuildContext context) {
return Text('你好,$name');
}
}
适合:
- 纯展示组件
- 传什么显示什么
- 不需要
setState
2.2 StatefulWidget
StatefulWidget 表示组件内部有会变化的状态。
class CounterPage extends StatefulWidget {
const CounterPage({super.key});
@override
State<CounterPage> createState() => _CounterPageState();
}
class _CounterPageState extends State<CounterPage> {
int count = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('count: $count'),
ElevatedButton(
onPressed: () {
setState(() {
count++;
});
},
child: const Text('加一'),
),
],
);
}
}
适合:
- 输入框内容变化
- tab 切换
- 开关状态
- 页面局部刷新
- 动画控制
简单判断:如果 UI 会因为自身内部数据改变而刷新,就用 StatefulWidget。
3. 应用入口 Widget
3.1 MaterialApp
MaterialApp 是 Material 风格应用的入口,绝大多数 Flutter 项目都会用它。
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const HomePage(),
);
}
}
常用属性:
| 属性 | 说明 |
|---|---|
title | 应用标题 |
theme | 全局主题 |
darkTheme | 暗色主题 |
home | 首页 |
routes | 命名路由 |
debugShowCheckedModeBanner | 是否显示右上角 debug 标识 |
3.2 CupertinoApp
CupertinoApp 是 iOS 风格应用入口。如果你要做纯 iOS 风格,可以用它。
class MyIosApp extends StatelessWidget {
const MyIosApp({super.key});
@override
Widget build(BuildContext context) {
return const CupertinoApp(
home: CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text('首页'),
),
child: Center(
child: Text('Hello Cupertino'),
),
),
);
}
}
大多数跨平台应用可以先从 MaterialApp 开始。
4. 页面骨架 Widget
4.1 Scaffold
Scaffold 是一个标准页面骨架。它提供了顶部栏、主体、抽屉、悬浮按钮、底部导航、SnackBar 等常见结构。
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('首页'),
),
body: const Center(
child: Text('页面内容'),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
);
}
}
常用属性:
| 属性 | 说明 |
|---|---|
appBar | 顶部导航栏 |
body | 页面主体 |
drawer | 左侧抽屉 |
endDrawer | 右侧抽屉 |
bottomNavigationBar | 底部导航 |
floatingActionButton | 悬浮按钮 |
backgroundColor | 页面背景色 |
4.2 AppBar
AppBar 一般放在 Scaffold.appBar。
AppBar(
title: const Text('文章详情'),
centerTitle: true,
actions: [
IconButton(
onPressed: () {},
icon: const Icon(Icons.share),
),
],
)
常用属性:
| 属性 | 说明 |
|---|---|
title | 标题 |
leading | 左侧按钮 |
actions | 右侧按钮列表 |
centerTitle | 标题是否居中 |
backgroundColor | 背景色 |
elevation | 阴影高度 |
4.3 SafeArea
SafeArea 用来避开刘海屏、状态栏、底部手势条。
Scaffold(
body: SafeArea(
child: Text('不会贴到状态栏'),
),
)
常用场景:
- 自定义顶部区域
- 没有使用
AppBar的页面 - 全屏页面里避免内容被遮挡
5. 基础显示 Widget
5.1 Text
Text 用来显示文字。
const Text(
'Hello Flutter',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
)
常用属性:
| 属性 | 说明 |
|---|---|
style | 文字样式 |
textAlign | 对齐方式 |
maxLines | 最大行数 |
overflow | 超出处理 |
softWrap | 是否自动换行 |
单行省略:
const Text(
'这是一段很长很长的文字',
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
5.2 RichText 和 TextSpan
需要一段文字里有不同样式时,用 RichText。
RichText(
text: const TextSpan(
style: TextStyle(color: Colors.black, fontSize: 16),
children: [
TextSpan(text: '价格:'),
TextSpan(
text: '¥99',
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
],
),
)
5.3 Icon
const Icon(
Icons.favorite,
color: Colors.red,
size: 32,
)
常用场景:
- 按钮图标
- 列表项前缀
- 状态提示
- 空状态页面
5.4 Image
网络图片:
Image.network(
'https://example.com/avatar.png',
width: 80,
height: 80,
fit: BoxFit.cover,
)
本地资源图片:
Image.asset(
'assets/images/avatar.png',
width: 80,
height: 80,
)
pubspec.yaml 里需要声明资源:
flutter:
assets:
- assets/images/
常用属性:
| 属性 | 说明 |
|---|---|
width | 宽度 |
height | 高度 |
fit | 图片填充方式 |
color | 图片着色 |
alignment | 对齐方式 |
常用 BoxFit:
| 值 | 含义 |
|---|---|
BoxFit.cover | 填满容器,可能裁剪 |
BoxFit.contain | 完整显示,可能留空 |
BoxFit.fill | 拉伸填满,可能变形 |
BoxFit.fitWidth | 按宽度适配 |
BoxFit.fitHeight | 按高度适配 |
6. 容器和间距 Widget
6.1 Container
Container 是最常用的容器。它可以控制宽高、边距、内边距、背景色、圆角、边框、阴影等。
Container(
width: 120,
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
margin: const EdgeInsets.only(top: 12),
alignment: Alignment.center,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'按钮样式',
style: TextStyle(color: Colors.white),
),
)
常用属性:
| 属性 | 说明 |
|---|---|
width / height | 宽高 |
padding | 内边距 |
margin | 外边距 |
alignment | 子元素对齐 |
color | 背景色 |
decoration | 装饰,圆角、边框、渐变、阴影 |
child | 子 Widget |
注意:color 和 decoration 里同时设置颜色会冲突。复杂样式统一放到 decoration。
6.2 Padding
只需要加内边距时,用 Padding 更清晰。
const Padding(
padding: EdgeInsets.all(16),
child: Text('有内边距的文字'),
)
常用写法:
EdgeInsets.all(16)
EdgeInsets.symmetric(horizontal: 16, vertical: 8)
EdgeInsets.only(left: 16, top: 8)
6.3 SizedBox
SizedBox 常用于固定尺寸或制造间距。
const SizedBox(height: 12)
固定子元素尺寸:
SizedBox(
width: 200,
height: 48,
child: ElevatedButton(
onPressed: () {},
child: const Text('提交'),
),
)
6.4 Center
const Center(
child: Text('居中显示'),
)
6.5 Align
Align 可以控制子元素在父容器中的位置。
const Align(
alignment: Alignment.topRight,
child: Icon(Icons.close),
)
6.6 ClipRRect
给图片裁剪圆角时很常用。
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
'https://example.com/image.png',
width: 120,
height: 80,
fit: BoxFit.cover,
),
)
7. Row、Column、Expanded
7.1 Row
Row 横向排列子元素。
Row(
children: const [
Icon(Icons.person),
SizedBox(width: 8),
Text('用户名'),
],
)
常用属性:
| 属性 | 说明 |
|---|---|
mainAxisAlignment | 主轴对齐,Row 中是横向 |
crossAxisAlignment | 交叉轴对齐,Row 中是纵向 |
children | 子元素列表 |
7.2 Column
Column 纵向排列子元素。
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('标题'),
SizedBox(height: 8),
Text('描述内容'),
],
)
7.3 主轴和交叉轴
| Widget | 主轴 | 交叉轴 |
|---|---|---|
Row | 横向 | 纵向 |
Column | 纵向 | 横向 |
常见对齐:
mainAxisAlignment: MainAxisAlignment.center
mainAxisAlignment: MainAxisAlignment.spaceBetween
mainAxisAlignment: MainAxisAlignment.spaceAround
mainAxisAlignment: MainAxisAlignment.spaceEvenly
crossAxisAlignment: CrossAxisAlignment.start
crossAxisAlignment: CrossAxisAlignment.center
crossAxisAlignment: CrossAxisAlignment.stretch
7.4 Expanded
Expanded 会占满剩余空间。它只能放在 Row、Column、Flex 里面。
Row(
children: [
const Icon(Icons.search),
const SizedBox(width: 8),
Expanded(
child: TextField(
decoration: const InputDecoration(
hintText: '请输入关键词',
),
),
),
],
)
常见用途:
- 一行里左边固定,右边自适应
- 搜索框占满剩余宽度
- 多列等分
等分布局:
Row(
children: [
Expanded(child: Container(height: 40, color: Colors.red)),
Expanded(child: Container(height: 40, color: Colors.blue)),
],
)
7.5 Flexible
Flexible 比 Expanded 更灵活。Expanded 强制占满剩余空间,Flexible 可以不占满。
Row(
children: const [
Flexible(
child: Text(
'这是一段可能很长的文字',
overflow: TextOverflow.ellipsis,
),
),
Icon(Icons.chevron_right),
],
)
7.6 Spacer
Spacer 用来撑开空白。
Row(
children: const [
Text('左侧'),
Spacer(),
Text('右侧'),
],
)
8. Stack 和 Positioned
Stack 用于层叠布局,常见于角标、图片遮罩、浮动按钮。
Stack(
children: [
Image.network(
'https://example.com/banner.png',
width: double.infinity,
height: 180,
fit: BoxFit.cover,
),
Positioned(
right: 12,
bottom: 12,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'热门',
style: TextStyle(color: Colors.white),
),
),
),
],
)
Positioned 只能作为 Stack 的直接子元素。
常用属性:
| 属性 | 说明 |
|---|---|
left | 距离左边 |
right | 距离右边 |
top | 距离顶部 |
bottom | 距离底部 |
width | 宽度 |
height | 高度 |
9. Wrap
Wrap 会在空间不够时自动换行,适合标签列表。
Wrap(
spacing: 8,
runSpacing: 8,
children: const [
Chip(label: Text('Flutter')),
Chip(label: Text('Dart')),
Chip(label: Text('Widget')),
Chip(label: Text('状态管理')),
],
)
常用属性:
| 属性 | 说明 |
|---|---|
spacing | 主轴间距 |
runSpacing | 换行后的行间距 |
alignment | 主轴对齐 |
runAlignment | 多行对齐 |
10. 滚动 Widget
10.1 SingleChildScrollView
适合内容不多,但可能超出屏幕的页面,比如详情页、表单页。
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text('标题'),
SizedBox(height: 12),
Text('很多内容...'),
],
),
)
注意:长列表不要用 SingleChildScrollView + Column,性能不好。长列表用 ListView.builder。
10.2 ListView
少量固定元素:
ListView(
padding: const EdgeInsets.all(16),
children: const [
Text('第一项'),
Text('第二项'),
Text('第三项'),
],
)
大量数据用 ListView.builder:
ListView.builder(
itemCount: 100,
itemBuilder: (context, index) {
return ListTile(
title: Text('第 $index 项'),
);
},
)
ListView.builder 会按需构建列表项,更适合长列表。
10.3 GridView
宫格布局:
GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.2,
),
itemCount: 20,
itemBuilder: (context, index) {
return Container(
alignment: Alignment.center,
color: Colors.blue.shade100,
child: Text('Item $index'),
);
},
)
常用属性:
| 属性 | 说明 |
|---|---|
crossAxisCount | 每行几个 |
crossAxisSpacing | 横向间距 |
mainAxisSpacing | 纵向间距 |
childAspectRatio | 子项宽高比 |
10.4 PageView
横向分页,常用于引导页、轮播、图片预览。
PageView(
children: const [
Center(child: Text('第 1 页')),
Center(child: Text('第 2 页')),
Center(child: Text('第 3 页')),
],
)
配合控制器:
final controller = PageController();
在 StatefulWidget 中记得释放:
@override
void dispose() {
controller.dispose();
super.dispose();
}
10.5 RefreshIndicator
下拉刷新:
RefreshIndicator(
onRefresh: () async {
await Future.delayed(const Duration(seconds: 1));
},
child: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
),
)
注意:RefreshIndicator 的 child 一般要是可滚动组件。
11. 按钮 Widget
11.1 ElevatedButton
强调按钮,适合主要操作。
ElevatedButton(
onPressed: () {},
child: const Text('提交'),
)
11.2 TextButton
文字按钮,适合次要操作。
TextButton(
onPressed: () {},
child: const Text('取消'),
)
11.3 OutlinedButton
描边按钮。
OutlinedButton(
onPressed: () {},
child: const Text('查看更多'),
)
11.4 IconButton
图标按钮。
IconButton(
onPressed: () {},
icon: const Icon(Icons.settings),
)
11.5 FloatingActionButton
悬浮按钮,一般用于页面主操作。
FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
)
按钮禁用:把 onPressed 设置为 null。
ElevatedButton(
onPressed: null,
child: const Text('不可点击'),
)
12. 输入和表单 Widget
12.1 TextField
普通输入框。
TextField(
decoration: const InputDecoration(
labelText: '用户名',
hintText: '请输入用户名',
border: OutlineInputBorder(),
),
onChanged: (value) {
print(value);
},
)
常用属性:
| 属性 | 说明 |
|---|---|
controller | 控制输入内容 |
decoration | 输入框样式 |
keyboardType | 键盘类型 |
obscureText | 是否隐藏文字,常用于密码 |
maxLines | 最大行数 |
onChanged | 内容变化 |
onSubmitted | 提交 |
12.2 TextEditingController
需要主动读取或设置输入框内容时,用 controller。
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final usernameController = TextEditingController();
@override
void dispose() {
usernameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return TextField(
controller: usernameController,
decoration: const InputDecoration(
labelText: '用户名',
),
);
}
}
只要自己创建了 controller,通常就要在 dispose 里释放。
12.3 Form 和 TextFormField
需要表单校验时,用 Form。
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
final formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: formKey,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(
labelText: '邮箱',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入邮箱';
}
if (!value.contains('@')) {
return '邮箱格式不正确';
}
return null;
},
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () {
final valid = formKey.currentState?.validate() ?? false;
if (valid) {
print('表单通过');
}
},
child: const Text('登录'),
),
],
),
);
}
}
13. 卡片和列表项 Widget
13.1 Card
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'卡片标题',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 8),
Text('卡片内容'),
],
),
),
)
适合:
- 商品卡片
- 文章卡片
- 设置分组
- 用户信息块
13.2 ListTile
ListTile 是很常用的列表项组件。
ListTile(
leading: const CircleAvatar(
child: Icon(Icons.person),
),
title: const Text('用户名'),
subtitle: const Text('这是一段简介'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
)
常用属性:
| 属性 | 说明 |
|---|---|
leading | 左侧区域 |
title | 主标题 |
subtitle | 副标题 |
trailing | 右侧区域 |
onTap | 点击事件 |
13.3 Divider
分割线。
const Divider(height: 1)
配合列表:
ListView.separated(
itemCount: 20,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
)
14. 交互反馈 Widget
14.1 SnackBar
页面底部轻提示。
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('保存成功'),
),
);
通常用于:
- 保存成功
- 操作失败
- 网络错误
- 撤销提示
14.2 AlertDialog
弹窗确认。
showDialog<void>(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('确定要删除吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
);
},
);
14.3 BottomSheet
底部弹出面板。
showModalBottomSheet<void>(
context: context,
builder: (context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.photo),
title: const Text('选择图片'),
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.camera_alt),
title: const Text('拍照'),
onTap: () => Navigator.pop(context),
),
],
),
);
},
);
适合:
- 分享面板
- 操作菜单
- 筛选器
- 选择器
15. 手势 Widget
15.1 GestureDetector
GestureDetector 可以给任意 Widget 添加手势。
GestureDetector(
onTap: () {
print('点击');
},
onLongPress: () {
print('长按');
},
child: Container(
width: 120,
height: 48,
alignment: Alignment.center,
color: Colors.blue,
child: const Text(
'点我',
style: TextStyle(color: Colors.white),
),
),
)
常用事件:
| 事件 | 说明 |
|---|---|
onTap | 点击 |
onDoubleTap | 双击 |
onLongPress | 长按 |
onPanUpdate | 拖动 |
onScaleUpdate | 缩放 |
15.2 InkWell
InkWell 带 Material 水波纹效果。
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(8),
onTap: () {},
child: const Padding(
padding: EdgeInsets.all(12),
child: Text('带水波纹的点击区域'),
),
),
)
如果你希望按钮或卡片点击时有 Material 风格反馈,优先考虑 InkWell。
16. 异步和状态构建 Widget
16.1 FutureBuilder
FutureBuilder 用于根据异步请求状态构建 UI。
Future<String> fetchName() async {
await Future.delayed(const Duration(seconds: 1));
return 'Mika';
}
class UserNameView extends StatelessWidget {
const UserNameView({super.key});
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: fetchName(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('出错了:${snapshot.error}');
}
return Text('用户名:${snapshot.data}');
},
);
}
}
注意:真实项目里不要在频繁重建的 build 中直接创建新的请求。更稳的方式是在 initState 里保存 Future。
class UserNamePage extends StatefulWidget {
const UserNamePage({super.key});
@override
State<UserNamePage> createState() => _UserNamePageState();
}
class _UserNamePageState extends State<UserNamePage> {
late final Future<String> nameFuture;
@override
void initState() {
super.initState();
nameFuture = fetchName();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: nameFuture,
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
return Text(snapshot.data!);
},
);
}
}
16.2 StreamBuilder
StreamBuilder 用于持续数据,比如倒计时、WebSocket、实时消息。
Stream<int> countdown() async* {
for (var i = 5; i >= 0; i--) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}
StreamBuilder<int>(
stream: countdown(),
builder: (context, snapshot) {
return Text('倒计时:${snapshot.data ?? 5}');
},
)
16.3 ValueListenableBuilder
适合监听 ValueNotifier。
final countNotifier = ValueNotifier<int>(0);
ValueListenableBuilder<int>(
valueListenable: countNotifier,
builder: (context, count, child) {
return Text('count: $count');
},
)
更新:
countNotifier.value++;
ValueNotifier + ValueListenableBuilder 很适合小范围状态,不一定一上来就用大型状态管理。
16.4 Builder
Builder 可以创建一个新的 BuildContext。当你需要拿到某个 Widget 下方的新 context 时有用。
Builder(
builder: (context) {
return ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Hello')),
);
},
child: const Text('显示提示'),
);
},
)
17. 响应式和约束 Widget
17.1 MediaQuery
获取屏幕尺寸、像素密度、系统字体缩放等。
final size = MediaQuery.sizeOf(context);
final width = size.width;
简单适配:
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final isWide = width >= 600;
return isWide ? const DesktopLayout() : const MobileLayout();
}
17.2 LayoutBuilder
LayoutBuilder 获取父容器给当前 Widget 的约束,比直接看屏幕宽度更适合组件内部响应式。
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
return const Row(
children: [
Expanded(child: Text('左侧')),
Expanded(child: Text('右侧')),
],
);
}
return const Column(
children: [
Text('上方'),
Text('下方'),
],
);
},
)
17.3 AspectRatio
固定宽高比。
AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
'https://example.com/banner.png',
fit: BoxFit.cover,
),
)
17.4 FittedBox
把子元素缩放到合适大小。
const FittedBox(
child: Text('很大的文字'),
)
常用于:
- 大数字适配
- 标签文字不溢出
- 图标和文字缩放
18. 动画 Widget
18.1 AnimatedContainer
最常用的隐式动画 Widget。
AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: selected ? 160 : 100,
height: 48,
decoration: BoxDecoration(
color: selected ? Colors.blue : Colors.grey,
borderRadius: BorderRadius.circular(selected ? 24 : 8),
),
)
当属性变化时,它会自动执行动画。
18.2 AnimatedOpacity
透明度动画。
AnimatedOpacity(
opacity: visible ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: const Text('渐隐渐显'),
)
18.3 AnimatedSwitcher
切换子元素时自动动画。
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: Text(
'$count',
key: ValueKey(count),
style: const TextStyle(fontSize: 32),
),
)
18.4 Hero
页面跳转共享元素动画。
Hero(
tag: 'avatar-1',
child: Image.network(
'https://example.com/avatar.png',
width: 80,
height: 80,
),
)
两个页面里使用相同 tag 的 Hero,跳转时会自动做转场。
19. 加载、空状态和错误状态
19.1 CircularProgressIndicator
const Center(
child: CircularProgressIndicator(),
)
19.2 LinearProgressIndicator
const LinearProgressIndicator()
19.3 空状态
const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.inbox, size: 48, color: Colors.grey),
SizedBox(height: 8),
Text('暂无数据'),
],
),
)
19.4 错误状态
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('加载失败'),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () {},
child: const Text('重试'),
),
],
),
)
一个成熟页面至少要考虑:
- loading
- empty
- error
- success
20. 导航相关 Widget
20.1 Navigator
跳转页面:
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailPage(),
),
);
返回:
Navigator.pop(context);
带返回值:
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) => const SelectPage(),
),
);
在目标页返回:
Navigator.pop(context, '选中的值');
20.2 BottomNavigationBar
底部导航。
BottomNavigationBar(
currentIndex: currentIndex,
onTap: (index) {
setState(() {
currentIndex = index;
});
},
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.person),
label: '我的',
),
],
)
通常放在 Scaffold.bottomNavigationBar。
20.3 TabBar 和 TabBarView
顶部 Tab。
DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: const Text('Tab 示例'),
bottom: const TabBar(
tabs: [
Tab(text: '推荐'),
Tab(text: '关注'),
],
),
),
body: const TabBarView(
children: [
Center(child: Text('推荐内容')),
Center(child: Text('关注内容')),
],
),
),
)
21. 常用 Material Widget
21.1 Chip
标签。
const Chip(
label: Text('Flutter'),
avatar: Icon(Icons.tag),
)
21.2 Checkbox
Checkbox(
value: checked,
onChanged: (value) {
setState(() {
checked = value ?? false;
});
},
)
21.3 Switch
Switch(
value: enabled,
onChanged: (value) {
setState(() {
enabled = value;
});
},
)
21.4 Radio
Radio<String>(
value: 'male',
groupValue: gender,
onChanged: (value) {
setState(() {
gender = value;
});
},
)
21.5 Slider
Slider(
value: volume,
min: 0,
max: 100,
divisions: 10,
label: volume.round().toString(),
onChanged: (value) {
setState(() {
volume = value;
});
},
)
22. 常用 Cupertino Widget
如果要做 iOS 风格,可以关注这些:
| Widget | 说明 |
|---|---|
CupertinoApp | iOS 风格应用入口 |
CupertinoPageScaffold | iOS 页面骨架 |
CupertinoNavigationBar | iOS 顶部导航 |
CupertinoButton | iOS 按钮 |
CupertinoSwitch | iOS 开关 |
CupertinoTextField | iOS 输入框 |
CupertinoAlertDialog | iOS 弹窗 |
CupertinoActionSheet | iOS 操作面板 |
CupertinoActivityIndicator | iOS loading |
CupertinoTabScaffold | iOS tab 页面 |
示例:
CupertinoButton(
color: CupertinoColors.activeBlue,
onPressed: () {},
child: const Text('确认'),
)
需要导入:
import 'package:flutter/cupertino.dart';
23. 常见页面组合示例
23.1 普通列表页
class ArticleListPage extends StatelessWidget {
const ArticleListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('文章列表'),
),
body: ListView.separated(
itemCount: 20,
separatorBuilder: (context, index) => const Divider(height: 1),
itemBuilder: (context, index) {
return ListTile(
title: Text('文章标题 $index'),
subtitle: const Text('这里是文章摘要'),
trailing: const Icon(Icons.chevron_right),
onTap: () {},
);
},
),
);
}
}
23.2 登录页
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final formKey = GlobalKey<FormState>();
final usernameController = TextEditingController();
final passwordController = TextEditingController();
@override
void dispose() {
usernameController.dispose();
passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('登录')),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Form(
key: formKey,
child: Column(
children: [
TextFormField(
controller: usernameController,
decoration: const InputDecoration(
labelText: '用户名',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入用户名';
}
return null;
},
),
const SizedBox(height: 12),
TextFormField(
controller: passwordController,
obscureText: true,
decoration: const InputDecoration(
labelText: '密码',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.length < 6) {
return '密码至少 6 位';
}
return null;
},
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
final valid = formKey.currentState?.validate() ?? false;
if (!valid) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('登录中...')),
);
},
child: const Text('登录'),
),
),
],
),
),
),
),
);
}
}
23.3 商品卡片
class ProductCard extends StatelessWidget {
final String title;
final String imageUrl;
final double price;
const ProductCard({
super.key,
required this.title,
required this.imageUrl,
required this.price,
});
@override
Widget build(BuildContext context) {
return Card(
clipBehavior: Clip.antiAlias,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
imageUrl,
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
'¥$price',
style: const TextStyle(
color: Colors.red,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
);
}
}
24. 常见布局报错和解决思路
24.1 RenderFlex overflowed
常见原因:Row 或 Column 里的内容超出了可用空间。
常见解决:
Row(
children: const [
Icon(Icons.info),
SizedBox(width: 8),
Expanded(
child: Text(
'很长很长的文字',
overflow: TextOverflow.ellipsis,
),
),
],
)
处理思路:
- 在
Row里给长文字包Expanded或Flexible。 - 给
Text设置maxLines和overflow。 - 页面内容太高时,使用滚动组件。
24.2 Vertical viewport was given unbounded height
常见原因:在 Column 里直接放 ListView,但没有给它高度。
解决:
Column(
children: [
const Text('标题'),
Expanded(
child: ListView.builder(
itemCount: 20,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
),
),
],
)
24.3 setState called after dispose
常见原因:异步任务回来时,页面已经销毁。
解决:
if (!mounted) return;
setState(() {
// 更新状态
});
24.4 Incorrect use of ParentDataWidget
常见原因:Expanded、Flexible、Positioned 放错位置。
规则:
| Widget | 必须放在哪里 |
|---|---|
Expanded | Row、Column、Flex 的直接子元素 |
Flexible | Row、Column、Flex 的直接子元素 |
Positioned | Stack 的直接子元素 |
25. 新手选 Widget 的口诀
| 需求 | 优先选择 |
|---|---|
| 做一个标准页面 | Scaffold |
| 顶部标题栏 | AppBar |
| 居中 | Center |
| 加内边距 | Padding |
| 加宽高、背景、圆角 | Container |
| 固定间距 | SizedBox |
| 横向排列 | Row |
| 纵向排列 | Column |
| 占满剩余空间 | Expanded |
| 文字太长省略 | Flexible + TextOverflow.ellipsis |
| 元素重叠 | Stack + Positioned |
| 自动换行标签 | Wrap |
| 长列表 | ListView.builder |
| 宫格 | GridView.builder |
| 表单校验 | Form + TextFormField |
| 点击区域 | InkWell 或 GestureDetector |
| 异步请求 UI | FutureBuilder |
| 实时数据 UI | StreamBuilder |
| 简单局部状态 | ValueNotifier + ValueListenableBuilder |
| 响应式布局 | LayoutBuilder |
26. 推荐学习顺序
第 1 阶段:先能搭页面
先掌握:
MaterialAppScaffoldAppBarTextIconImageContainerPaddingSizedBoxRowColumnExpanded
能做出普通页面结构后,再继续。
第 2 阶段:列表和输入
重点掌握:
ListView.builderGridView.builderListTileCardTextFieldFormTextFormFieldElevatedButton
这时你就能做登录页、列表页、详情页。
第 3 阶段:交互和状态
重点掌握:
setStateFutureBuilderStreamBuilderValueListenableBuilderSnackBarDialogBottomSheetNavigator
这时就能写出能请求数据、能跳转、能反馈用户操作的页面。
第 4 阶段:适配和优化
重点掌握:
MediaQueryLayoutBuilderSafeAreaCustomScrollViewSliverAppBarAnimatedContainerHero
这时再去处理复杂页面、响应式和动画。
27. 官方资料
- Flutter Widget catalog:https://docs.flutter.dev/ui/widgets
- Basic widgets:https://docs.flutter.dev/ui/widgets/basics
- Layout widgets:https://docs.flutter.dev/ui/widgets/layout
- Scrolling widgets:https://docs.flutter.dev/ui/widgets/scrolling
- Input widgets:https://docs.flutter.dev/ui/widgets/input
- Material widgets:https://docs.flutter.dev/ui/widgets/material
- Cupertino widgets:https://docs.flutter.dev/ui/widgets/cupertino
- Layouts in Flutter:https://docs.flutter.dev/ui/layout
28. 最后总结
Flutter 新手不要一开始就试图记住所有 Widget。更好的方式是按页面需求来学:
- 页面结构用
Scaffold。 - 横竖排列用
Row和Column。 - 间距尺寸用
Padding和SizedBox。 - 样式容器用
Container。 - 长列表用
ListView.builder。 - 表单用
Form和TextFormField。 - 异步数据用
FutureBuilder。 - 复杂适配用
LayoutBuilder。
真正开发时,大多数页面都是这些 Widget 的组合。先把常用 Widget 用熟,再去学状态管理、网络请求、路由封装和复杂动画,会轻松很多。
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录