踩坑 · Flutter 运行时
分类:踩坑记录 → 运行时 覆盖:白屏、布局溢出、状态错误、崩溃、内存
setState() called after dispose()
症状
setState() called after dispose(): _XxxPageState#12345(lifecycle state: defunct, not mounted)原因 异步任务(网络/定时器)完成时页面已销毁,仍调用了 setState。
解法
// 方案一:回调里判 mounted
final data = await api.fetch();
if (!mounted) return; // ← 关键
setState(() { _data = data; });
// 方案二(Riverpod 场景):用 autoDispose,页面销毁自动取消
final dataProvider = FutureProvider.autoDispose((ref) async { ... });预防 所有 await 之后的 setState 前都加 if (!mounted) return;;异步一律走 Riverpod 的 autoDispose,见 08 篇。
RenderFlex overflowed by XX pixels
症状 底部出现黄黑条纹,控制台报 A RenderFlex overflowed by 47 pixels on the bottom。
原因Row/Column 的子组件总尺寸超出父约束。
解法 按场景选:
// ① 内容可压缩 → 用 Expanded / Flexible 包住
Column(children: [
const Header(),
Expanded(child: ListView(...)), // ← 让列表占剩余空间
])
// ② 内容不可压缩 → 允许滚动
SingleChildScrollView(child: Column(children: [...]))
// ③ 文字/内容过长 → 允许截断
Text(longText, maxLines: 2, overflow: TextOverflow.ellipsis)预防 Column 里放列表/长内容时第一反应就是加 Expanded。布局基础见 02 篇。
Vertical viewport was given unbounded height
症状ListView 放在 Column 中报此错,列表不显示。
原因ListView 需要明确的高度约束,Column 给的是无界约束。
解法
// ✅ 推荐:Expanded 给确定约束
Column(children: [Expanded(child: ListView(...))])
// ✅ 或限制高度
SizedBox(height: 300, child: ListView(...))
// ⚠️ 能不用就不用:shrinkWrap(会让列表一次性构建全部子项,长列表性能差)
ListView(shrinkWrap: true, ...)预防shrinkWrap: true 只用于确定很短的列表(如 3~5 项的设置项)。
页面白屏(无任何报错)
症状 页面全白,控制台无异常输出。
原因(按概率)
- 父级给了零尺寸约束(如
SizedBox(height: 0)包内容) build返回了空内容(条件分支走到了空分支)- 数据为空且没做空态处理
- Scaffold 嵌套在了无约束容器里
排查步骤
// ① 给可疑 Widget 加背景色,看它到底占多大地方
Container(color: Colors.red, child: suspectWidget)
// ② 用 Flutter Inspector 的 "Select Widget Mode" 点选看边界
// ③ 检查条件分支
return data.isEmpty ? const SizedBox() : Content(data); // ← 空态没处理预防 空态/加载态/错误态三态都要有 UI,不要返回空容器。
Looking up a deactivated widget's ancestor
症状await 之后使用 context(如 context.read、Navigator.of(context))报此错。
原因 异步期间页面被卸载,context 已失效。
解法
// ❌ 错:await 后用旧 context
await doSomething();
context.read(provider); // 可能已失效
// ✅ 方案一:await 前先取出需要的东西
final notifier = context.read(myProvider.notifier); // 先取
await doSomething();
notifier.refresh(); // 后调用
// ✅ 方案二:用全局 navigatorKey(跳转场景)
navigatorKey.currentState?.pushNamed('/detail');预防await 之后不要再用 context——这是 Flutter 的铁律。
内存持续上涨 / 页面销毁后仍在运行
症状 反复进出某页面后内存持续增长;已退出的页面仍在打印日志。
原因(四大高发点)
AnimationController/TextEditingController未disposeStreamSubscription未cancelTimer未cancel- 全局单例持有页面引用或
BuildContext
排查
// 用 DevTools Memory:进出页面 10 次 → 手动 GC → 观察堆是否回落
// 详见 22 篇性能分析解法
late final TextEditingController _ctrl;
late final StreamSubscription _sub;
Timer? _timer;
@override
void initState() {
super.initState();
_ctrl = TextEditingController();
_sub = stream.listen(_onData);
}
@override
void dispose() {
_timer?.cancel();
_sub.cancel();
_ctrl.dispose();
super.dispose(); // 写在最后
}预防 写了 initState 就必须写 dispose;用 Riverpod 的 autoDispose 让框架管理生命周期。见 05 篇 dispose 泄漏专题。
列表滚动卡顿
症状 长列表滚动掉帧,尤其快速滑动时。
原因(按概率)
- 用了
ListView(children: [...])一次性构建全部子项 - 列表项重建范围过大
- 图片未指定解码尺寸,原图占内存
- 在
build里做了耗时计算
解法
// ① 用 builder 懒加载
ListView.builder(itemCount: n, itemBuilder: (ctx, i) => Tile(data: items[i]))
// ② 能定高就定高(滚动位置计算 O(1))
ListView.builder(itemExtent: 72, ...)
// ③ 图片按显示尺寸解码
Image.network(url, cacheWidth: 300, cacheHeight: 300)
// ④ 列表项抽成 const 构造的独立 Widget预防 见 22 篇性能分析,用 profile 模式 + DevTools 实测,不要凭感觉优化。
Null check operator used on a null value
症状 使用 ! 的地方崩溃。
原因 对 null 值用了 ! 强制解包。常见于异步数据未到就渲染、或接口字段缺失。
解法
// ❌ 危险
Text(data!.name)
// ✅ 安全
Text(data?.name ?? '--')
// ✅ 或用模式匹配
if (data case final d?) { Text(d.name) }预防代码规范里禁用 !(除非有明确断言),改为 ?. + ?? 默认值。这与 01 篇空安全一致。
待记录
- [ ] 键盘弹出遮挡输入框
- [ ] 热重载后状态异常(需 Full Restart)
- [ ] 深色模式切换后颜色未更新
- [ ] 列表下拉刷新与上拉加载冲突