Flutter 入门
最后更新:2026-03-05 前置知识:Dart入门(/dart-ru-men/) 1. Flutter 是什么(#flutter-%E6%98%AF%E4%BB%80%E4%B9%88) 2. 安装与环境配置(#%E5%AE%89%E8%A3%85%E4%B8%8E%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE) 3. 项目结构(#%E9%A1%B9%E7%9B%AE%E7%BB%93%E6%9E%84) 4. 热重载与热重启(#%E7%83%AD%E9%87%8D%E8%BD%BD%E4%B8%8E%E7%83%AD%E9
最后更新:2026-03-05
官方文档:https://docs.flutter.dev/
适用版本:Flutter 3.x(2026-05-07 核实)
前置知识:Dart入门
目录
- Flutter 是什么
- 安装与环境配置
- 项目结构
- 热重载与热重启
- Widget 核心概念
- 常用基础 Widget
- 状态管理
- 导航与路由
- 网络请求
- 本地存储
- 常用 Widget 进阶
- 主题与样式
- 综合实战:新闻阅读 App
Flutter 是什么
Flutter 是 Google 开发的开源 UI 框架,使用 Dart 语言编写,可从单一代码库编译到 Android、iOS、Web、Windows、macOS、Linux 六个平台。
跨平台原理
Flutter 不依赖平台原生控件,而是自带一套渲染引擎(Skia / Impeller),直接在画布上绘制每一个像素。这意味着:
- 所有平台上的 UI 表现完全一致
- 不受原生控件版本差异影响
- 渲染性能接近原生(目标 60/120 fps)
与 React Native 的区别
| 对比项 | Flutter | React Native |
|---|---|---|
| 语言 | Dart | JavaScript / TypeScript |
| 渲染方式 | 自绘引擎,不用原生控件 | 调用原生控件(Bridge / JSI) |
| UI 一致性 | 全平台像素级一致 | 依赖平台,有差异 |
| 性能 | 高,无 Bridge 开销 | JSI 后有所提升,但仍有开销 |
| 生态 | 较新,成长快 | 较成熟,包数量多 |
| 学习曲线 | 需学 Dart | 前端开发者上手快 |
安装与环境配置
Windows
- 下载 Flutter SDK:https://docs.flutter.dev/get-started/install/windows
- 解压到无空格、无中文路径,例如
C:\dev\flutter - 将
C:\dev\flutter\bin加入系统 PATH - 安装 Android Studio,打开 SDK Manager 安装 Android SDK
- 运行
flutter doctor检查环境
# 检查环境
flutter doctor
# 接受 Android 许可证
flutter doctor --android-licenses
macOS
# 使用 Homebrew 安装(推荐)
brew install --cask flutter
# 或手动下载解压后添加到 PATH
export PATH="$HOME/dev/flutter/bin:$PATH"
# iOS 需要 Xcode
xcode-select --install
sudo xcodebuild -license accept
# 检查环境
flutter doctor
flutter doctor 输出示例
Doctor summary (to see all details, run flutter doctor -v):
[v] Flutter (Channel stable, 3.19.0, on macOS 14.0)
[v] Android toolchain - develop for Android devices
[v] Xcode - develop for iOS and macOS
[v] Chrome - develop for the web
[v] Android Studio (version 2023.1)
[v] VS Code (version 1.85.0)
[v] Connected device (3 available)
[v] Network resources
每一项显示 [v] 表示正常,[!] 表示有警告,[x] 表示有问题需要修复。
项目结构
# 创建新项目
flutter create my_app
cd my_app
flutter run
my_app/
├── android/ # Android 原生工程,一般不需要手动修改
├── ios/ # iOS 原生工程,一般不需要手动修改
├── lib/ # Dart 代码,主要工作区
│ └── main.dart # 应用入口
├── test/ # 单元测试
├── web/ # Web 平台配置
├── pubspec.yaml # 项目配置,依赖声明
└── pubspec.lock # 依赖版本锁定文件
lib/main.dart
import 'package:flutter/material.dart';
// 应用入口函数
void main() {
runApp(const MyApp());
}
// 根 Widget,通常是 StatelessWidget
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('首页')),
body: const Center(child: Text('Hello Flutter')),
);
}
}
pubspec.yaml
name: my_app
description: 我的第一个 Flutter 项目
version: 1.0.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
# 添加第三方包
provider: ^6.1.1
http: ^1.2.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.0
flutter:
uses-material-design: true
# 声明本地图片资源
assets:
- assets/images/
# 声明本地字体
fonts:
- family: MyFont
fonts:
- asset: assets/fonts/MyFont-Regular.ttf
添加依赖后运行 flutter pub get 安装。
热重载与热重启
| 操作 | 快捷键(VS Code) | 效果 |
|---|---|---|
| 热重载(Hot Reload) | r(终端)/ Ctrl+F5 |
注入新代码,保留应用状态,速度极快 |
| 热重启(Hot Restart) | R(终端)/ Ctrl+Shift+F5 |
重启 Dart VM,清空状态,比冷启动快 |
| 冷启动 | 重新运行 | 完整编译运行 |
热重载的限制:
- 修改
initState、dispose等生命周期方法后需要热重启 - 修改
main()函数后需要热重启 - 修改原生代码(android/ios 目录)后需要冷启动
Widget 核心概念
一切皆 Widget
Flutter 中的一切都是 Widget:文字、按钮、布局容器、内边距、动画、手势检测器,甚至主题、路由配置也是 Widget。
// 就连 Padding 和 Center 也是 Widget
Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('Hello'),
),
)
StatelessWidget vs StatefulWidget
| 对比项 | StatelessWidget | StatefulWidget |
|---|---|---|
| 状态 | 无内部状态 | 有内部状态(State 对象) |
| 重建时机 | 父 Widget 重建时 | 父重建 或 调用 setState() 时 |
| 使用场景 | 纯展示,数据来自外部 | 需要响应用户交互、维护内部数据 |
| 性能 | 稍好 | 稍差,但影响可忽略 |
// StatelessWidget 示例
class GreetingCard extends StatelessWidget {
final String name; // 数据从外部传入
const GreetingCard({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Text('你好,$name');
}
}
// StatefulWidget 示例
class Counter extends StatefulWidget {
const Counter({super.key});
@override
State<Counter> createState() => _CounterState();
}
class _CounterState extends State<Counter> {
int _count = 0; // 内部状态
void _increment() {
// setState 触发 build 方法重新执行
setState(() {
_count++;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('计数:$_count'),
ElevatedButton(
onPressed: _increment,
child: const Text('加一'),
),
],
);
}
}
Widget 树、Element 树、RenderObject 树
Flutter 内部维护三棵树:
- Widget 树:开发者描述 UI 的配置信息,不可变对象,每次 build 都可能重建
- Element 树:Widget 树的实例化,维护 Widget 与渲染对象之间的对应关系,生命周期比 Widget 长
- RenderObject 树:负责实际的布局计算和绘制
开发者只需关心 Widget 树,Flutter 框架管理另外两棵树,这也是 Flutter 能做到高效更新的原因——当 Widget 重建时,Element 树会复用旧的 Element,只更新变化的部分。
build 方法与 BuildContext
@override
Widget build(BuildContext context) {
// context 代表当前 Widget 在 Widget 树中的位置
// 通过 context 可以向上查找祖先 Widget 的数据
final theme = Theme.of(context); // 获取主题
final mediaQuery = MediaQuery.of(context); // 获取屏幕信息
final navigator = Navigator.of(context); // 获取导航器
return Text(
'屏幕宽度:${mediaQuery.size.width}',
style: theme.textTheme.bodyLarge,
);
}
注意事项:
build方法应该是纯函数,不应有副作用- 不要在
build中做耗时操作 build可能被频繁调用,应保持轻量
常用基础 Widget
Container
Container 是最常用的布局 Widget,类似 CSS 中的 div,可设置大小、内外边距、背景、圆角等。
参数表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
width |
double? |
- | 宽度,不设则由子 Widget 决定 |
height |
double? |
- | 高度 |
margin |
EdgeInsetsGeometry? |
- | 外边距 |
padding |
EdgeInsetsGeometry? |
- | 内边距 |
alignment |
AlignmentGeometry? |
- | 子 Widget 对齐方式 |
decoration |
Decoration? |
- | 背景、边框、圆角、阴影等 |
color |
Color? |
- | 背景色(与 decoration 互斥) |
child |
Widget? |
- | 子 Widget |
constraints |
BoxConstraints? |
- | 尺寸约束 |
transform |
Matrix4? |
- | 变换矩阵 |
Container(
width: 200,
height: 100,
margin: const EdgeInsets.all(16), // 四周 16 像素外边距
padding: const EdgeInsets.symmetric( // 水平 12,垂直 8 内边距
horizontal: 12,
vertical: 8,
),
alignment: Alignment.center, // 子 Widget 居中
decoration: BoxDecoration(
color: Colors.blue.shade100, // 背景色
borderRadius: BorderRadius.circular(12), // 圆角
border: Border.all( // 边框
color: Colors.blue,
width: 2,
),
boxShadow: [ // 阴影
BoxShadow(
color: Colors.black26,
blurRadius: 8,
offset: const Offset(0, 4),
),
],
),
child: const Text('我是 Container'),
)
Row / Column
Row 水平排列子 Widget,Column 垂直排列。
参数表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
children |
List<Widget> |
[] |
子 Widget 列表 |
mainAxisAlignment |
MainAxisAlignment |
.start |
主轴对齐(Row:水平,Column:垂直) |
crossAxisAlignment |
CrossAxisAlignment |
.center |
交叉轴对齐 |
mainAxisSize |
MainAxisSize |
.max |
主轴占用空间:.max 尽量大,.min 包裹内容 |
verticalDirection |
VerticalDirection |
.down |
Column 排列方向 |
MainAxisAlignment 取值
| 取值 | 效果 |
|---|---|
start |
起始端对齐 |
end |
末端对齐 |
center |
居中 |
spaceBetween |
两端对齐,子项间均匀留白 |
spaceAround |
子项两侧留白相等,边缘留白是中间的一半 |
spaceEvenly |
所有空白均匀分配 |
// Row 示例
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(Icons.home),
const Text('标题'),
IconButton(
onPressed: () {},
icon: const Icon(Icons.settings),
),
],
)
// Column 示例
Column(
mainAxisSize: MainAxisSize.min, // 高度包裹内容
crossAxisAlignment: CrossAxisAlignment.start, // 左对齐
children: const [
Text('第一行'),
SizedBox(height: 8), // 间距
Text('第二行'),
SizedBox(height: 8),
Text('第三行'),
],
)
Stack / Positioned
Stack 将子 Widget 叠加显示,类似 CSS 中的 position: absolute。
Stack 参数表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
children |
List<Widget> |
[] |
子 Widget,后面的显示在上层 |
alignment |
AlignmentGeometry |
topStart |
未定位子 Widget 的对齐方式 |
fit |
StackFit |
loose |
未定位子 Widget 的约束方式 |
clipBehavior |
Clip |
hardEdge |
溢出裁剪方式 |
Positioned 参数表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
child |
Widget |
必填 | 子 Widget |
top |
double? |
- | 距顶部距离 |
bottom |
double? |
- | 距底部距离 |
left |
double? |
- | 距左侧距离 |
right |
double? |
- | 距右侧距离 |
width |
double? |
- | 宽度 |
height |
double? |
- | 高度 |
Stack(
alignment: Alignment.center,
children: [
// 底层:背景图
Container(
width: 300,
height: 200,
color: Colors.grey.shade200,
),
// 右上角:徽章
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
child: const Text(
'99',
style: TextStyle(color: Colors.white, fontSize: 12),
),
),
),
// 底部居中:文字说明
const Positioned(
bottom: 16,
left: 0,
right: 0,
child: Text(
'图片说明',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.black54),
),
),
],
)
Expanded / Flexible
在 Row/Column 中按比例分配剩余空间。
参数表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
flex |
int |
1 |
权重,相对于同级 Expanded/Flexible 的比例 |
fit(仅 Flexible) |
FlexFit |
loose |
tight:强制填满,loose:最多占用 flex 比例空间 |
child |
Widget |
必填 | 子 Widget |
Row(
children: [
// 占剩余空间的 2/3
Expanded(
flex: 2,
child: Container(color: Colors.blue, height: 50),
),
// 占剩余空间的 1/3
Expanded(
flex: 1,
child: Container(color: Colors.red, height: 50),
),
],
)
// Flexible 示例:文字不超过可用空间
Row(
children: [
Flexible(
child: Text(
'这是一段很长很长很长很长很长很长的文字,不会溢出',
overflow: TextOverflow.ellipsis, // 超出显示省略号
),
),
const Icon(Icons.arrow_forward),
],
)
SizedBox
固定尺寸的盒子,常用于添加间距或约束子 Widget 大小。
// 固定尺寸
const SizedBox(width: 200, height: 100, child: Text('固定大小'))
// 常用作间距(比 Container 轻量)
const Column(
children: [
Text('第一行'),
SizedBox(height: 16), // 垂直间距
Text('第二行'),
],
)
// 强制子 Widget 充满父容器
SizedBox.expand(child: Container(color: Colors.blue))
Padding / Center / Align
// Padding:添加内边距
Padding(
padding: const EdgeInsets.only(left: 16, top: 8), // 仅左和上
child: const Text('有内边距的文字'),
)
// Center:居中
const Center(child: Text('居中'))
// Align:指定对齐位置
Align(
alignment: Alignment.bottomRight, // 右下角
child: const Text('右下角'),
)
// Alignment 常用值:topLeft, topCenter, topRight,
// centerLeft, center, centerRight,
// bottomLeft, bottomCenter, bottomRight
// 也可以用坐标:Alignment(0.0, 0.0) 表示正中心
Wrap
类似 Row/Column,但子项超出时自动换行/换列。
参数表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
direction |
Axis |
horizontal |
排列方向 |
spacing |
double |
0.0 |
主轴方向的子项间距 |
runSpacing |
double |
0.0 |
交叉轴方向的行间距 |
alignment |
WrapAlignment |
start |
主轴对齐 |
runAlignment |
WrapAlignment |
start |
多行时的行对齐 |
children |
List<Widget> |
[] |
子 Widget |
// 标签流式布局
Wrap(
spacing: 8, // 横向间距
runSpacing: 8, // 纵向间距
children: ['Flutter', 'Dart', 'Android', 'iOS', 'Web', 'Windows']
.map(
(tag) => Chip(
label: Text(tag),
backgroundColor: Colors.blue.shade100,
),
)
.toList(),
)
Text
参数表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
data |
String |
必填 | 文字内容 |
style |
TextStyle? |
- | 文字样式 |
textAlign |
TextAlign? |
- | 水平对齐 |
maxLines |
int? |
- | 最大行数 |
overflow |
TextOverflow? |
- | 溢出处理 |
softWrap |
bool? |
true |
是否自动换行 |
textScaler |
TextScaler? |
- | 文字缩放 |
TextStyle 参数表
| 参数 | 类型 | 说明 |
|---|---|---|
fontSize |
double? |
字体大小 |
fontWeight |
FontWeight? |
字重:w100~w900,bold = w700 |
fontStyle |
FontStyle? |
normal / italic |
color |
Color? |
文字颜色 |
backgroundColor |
Color? |
背景色 |
letterSpacing |
double? |
字间距 |
wordSpacing |
double? |
词间距 |
height |
double? |
行高(相对于 fontSize 的倍数) |
decoration |
TextDecoration? |
装饰线:underline/overline/lineThrough |
decorationColor |
Color? |
装饰线颜色 |
fontFamily |
String? |
字体族 |
shadows |
List<Shadow>? |
文字阴影 |
Text(
'这是标题文字',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
letterSpacing: 1.5,
height: 1.4,
decoration: TextDecoration.none,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
RichText / TextSpan
在同一段文字中混合多种样式。
RichText(
text: TextSpan(
// 默认样式,子 Span 会继承
style: const TextStyle(fontSize: 16, color: Colors.black),
children: [
const TextSpan(text: '普通文字,'),
TextSpan(
text: '蓝色加粗,',
style: const TextStyle(
color: Colors.blue,
fontWeight: FontWeight.bold,
),
),
TextSpan(
text: '可点击链接',
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () {
// 点击回调(需要 import 'package:flutter/gestures.dart')
print('链接被点击');
},
),
],
),
)
Image
参数表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
fit |
BoxFit? |
- | 图片适应方式 |
width |
double? |
- | 宽度 |
height |
double? |
- | 高度 |
color |
Color? |
- | 混合颜色(配合 colorBlendMode 使用) |
alignment |
Alignment |
center |
图片对齐方式 |
repeat |
ImageRepeat |
noRepeat |
平铺方式 |
errorBuilder |
Function? |
- | 加载失败时显示的 Widget |
loadingBuilder |
Function? |
- | 加载中显示的 Widget(仅 network) |
BoxFit 取值
| 取值 | 效果 |
|---|---|
fill |
拉伸填满,不保持比例 |
contain |
保持比例,显示完整图片 |
cover |
保持比例,覆盖整个区域,可能裁剪 |
fitWidth |
宽度适应,高度可能裁剪 |
fitHeight |
高度适应,宽度可能裁剪 |
none |
原始大小,不缩放 |
scaleDown |
仅缩小,不放大 |
// 本地资源图片(需在 pubspec.yaml 的 assets 中声明)
Image.asset(
'assets/images/logo.png',
width: 100,
height: 100,
fit: BoxFit.contain,
)
// 网络图片
Image.network(
'https://example.com/photo.jpg',
width: 300,
height: 200,
fit: BoxFit.cover,
// 加载中显示进度
loadingBuilder: (context, child, loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
},
// 加载失败显示占位
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 100);
},
)
Icon
// 使用 Material Icons
const Icon(Icons.home, size: 32, color: Colors.blue)
const Icon(Icons.favorite, size: 24, color: Colors.red)
const Icon(Icons.settings)
// 常用图标
// Icons.home, Icons.search, Icons.person, Icons.settings
// Icons.add, Icons.delete, Icons.edit, Icons.close
// Icons.arrow_back, Icons.arrow_forward
// Icons.favorite, Icons.favorite_border
// Icons.share, Icons.download, Icons.upload
// Icons.notification_add, Icons.notifications
// Icons.menu, Icons.more_vert(竖三点), Icons.more_horiz(横三点)
ElevatedButton / TextButton / OutlinedButton
参数表(三种按钮通用)
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
onPressed |
VoidCallback? |
- | 点击回调,为 null 时按钮禁用 |
onLongPress |
VoidCallback? |
- | 长按回调 |
child |
Widget |
必填 | 按钮内容 |
style |
ButtonStyle? |
- | 样式配置 |
autofocus |
bool |
false |
是否自动获取焦点 |
ButtonStyle 常用参数
| 参数 | 类型 | 说明 |
|---|---|---|
backgroundColor |
MaterialStateProperty<Color?> |
背景色 |
foregroundColor |
MaterialStateProperty<Color?> |
前景色(文字、图标) |
padding |
MaterialStateProperty<EdgeInsetsGeometry?> |
内边距 |
shape |
MaterialStateProperty<OutlinedBorder?> |
形状 |
elevation |
MaterialStateProperty<double?> |
海拔阴影 |
minimumSize |
MaterialStateProperty<Size?> |
最小尺寸 |
fixedSize |
MaterialStateProperty<Size?> |
固定尺寸 |
// ElevatedButton:有背景色的实心按钮
ElevatedButton(
onPressed: () => print('点击'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, // 背景色
foregroundColor: Colors.white, // 文字颜色
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 4,
minimumSize: const Size(120, 48),
),
child: const Text('确认'),
)
// TextButton:无背景的文字按钮
TextButton(
onPressed: () => print('取消'),
child: const Text('取消'),
)
// OutlinedButton:有边框的按钮
OutlinedButton(
onPressed: () {},
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.blue, width: 2),
),
child: const Text('次要操作'),
)
// IconButton:图标按钮
IconButton(
onPressed: () {},
icon: const Icon(Icons.favorite),
color: Colors.red,
iconSize: 32,
tooltip: '收藏', // 长按显示的提示文字
)
// FloatingActionButton:悬浮按钮
FloatingActionButton(
onPressed: () {},
tooltip: '添加',
child: const Icon(Icons.add),
)
// 禁用按钮:将 onPressed 设为 null
ElevatedButton(
onPressed: null, // 禁用
child: const Text('已禁用'),
)
TextField
参数表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
controller |
TextEditingController? |
- | 控制器,用于读取/设置文字 |
decoration |
InputDecoration? |
- | 外观装饰 |
keyboardType |
TextInputType? |
- | 键盘类型 |
obscureText |
bool |
false |
是否隐藏文字(密码输入) |
maxLines |
int? |
1 |
最大行数,为 null 则不限 |
minLines |
int? |
- | 最小行数 |
maxLength |
int? |
- | 最大字符数 |
onChanged |
Function(String)? |
- | 文字变化回调 |
onSubmitted |
Function(String)? |
- | 提交(回车)回调 |
onTap |
VoidCallback? |
- | 点击回调 |
enabled |
bool? |
true |
是否可用 |
autofocus |
bool |
false |
是否自动聚焦 |
focusNode |
FocusNode? |
- | 焦点控制 |
style |
TextStyle? |
- | 文字样式 |
textAlign |
TextAlign |
start |
对齐方式 |
readOnly |
bool |
false |
只读模式 |
InputDecoration 参数表
| 参数 | 类型 | 说明 |
|---|---|---|
labelText |
String? |
标签文字(聚焦时上移) |
hintText |
String? |
占位提示文字 |
helperText |
String? |
下方帮助文字 |
errorText |
String? |
错误提示文字(红色) |
prefixIcon |
Widget? |
左侧图标 |
suffixIcon |
Widget? |
右侧图标 |
prefixText |
String? |
左侧文字 |
suffixText |
String? |
右侧文字 |
border |
InputBorder? |
边框 |
filled |
bool? |
是否填充背景色 |
fillColor |
Color? |
填充背景色 |
contentPadding |
EdgeInsetsGeometry? |
内容内边距 |
// TextEditingController 用法
class LoginForm extends StatefulWidget {
const LoginForm({super.key});
@override
State<LoginForm> createState() => _LoginFormState();
}
class _LoginFormState extends State<LoginForm> {
// 1. 创建控制器
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
// 2. 销毁时释放资源(重要!防止内存泄漏)
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _login() {
// 3. 读取文字
final email = _emailController.text.trim();
final password = _passwordController.text;
print('邮箱:$email,密码:$password');
}
@override
Widget build(BuildContext context) {
return Column(
children: [
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: '邮箱',
hintText: '请输入邮箱地址',
prefixIcon: Icon(Icons.email),
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
obscureText: true, // 密码隐藏
decoration: InputDecoration(
labelText: '密码',
hintText: '请输入密码',
prefixIcon: const Icon(Icons.lock),
border: const OutlineInputBorder(),
suffixIcon: IconButton( // 显示/隐藏密码按钮
onPressed: () {},
icon: const Icon(Icons.visibility),
),
),
onSubmitted: (_) => _login(), // 回车触发登录
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _login,
child: const Text('登录'),
),
],
);
}
}
ListView
参数表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
children |
List<Widget>? |
- | 静态子列表(少量数据用) |
scrollDirection |
Axis |
vertical |
滚动方向 |
reverse |
bool |
false |
是否反向排列 |
physics |
ScrollPhysics? |
- | 滚动物理效果 |
shrinkWrap |
bool |
false |
是否根据内容计算高度 |
padding |
EdgeInsetsGeometry? |
- | 列表内边距 |
itemExtent |
double? |
- | 固定子项高度(优化性能) |
primary |
bool? |
- | 是否是主滚动视图 |
ListView.builder 参数表
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
itemCount |
int? |
- | 总项数,不填则无限 |
itemBuilder |
IndexedWidgetBuilder |
必填 | 构建每项的函数 |
scrollDirection |
Axis |
vertical |
滚动方向 |
physics |
ScrollPhysics? |
- | 滚动物理效果 |
shrinkWrap |
bool |
false |
是否根据内容计算高度 |
// 静态列表(数据量少时使用)
ListView(
padding: const EdgeInsets.all(16),
children: const [
ListTile(
leading: Icon(Icons.home),
title: Text('首页'),
subtitle: Text('返回主页'),
trailing: Icon(Icons.arrow_forward_ios),
),
Divider(), // 分割线
ListTile(
leading: Icon(Icons.settings),
title: Text('设置'),
),
],
)
// 动态列表(数据量大时使用,按需创建 Widget)
final List<String> items = List.generate(100, (i) => '列表项 ${i + 1}');
ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(items[index]),
onTap: () => print('点击了 ${items[index]}'),
);
},
)
// 水平滚动列表
SizedBox(
height: 120,
child: ListView.builder(
scrollDirection: Axis.horizontal, // 水平滚动
itemCount: 10,
itemBuilder: (context, index) {
return Container(
width: 100,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius: BorderRadius.circular(8),
),
child: Center(child: Text('卡片 $index')),
);
},
),
)
GridView.builder
gridDelegate 说明
| 类型 | 说明 |
|---|---|
SliverGridDelegateWithFixedCrossAxisCount |
固定列数 |
SliverGridDelegateWithMaxCrossAxisExtent |
固定每项最大宽度 |
SliverGridDelegateWithFixedCrossAxisCount 参数
| 参数 | 类型 | 说明 |
|---|---|---|
crossAxisCount |
int |
列数(必填) |
mainAxisSpacing |
double |
主轴方向间距 |
crossAxisSpacing |
double |
交叉轴方向间距 |
childAspectRatio |
double |
子项宽高比(默认 1.0) |
GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, // 3 列
crossAxisSpacing: 8, // 列间距
mainAxisSpacing: 8, // 行间距
childAspectRatio: 1.0, // 宽高比 1:1(正方形)
),
itemCount: 30,
itemBuilder: (context, index) {
return Container(
decoration: BoxDecoration(
color: Colors.primaries[index % Colors.primaries.length].shade200,
borderRadius: BorderRadius.circular(8),
),
child: Center(child: Text('$index')),
);
},
)
SingleChildScrollView
当内容可能超出屏幕时,包裹在 SingleChildScrollView 中使其可滚动。
SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('表单标题', style: TextStyle(fontSize: 24)),
const SizedBox(height: 16),
const TextField(decoration: InputDecoration(labelText: '姓名')),
const SizedBox(height: 16),
const TextField(decoration: InputDecoration(labelText: '邮箱')),
const SizedBox(height: 16),
const TextField(
maxLines: 5,
decoration: InputDecoration(
labelText: '备注',
border: OutlineInputBorder(),
),
),
const SizedBox(height: 24),
ElevatedButton(onPressed: () {}, child: const Text('提交')),
],
),
)
AppBar
参数表
| 参数 | 类型 | 说明 |
|---|---|---|
title |
Widget? |
标题 Widget |
leading |
Widget? |
左侧 Widget(默认返回按钮或菜单图标) |
actions |
List<Widget>? |
右侧操作按钮列表 |
bottom |
PreferredSizeWidget? |
底部 Widget(常用 TabBar) |
backgroundColor |
Color? |
背景色 |
foregroundColor |
Color? |
前景色(标题、图标) |
elevation |
double? |
阴影高度 |
centerTitle |
bool? |
标题是否居中 |
automaticallyImplyLeading |
bool |
是否自动添加返回按钮 |
AppBar(
title: const Text('文章详情'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.pop(context),
),
actions: [
IconButton(
icon: const Icon(Icons.share),
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.more_vert),
onPressed: () {},
),
],
bottom: const TabBar(
tabs: [
Tab(text: '推荐'),
Tab(text: '最新'),
Tab(text: '热门'),
],
),
)
BottomNavigationBar / NavigationBar
// BottomNavigationBar(经典样式)
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
final List<Widget> _pages = [
const HomePage(),
const FavoritePage(),
const ProfilePage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: _pages[_currentIndex],
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (index) => setState(() => _currentIndex = index),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.home_outlined),
activeIcon: Icon(Icons.home),
label: '首页',
),
BottomNavigationBarItem(
icon: Icon(Icons.favorite_outline),
activeIcon: Icon(Icons.favorite),
label: '收藏',
),
BottomNavigationBarItem(
icon: Icon(Icons.person_outline),
activeIcon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}
// NavigationBar(Flutter 3+ Material You 风格,推荐)
NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) => setState(() => _currentIndex = index),
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: '首页',
),
NavigationDestination(
icon: Icon(Icons.favorite_outline),
selectedIcon: Icon(Icons.favorite),
label: '收藏',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: '我的',
),
],
)
Drawer
Scaffold(
appBar: AppBar(title: const Text('首页')),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
// 抽屉头部
UserAccountsDrawerHeader(
accountName: const Text('张三'),
accountEmail: const Text('[email protected]'),
currentAccountPicture: const CircleAvatar(
backgroundColor: Colors.white,
child: Icon(Icons.person, size: 40),
),
decoration: const BoxDecoration(color: Colors.blue),
),
// 菜单项
ListTile(
leading: const Icon(Icons.home),
title: const Text('首页'),
onTap: () {
Navigator.pop(context); // 关闭抽屉
},
),
ListTile(
leading: const Icon(Icons.settings),
title: const Text('设置'),
onTap: () {
Navigator.pop(context);
Navigator.pushNamed(context, '/settings');
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.logout),
title: const Text('退出登录'),
onTap: () {},
),
],
),
),
body: const Center(child: Text('内容区域')),
)
状态管理
setState 基础用法
class ShoppingCart extends StatefulWidget {
const ShoppingCart({super.key});
@override
State<ShoppingCart> createState() => _ShoppingCartState();
}
class _ShoppingCartState extends State<ShoppingCart> {
final List<String> _items = [];
void _addItem(String item) {
// setState 内部同步修改状态,Flutter 会在下一帧重建 Widget
setState(() {
_items.add(item);
});
}
void _removeItem(int index) {
setState(() {
_items.removeAt(index);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('购物车(${_items.length})')),
body: ListView.builder(
itemCount: _items.length,
itemBuilder: (context, index) => ListTile(
title: Text(_items[index]),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _removeItem(index),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _addItem('商品 ${_items.length + 1}'),
child: const Icon(Icons.add),
),
);
}
}
setState 注意事项
- 不要在
build方法中调用setState setState内部应只做状态修改,不做耗时操作- 异步操作完成后调用
setState前,先检查mounted(防止组件已销毁)
Future<void> _loadData() async {
final data = await fetchDataFromNetwork();
// 异步完成后检查组件是否还在树中
if (!mounted) return;
setState(() {
_data = data;
});
}
InheritedWidget 原理
InheritedWidget 是 Flutter 内置的状态向下传递机制,Provider 等状态管理库都基于它实现。它允许子 Widget 在 Widget 树中向上查找祖先数据,而无需逐层传递参数。
// 简单示例(理解原理,实际使用 Provider)
class AppState extends InheritedWidget {
final int counter;
const AppState({
super.key,
required this.counter,
required super.child,
});
// 子 Widget 通过 of 方法获取数据
static AppState of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppState>()!;
}
// 返回 true 时,依赖此 Widget 的子 Widget 会重建
@override
bool updateShouldNotify(AppState oldWidget) {
return counter != oldWidget.counter;
}
}
Provider
Provider 是官方推荐的轻量级状态管理方案。
# 添加依赖
flutter pub add provider
核心 API 参数表
| 类/方法 | 参数 | 说明 |
|---|---|---|
ChangeNotifierProvider |
create:(context) => T |
创建 ChangeNotifier 实例 |
ChangeNotifierProvider |
child:Widget |
子 Widget 树 |
Consumer<T> |
builder:(context, T value, child) |
监听状态变化并重建 |
context.watch<T>() |
无参数 | 监听 T 的变化,变化时重建当前 Widget |
context.read<T>() |
无参数 | 读取 T,不监听变化(用于事件处理) |
context.select<T, R>() |
selector:(T) => R |
只监听 T 的某个字段变化 |
// 1. 定义状态类
class FavoriteModel extends ChangeNotifier {
final List<String> _favorites = [];
List<String> get favorites => List.unmodifiable(_favorites);
bool isFavorite(String id) => _favorites.contains(id);
void toggle(String id) {
if (_favorites.contains(id)) {
_favorites.remove(id);
} else {
_favorites.add(id);
}
notifyListeners(); // 通知所有监听者重建
}
}
// 2. 在根 Widget 提供状态
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => FavoriteModel(),
child: const MyApp(),
),
);
}
// 3. 在子 Widget 中使用
class ArticleCard extends StatelessWidget {
final String articleId;
final String title;
const ArticleCard({super.key, required this.articleId, required this.title});
@override
Widget build(BuildContext context) {
// context.watch 监听变化,isFavorite 改变时重建
final isFavorite = context.watch<FavoriteModel>().isFavorite(articleId);
return ListTile(
title: Text(title),
trailing: IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : null,
),
onPressed: () {
// context.read 不监听,只执行操作
context.read<FavoriteModel>().toggle(articleId);
},
),
);
}
}
// 提供多个状态(MultiProvider)
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => FavoriteModel()),
ChangeNotifierProvider(create: (_) => UserModel()),
],
child: const MyApp(),
)
Riverpod 简介
Riverpod 是 Provider 的升级版,解决了 Provider 的一些限制,是目前最受推荐的状态管理方案之一。
flutter pub add flutter_riverpod
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 定义 Provider
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
return CounterNotifier();
});
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
void decrement() => state--;
}
// 根 Widget 包裹 ProviderScope
void main() {
runApp(const ProviderScope(child: MyApp()));
}
// 使用状态:ConsumerWidget 替代 StatelessWidget
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider); // 监听
return Scaffold(
body: Center(child: Text('$count')),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}
导航与路由
Navigator.push / pop
参数表
| 方法 | 参数 | 说明 |
|---|---|---|
Navigator.push |
context, route |
进入新页面 |
Navigator.pop |
context, [result] |
返回上一页,可传返回值 |
Navigator.pushReplacement |
context, route |
替换当前页面 |
Navigator.pushAndRemoveUntil |
context, route, predicate |
清空栈并跳转 |
Navigator.canPop |
context |
是否可以返回 |
MaterialPageRoute 参数表
| 参数 | 类型 | 说明 |
|---|---|---|
builder |
(context) => Widget |
目标页面构建函数(必填) |
settings |
RouteSettings? |
路由设置(名称、参数) |
fullscreenDialog |
bool |
是否全屏弹出(iOS 样式) |
maintainState |
bool |
是否保持状态(默认 true) |
// 基础跳转
ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DetailPage(id: '123'),
),
);
},
child: const Text('查看详情'),
)
// 接收返回值
ElevatedButton(
onPressed: () async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => const SelectPage()),
);
if (result != null) {
print('用户选择了:$result');
}
},
child: const Text('选择'),
)
// 返回时传值
Navigator.pop(context, '用户的选择');
// 替换当前页(登录后跳转首页,不允许返回登录页)
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const HomePage()),
);
// 清空栈并跳转(退出登录)
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
(route) => false, // false 表示清空所有
);
命名路由
// 在 MaterialApp 配置路由表
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => const HomePage(),
'/detail': (context) => const DetailPage(),
'/settings': (context) => const SettingsPage(),
},
)
// 跳转
Navigator.pushNamed(context, '/detail');
// 传参(通过 arguments)
Navigator.pushNamed(
context,
'/detail',
arguments: {'id': '123', 'title': '文章标题'},
);
// 接收参数
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments
as Map<String, String>;
final id = args['id'];
final title = args['title'];
return Scaffold(
appBar: AppBar(title: Text(title ?? '')),
body: Text('文章 ID:$id'),
);
}
}
go_router(推荐)
go_router 是 Flutter 官方维护的声明式路由库,支持 URL 路由、嵌套路由、重定向等。
flutter pub add go_router
import 'package:go_router/go_router.dart';
// 配置路由
final GoRouter router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/detail/:id', // 路径参数
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailPage(id: id);
},
),
GoRoute(
path: '/search',
builder: (context, state) {
final query = state.uri.queryParameters['q'] ?? '';
return SearchPage(query: query);
},
),
],
// 重定向(例如未登录跳转登录页)
redirect: (context, state) {
final isLoggedIn = UserService.isLoggedIn;
if (!isLoggedIn && state.matchedLocation != '/login') {
return '/login';
}
return null; // 不重定向
},
);
// 在 MaterialApp.router 使用
MaterialApp.router(routerConfig: router)
// 跳转
context.go('/'); // 替换当前路由
context.push('/detail/123'); // 叠加路由(可返回)
context.pop(); // 返回
context.go('/search?q=flutter'); // 带查询参数
网络请求
http 包
flutter pub add http
参数表
| 方法 | 必填参数 | 可选参数 | 说明 |
|---|---|---|---|
http.get |
Uri url |
Map<String, String>? headers |
GET 请求 |
http.post |
Uri url |
headers, body, encoding |
POST 请求 |
http.put |
Uri url |
headers, body, encoding |
PUT 请求 |
http.delete |
Uri url |
headers, body |
DELETE 请求 |
import 'dart:convert';
import 'package:http/http.dart' as http;
// 模型类
class Article {
final int id;
final String title;
final String body;
Article({required this.id, required this.title, required this.body});
// 从 JSON 创建对象
factory Article.fromJson(Map<String, dynamic> json) {
return Article(
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
);
}
// 转为 JSON
Map<String, dynamic> toJson() => {
'id': id,
'title': title,
'body': body,
};
}
// GET 请求
Future<List<Article>> fetchArticles() async {
final response = await http.get(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer your_token',
},
);
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((json) => Article.fromJson(json)).toList();
} else {
throw Exception('请求失败:${response.statusCode}');
}
}
// POST 请求
Future<Article> createArticle(String title, String body) async {
final response = await http.post(
Uri.parse('https://jsonplaceholder.typicode.com/posts'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({'title': title, 'body': body, 'userId': 1}),
);
if (response.statusCode == 201) {
return Article.fromJson(jsonDecode(response.body));
} else {
throw Exception('创建失败:${response.statusCode}');
}
}
dio 包
dio 功能更强大,支持拦截器、文件上传下载、FormData 等。
flutter pub add dio
BaseOptions 参数表
| 参数 | 类型 | 说明 |
|---|---|---|
baseUrl |
String? |
基础 URL |
connectTimeout |
Duration? |
连接超时 |
receiveTimeout |
Duration? |
接收超时 |
sendTimeout |
Duration? |
发送超时 |
headers |
Map<String, dynamic>? |
默认请求头 |
contentType |
String? |
默认 Content-Type |
responseType |
ResponseType |
响应类型(json/stream/plain/bytes) |
import 'package:dio/dio.dart';
// 封装 API 客户端
class ApiClient {
static final Dio _dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
headers: {'Content-Type': 'application/json'},
),
)
// 请求/响应拦截器
..interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// 在请求头中自动加入 Token
options.headers['Authorization'] = 'Bearer ${UserService.token}';
handler.next(options); // 继续请求
},
onResponse: (response, handler) {
// 统一处理响应
handler.next(response);
},
onError: (error, handler) {
// 统一处理错误(如 401 跳转登录)
if (error.response?.statusCode == 401) {
UserService.logout();
}
handler.next(error);
},
),
)
// 日志拦截器
..interceptors.add(LogInterceptor(responseBody: true));
static Future<List<Article>> getArticles() async {
try {
final response = await _dio.get('/articles');
return (response.data as List)
.map((json) => Article.fromJson(json))
.toList();
} on DioException catch (e) {
throw Exception('请求失败:${e.message}');
}
}
static Future<void> uploadFile(String filePath) async {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(filePath, filename: 'upload.jpg'),
});
await _dio.post('/upload', data: formData);
}
}
本地存储
shared_preferences
适合存储简单的键值对(用户偏好设置、Token 等)。
flutter pub add shared_preferences
参数表
| 方法 | 参数 | 说明 |
|---|---|---|
getString(key) |
String key |
读取字符串 |
setString(key, value) |
String key, String value |
写入字符串 |
getInt(key) |
String key |
读取整数 |
setInt(key, value) |
String key, int value |
写入整数 |
getBool(key) |
String key |
读取布尔值 |
setBool(key, value) |
String key, bool value |
写入布尔值 |
getDouble(key) |
String key |
读取浮点数 |
getStringList(key) |
String key |
读取字符串列表 |
remove(key) |
String key |
删除指定键 |
clear() |
无 | 清除所有数据 |
containsKey(key) |
String key |
是否包含指定键 |
import 'package:shared_preferences/shared_preferences.dart';
class PrefsService {
static const _keyToken = 'auth_token';
static const _keyDarkMode = 'dark_mode';
// 保存 Token
static Future<void> saveToken(String token) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keyToken, token);
}
// 读取 Token
static Future<String?> getToken() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getString(_keyToken);
}
// 保存深色模式偏好
static Future<void> setDarkMode(bool enabled) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyDarkMode, enabled);
}
static Future<bool> getDarkMode() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_keyDarkMode) ?? false; // 默认 false
}
// 清除登录信息
static Future<void> clearAuth() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_keyToken);
}
}
sqflite 基础
适合存储结构化数据。
flutter pub add sqflite path
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseService {
static Database? _db;
static Future<Database> get database async {
_db ??= await _initDatabase();
return _db!;
}
static Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'app.db');
return openDatabase(
path,
version: 1,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE articles (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
body TEXT,
is_favorite INTEGER DEFAULT 0
)
''');
},
);
}
// 插入
static Future<int> insertArticle(Map<String, dynamic> article) async {
final db = await database;
return db.insert('articles', article,
conflictAlgorithm: ConflictAlgorithm.replace);
}
// 查询全部
static Future<List<Map<String, dynamic>>> getArticles() async {
final db = await database;
return db.query('articles', orderBy: 'id DESC');
}
// 查询收藏
static Future<List<Map<String, dynamic>>> getFavorites() async {
final db = await database;
return db.query('articles', where: 'is_favorite = ?', whereArgs: [1]);
}
// 更新
static Future<int> updateFavorite(int id, bool isFavorite) async {
final db = await database;
return db.update(
'articles',
{'is_favorite': isFavorite ? 1 : 0},
where: 'id = ?',
whereArgs: [id],
);
}
// 删除
static Future<int> deleteArticle(int id) async {
final db = await database;
return db.delete('articles', where: 'id = ?', whereArgs: [id]);
}
}
常用 Widget 进阶
Form 表单验证
class RegisterForm extends StatefulWidget {
const RegisterForm({super.key});
@override
State<RegisterForm> createState() => _RegisterFormState();
}
class _RegisterFormState extends State<RegisterForm> {
// GlobalKey 用于访问 FormState
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
void _submit() {
// 触发所有 TextFormField 的 validator
if (_formKey.currentState!.validate()) {
print('表单验证通过');
print('邮箱:${_emailController.text}');
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
labelText: '邮箱',
border: OutlineInputBorder(),
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入邮箱';
}
if (!value.contains('@')) {
return '邮箱格式不正确';
}
return null; // 返回 null 表示验证通过
},
),
const SizedBox(height: 16),
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: 24),
ElevatedButton(onPressed: _submit, child: const Text('注册')),
],
),
);
}
}
FutureBuilder
用于处理异步数据加载,根据 Future 的状态显示不同 UI。
参数表
| 参数 | 类型 | 说明 |
|---|---|---|
future |
Future<T>? |
要监听的 Future |
builder |
(context, AsyncSnapshot<T>) => Widget |
构建函数(必填) |
initialData |
T? |
初始数据(Future 完成前显示) |
AsyncSnapshot 状态
snapshot.connectionState |
说明 |
|---|---|
ConnectionState.none |
无 Future |
ConnectionState.waiting |
等待中 |
ConnectionState.active |
进行中(Stream 用) |
ConnectionState.done |
完成 |
FutureBuilder<List<Article>>(
future: ApiClient.getArticles(),
builder: (context, snapshot) {
// 加载中
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
// 发生错误
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text('加载失败:${snapshot.error}'),
ElevatedButton(
onPressed: () => setState(() {}), // 刷新
child: const Text('重试'),
),
],
),
);
}
// 数据为空
if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('暂无数据'));
}
// 显示数据
final articles = snapshot.data!;
return ListView.builder(
itemCount: articles.length,
itemBuilder: (context, index) => ListTile(
title: Text(articles[index].title),
),
);
},
)
StreamBuilder
与 FutureBuilder 类似,但用于持续更新的数据流(WebSocket、实时数据库等)。
StreamBuilder<int>(
stream: Stream.periodic(
const Duration(seconds: 1),
(count) => count,
),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text('等待数据...');
return Text('已过 ${snapshot.data} 秒');
},
)
GestureDetector
参数表
| 参数 | 类型 | 说明 |
|---|---|---|
onTap |
VoidCallback? |
单击 |
onDoubleTap |
VoidCallback? |
双击 |
onLongPress |
VoidCallback? |
长按 |
onPanUpdate |
Function(DragUpdateDetails)? |
拖动 |
onScaleUpdate |
Function(ScaleUpdateDetails)? |
缩放 |
behavior |
HitTestBehavior? |
命中测试行为 |
child |
Widget? |
子 Widget |
GestureDetector(
onTap: () => print('单击'),
onDoubleTap: () => print('双击'),
onLongPress: () => print('长按'),
// 拖动
onPanUpdate: (details) {
print('拖动:dx=${details.delta.dx}, dy=${details.delta.dy}');
},
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: const Center(child: Text('点我')),
),
)
AnimatedContainer
AnimatedContainer 会自动对属性变化进行动画过渡。
class AnimatedBox extends StatefulWidget {
const AnimatedBox({super.key});
@override
State<AnimatedBox> createState() => _AnimatedBoxState();
}
class _AnimatedBoxState extends State<AnimatedBox> {
bool _expanded = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _expanded = !_expanded),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300), // 动画时长
curve: Curves.easeInOut, // 缓动曲线
width: _expanded ? 200 : 100,
height: _expanded ? 200 : 100,
color: _expanded ? Colors.blue : Colors.red,
child: const Center(child: Text('点击')),
),
);
}
}
Hero 动画
Hero 实现两个页面之间的共享元素过渡动画。
// 第一个页面
Hero(
tag: 'article-image-${article.id}', // tag 必须唯一
child: Image.network(article.imageUrl, fit: BoxFit.cover),
)
// 第二个页面(使用相同 tag)
Hero(
tag: 'article-image-${article.id}',
child: Image.network(article.imageUrl, fit: BoxFit.contain),
)
AnimatedOpacity
class FadeInWidget extends StatefulWidget {
const FadeInWidget({super.key});
@override
State<FadeInWidget> createState() => _FadeInWidgetState();
}
class _FadeInWidgetState extends State<FadeInWidget> {
double _opacity = 0;
@override
void initState() {
super.initState();
// 显示后淡入
Future.delayed(
const Duration(milliseconds: 100),
() => setState(() => _opacity = 1.0),
);
}
@override
Widget build(BuildContext context) {
return AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: _opacity,
child: const Text('淡入显示的内容'),
);
}
}
主题与样式
ThemeData 参数表
| 参数 | 类型 | 说明 |
|---|---|---|
colorScheme |
ColorScheme? |
Material 3 颜色方案 |
useMaterial3 |
bool |
是否使用 Material 3 |
textTheme |
TextTheme? |
文字主题 |
appBarTheme |
AppBarTheme? |
AppBar 全局样式 |
elevatedButtonTheme |
ElevatedButtonThemeData? |
ElevatedButton 全局样式 |
inputDecorationTheme |
InputDecorationTheme? |
TextField 全局样式 |
cardTheme |
CardTheme? |
Card 全局样式 |
scaffoldBackgroundColor |
Color? |
Scaffold 背景色 |
MaterialApp(
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.light,
),
textTheme: const TextTheme(
headlineLarge: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
bodyLarge: TextStyle(fontSize: 16, height: 1.6),
),
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 48), // 按钮默认全宽
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
darkTheme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
brightness: Brightness.dark,
),
),
themeMode: ThemeMode.system, // 跟随系统深色模式
home: const HomePage(),
)
在 Widget 中读取主题:
// 读取当前主题
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
Text(
'标题',
style: theme.textTheme.headlineMedium?.copyWith(
color: colorScheme.primary,
),
)
响应式布局
MediaQuery
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context); // 屏幕尺寸
final padding = MediaQuery.paddingOf(context); // 安全区域(刘海、导航栏)
// 根据屏幕宽度决定布局
final isTablet = size.width > 600;
return isTablet
? Row(children: [sidebar, content]) // 平板:左右布局
: Column(children: [content]); // 手机:上下布局
}
LayoutBuilder
LayoutBuilder 根据父容器约束来布局,比 MediaQuery 更准确。
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth > 600) {
// 宽屏:两列网格
return GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: 10,
itemBuilder: (context, index) => Card(child: Text('$index')),
);
} else {
// 窄屏:单列列表
return ListView.builder(
itemCount: 10,
itemBuilder: (context, index) => ListTile(title: Text('$index')),
);
}
},
)
综合实战:新闻阅读 App
本节实现一个包含底部导航、文章列表、详情页、收藏功能的完整 App。
项目结构
lib/
├── main.dart
├── models/
│ └── article.dart # 文章数据模型
├── providers/
│ └── favorite_provider.dart # 收藏状态管理
├── services/
│ └── news_service.dart # 网络请求
└── pages/
├── main_page.dart # 底部导航容器
├── home_page.dart # 首页(文章列表)
├── detail_page.dart # 文章详情
├── favorite_page.dart # 收藏列表
└── profile_page.dart # 我的
models/article.dart
class Article {
final int id;
final String title;
final String body;
final String author;
final String imageUrl;
const Article({
required this.id,
required this.title,
required this.body,
required this.author,
required this.imageUrl,
});
factory Article.fromJson(Map<String, dynamic> json) {
return Article(
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
author: 'User ${json['userId']}',
imageUrl: 'https://picsum.photos/seed/${json['id']}/400/200',
);
}
}
providers/favorite_provider.dart
import 'package:flutter/foundation.dart';
import '../models/article.dart';
class FavoriteProvider extends ChangeNotifier {
final List<Article> _favorites = [];
List<Article> get favorites => List.unmodifiable(_favorites);
bool isFavorite(int id) => _favorites.any((a) => a.id == id);
void toggle(Article article) {
if (isFavorite(article.id)) {
_favorites.removeWhere((a) => a.id == article.id);
} else {
_favorites.add(article);
}
notifyListeners();
}
}
services/news_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/article.dart';
class NewsService {
static const _baseUrl = 'https://jsonplaceholder.typicode.com';
static Future<List<Article>> getArticles({int page = 1}) async {
final response = await http.get(
Uri.parse('$_baseUrl/posts?_page=$page&_limit=20'),
);
if (response.statusCode == 200) {
final List<dynamic> jsonList = jsonDecode(response.body);
return jsonList.map((json) => Article.fromJson(json)).toList();
} else {
throw Exception('加载文章失败:${response.statusCode}');
}
}
}
main.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'pages/main_page.dart';
import 'providers/favorite_provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => FavoriteProvider(),
child: const MyApp(),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '新闻阅读',
debugShowCheckedModeBanner: false,
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
),
home: const MainPage(),
);
}
}
pages/main_page.dart
import 'package:flutter/material.dart';
import 'home_page.dart';
import 'favorite_page.dart';
import 'profile_page.dart';
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
final List<Widget> _pages = const [
HomePage(),
FavoritePage(),
ProfilePage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack( // 保持各页面状态(不用 _pages[_currentIndex])
index: _currentIndex,
children: _pages,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (index) {
setState(() => _currentIndex = index);
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: '首页',
),
NavigationDestination(
icon: Icon(Icons.favorite_outline),
selectedIcon: Icon(Icons.favorite),
label: '收藏',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}
pages/home_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/article.dart';
import '../providers/favorite_provider.dart';
import '../services/news_service.dart';
import 'detail_page.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late Future<List<Article>> _articlesFuture;
@override
void initState() {
super.initState();
_articlesFuture = NewsService.getArticles();
}
void _refresh() {
setState(() {
_articlesFuture = NewsService.getArticles();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('今日新闻'),
actions: [
IconButton(icon: const Icon(Icons.refresh), onPressed: _refresh),
],
),
body: FutureBuilder<List<Article>>(
future: _articlesFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.cloud_off, size: 64, color: Colors.grey),
const SizedBox(height: 16),
Text('${snapshot.error}'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _refresh,
child: const Text('重试'),
),
],
),
);
}
final articles = snapshot.data ?? [];
return RefreshIndicator( // 下拉刷新
onRefresh: () async => _refresh(),
child: ListView.separated(
itemCount: articles.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
return _ArticleCard(article: articles[index]);
},
),
);
},
),
);
}
}
class _ArticleCard extends StatelessWidget {
final Article article;
const _ArticleCard({required this.article});
@override
Widget build(BuildContext context) {
// 使用 context.select 只监听该文章的收藏状态,避免不必要的重建
final isFavorite = context.select<FavoriteProvider, bool>(
(p) => p.isFavorite(article.id),
);
return InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(article: article),
),
);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 文章图片
Hero(
tag: 'article-image-${article.id}',
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
article.imageUrl,
width: 80,
height: 80,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 80,
height: 80,
color: Colors.grey.shade200,
child: const Icon(Icons.image),
),
),
),
),
const SizedBox(width: 12),
// 文章信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
article.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
article.author,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
),
// 收藏按钮
IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : Colors.grey,
),
onPressed: () {
context.read<FavoriteProvider>().toggle(article);
},
),
],
),
),
);
}
}
pages/detail_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/article.dart';
import '../providers/favorite_provider.dart';
class DetailPage extends StatelessWidget {
final Article article;
const DetailPage({super.key, required this.article});
@override
Widget build(BuildContext context) {
final isFavorite = context.watch<FavoriteProvider>().isFavorite(article.id);
return Scaffold(
appBar: AppBar(
title: const Text('文章详情'),
actions: [
IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : null,
),
onPressed: () {
context.read<FavoriteProvider>().toggle(article);
},
),
],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 文章头图(Hero 动画)
Hero(
tag: 'article-image-${article.id}',
child: Image.network(
article.imageUrl,
width: double.infinity,
height: 220,
fit: BoxFit.cover,
),
),
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
article.title,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
height: 1.4,
),
),
const SizedBox(height: 8),
Text(
article.author,
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 14,
),
),
const Divider(height: 32),
Text(
article.body,
style: const TextStyle(fontSize: 16, height: 1.8),
),
],
),
),
],
),
),
);
}
}
pages/favorite_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/favorite_provider.dart';
import 'detail_page.dart';
class FavoritePage extends StatelessWidget {
const FavoritePage({super.key});
@override
Widget build(BuildContext context) {
final favorites = context.watch<FavoriteProvider>().favorites;
return Scaffold(
appBar: AppBar(title: const Text('我的收藏')),
body: favorites.isEmpty
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite_border, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text('还没有收藏文章', style: TextStyle(color: Colors.grey)),
],
),
)
: ListView.builder(
itemCount: favorites.length,
itemBuilder: (context, index) {
final article = favorites[index];
return ListTile(
leading: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.network(
article.imageUrl,
width: 56,
height: 56,
fit: BoxFit.cover,
),
),
title: Text(
article.title,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(article.author),
trailing: IconButton(
icon: const Icon(Icons.favorite, color: Colors.red),
onPressed: () {
context.read<FavoriteProvider>().toggle(article);
},
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailPage(article: article),
),
);
},
);
},
),
);
}
}
pages/profile_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/favorite_provider.dart';
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
final favoriteCount = context.watch<FavoriteProvider>().favorites.length;
return Scaffold(
appBar: AppBar(title: const Text('我的')),
body: Column(
children: [
// 用户信息卡片
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
color: Theme.of(context).colorScheme.primaryContainer,
child: Column(
children: [
const CircleAvatar(
radius: 40,
child: Icon(Icons.person, size: 48),
),
const SizedBox(height: 12),
const Text(
'读者用户',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text('已收藏 $favoriteCount 篇文章'),
],
),
),
const SizedBox(height: 16),
// 设置列表
ListTile(
leading: const Icon(Icons.notifications),
title: const Text('通知设置'),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () {},
),
ListTile(
leading: const Icon(Icons.dark_mode),
title: const Text('深色模式'),
trailing: Switch(value: false, onChanged: (_) {}),
),
ListTile(
leading: const Icon(Icons.info),
title: const Text('关于'),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: () {},
),
],
),
);
}
}
踩坑与注意事项
setState 在异步回调中的空安全问题
// 错误:组件已销毁后仍调用 setState
Future<void> _load() async {
final data = await fetchData();
setState(() => _data = data); // 组件可能已销毁,报错
}
// 正确:检查 mounted
Future<void> _load() async {
final data = await fetchData();
if (!mounted) return;
setState(() => _data = data);
}
避免在 build 中创建对象
// 错误:每次 build 都创建新的 Future,导致 FutureBuilder 死循环
Widget build(BuildContext context) {
return FutureBuilder(
future: fetchData(), // 每次 build 都是新的 Future
builder: (_, __) => ...,
);
}
// 正确:在 initState 中创建
late Future<Data> _future;
@override
void initState() {
super.initState();
_future = fetchData();
}
Widget build(BuildContext context) {
return FutureBuilder(future: _future, builder: (_, __) => ...);
}
TextEditingController 必须 dispose
@override
void dispose() {
_controller.dispose(); // 忘记 dispose 会内存泄漏
super.dispose();
}
ListView 在 Column 中的无界高度问题
// 错误:Column 中嵌套 ListView,高度无界报错
Column(
children: [
ListView(...), // 报错:unbounded height
],
)
// 正确方法 1:给 ListView 固定高度
Column(
children: [
SizedBox(
height: 300,
child: ListView(...),
),
],
)
// 正确方法 2:使用 shrinkWrap(性能较差,少量数据可用)
Column(
children: [
ListView(shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), ...),
],
)
// 正确方法 3:改用 CustomScrollView
CustomScrollView(
slivers: [
SliverToBoxAdapter(child: header),
SliverList(...),
],
)
图片缓存
Image.network 不会自动缓存,推荐使用 cached_network_image 包:
flutter pub add cached_network_image
import 'package:cached_network_image/cached_network_image.dart';
CachedNetworkImage(
imageUrl: 'https://example.com/image.jpg',
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
)
最佳实践
优先用 const 构造 Widget:能用 const 的地方一律加 const,Flutter 会跳过这些节点的重建,是最简单的性能优化。
// 好:const 标记,不会因父 Widget 重建而重建
const Text('Hello');
// 差:每次父 rebuild 都新建实例
Text('Hello');
StatelessWidget 优先,setState 范围最小化:将有状态逻辑提取到独立的小 Widget,避免整棵树因局部状态变化重绘。复杂应用使用 Riverpod 或 Bloc 管理状态。
图片使用 cached_network_image:网络图片不走缓存会反复下载,CachedNetworkImage 自动处理内存缓存和磁盘缓存,生产环境必备。
Navigator 2.0 场景用 go_router:直接操作 Navigator.push 在深链接和 Web URL 同步场景下难以维护,go_router(官方推荐)提供声明式路由和统一的深链接处理。详见 GoRouter完全指南。
异步操作用 FutureBuilder / StreamBuilder 显式处理加载态:不要直接在 initState 里赋值触发 setState,而是用 Builder 组件绑定 Future/Stream,让 UI 与数据状态同步。
FutureBuilder<User>(
future: fetchUser(id),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snapshot.hasError) return Text('Error: ${snapshot.error}');
return UserCard(user: snapshot.data!);
},
);
常见陷阱
陷阱:在 initState 中调用 context 相关方法
现象: 调用 Theme.of(context) 或 Navigator.of(context) 时报错 dependOnInheritedWidgetOfExactType called during initState。
原因: initState 执行时 Widget 尚未挂载到树上,context 没有可访问的上层 InheritedWidget。
解决: 将需要 context 的初始化代码移到 didChangeDependencies 或用 WidgetsBinding.instance.addPostFrameCallback。
@override
void didChangeDependencies() {
super.didChangeDependencies();
final theme = Theme.of(context); // 正确:此时 context 已可用
}
陷阱:setState 在 dispose 后调用
现象: 异步操作完成后调用 setState,控制台报 setState() called after dispose()。
原因: Widget 已销毁但回调仍持有对它的引用并尝试更新状态。
解决: 在回调前检查 mounted:
Future<void> loadData() async {
final result = await fetchData();
if (!mounted) return; // Widget 已销毁则跳过
setState(() => data = result);
}
陷阱:ListView 子项高度不固定导致滚动性能差
现象: 长列表滚动时掉帧,或报 RenderFlex children have non-zero flex but incoming height constraints are unbounded。
原因: 未指定 itemExtent 时 Flutter 需要布局每个 item 才能计算位置,大量 item 时 CPU 开销显著。
解决: 若 item 高度固定,设置 itemExtent;高度不一致时改用 ListView.builder 并配合 AutomaticKeepAliveClientMixin。