Skip to content

踩坑 · Flutter 运行时 ​

分类:踩坑记录 → 运行时 覆盖:白屏、布局溢出、状态错误、崩溃、内存


setState() called after dispose() ​

症状

setState() called after dispose(): _XxxPageState#12345(lifecycle state: defunct, not mounted)

原因 异步任务(网络/定时器)完成时页面已销毁,仍调用了 setState。

解法

dart
// 方案一:回调里判 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 的子组件总尺寸超出父约束。

解法 按场景选:

dart
// ① 内容可压缩 → 用 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 给的是无界约束。

解法

dart
// ✅ 推荐:Expanded 给确定约束
Column(children: [Expanded(child: ListView(...))])

// ✅ 或限制高度
SizedBox(height: 300, child: ListView(...))

// ⚠️ 能不用就不用:shrinkWrap(会让列表一次性构建全部子项,长列表性能差)
ListView(shrinkWrap: true, ...)

预防shrinkWrap: true 只用于确定很短的列表(如 3~5 项的设置项)。


页面白屏(无任何报错) ​

症状 页面全白,控制台无异常输出。

原因(按概率)

  1. 父级给了零尺寸约束(如 SizedBox(height: 0) 包内容)
  2. build 返回了空内容(条件分支走到了空分支)
  3. 数据为空且没做空态处理
  4. Scaffold 嵌套在了无约束容器里

排查步骤

dart
// ① 给可疑 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 已失效。

解法

dart
// ❌ 错: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 的铁律。


内存持续上涨 / 页面销毁后仍在运行 ​

症状 反复进出某页面后内存持续增长;已退出的页面仍在打印日志。

原因(四大高发点)

  1. AnimationController / TextEditingController 未 dispose
  2. StreamSubscription 未 cancel
  3. Timer 未 cancel
  4. 全局单例持有页面引用或 BuildContext

排查

dart
// 用 DevTools Memory:进出页面 10 次 → 手动 GC → 观察堆是否回落
// 详见 22 篇性能分析

解法

dart
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 泄漏专题。


列表滚动卡顿 ​

症状 长列表滚动掉帧,尤其快速滑动时。

原因(按概率)

  1. 用了 ListView(children: [...]) 一次性构建全部子项
  2. 列表项重建范围过大
  3. 图片未指定解码尺寸,原图占内存
  4. 在 build 里做了耗时计算

解法

dart
// ① 用 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 值用了 ! 强制解包。常见于异步数据未到就渲染、或接口字段缺失。

解法

dart
// ❌ 危险
Text(data!.name)

// ✅ 安全
Text(data?.name ?? '--')

// ✅ 或用模式匹配
if (data case final d?) { Text(d.name) }

预防代码规范里禁用 !(除非有明确断言),改为 ?. + ?? 默认值。这与 01 篇空安全一致。


待记录 ​

  • [ ] 键盘弹出遮挡输入框
  • [ ] 热重载后状态异常(需 Full Restart)
  • [ ] 深色模式切换后颜色未更新
  • [ ] 列表下拉刷新与上拉加载冲突