Flutter延迟调用Navigator.pushReplacement报空值检查运算符异常
异常信息
延迟执行路由替换跳转时,控制台抛出如下错误:
[ERROR:flutter/lib/ui/ui_dart_state.cc(198)] Unhandled Exception: Null check operator used on a null value E/flutter ( 8437): #0 StatefulElement.state (package:flutter/src/widgets/framework.dart:4926:44) E/flutter ( 8437): #1 Navigator.of (package:flutter/src/widgets/navigator.dart:2542:47) E/flutter ( 8437): #2 Navigator.pushReplacement (package:flutter/src/widgets/navigator.dart:2103:22)
复现场景
- 基础触发逻辑:通过Timer延迟3秒执行
Navigator.pushReplacement跳转,代码如下:
Timer(Duration(seconds: 3), () { Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => MenuScreen())); });
- 跳转目标页MenuScreen代码:
class MenuScreen extends StatefulWidget { const MenuScreen({Key? key}) : super(key: key); @override State<MenuScreen> createState() => _MenuScreenState(); } class _MenuScreenState extends State<MenuScreen> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('My Sessions'),), body: Container(), ); } }
- 测试发现两个特殊现象:
- 相同跳转逻辑绑定在按钮点击事件中触发时,无任何报错
- 将
pushReplacement替换为Navigator.push方法时,不会触发该异常
- 问题对应的启动页SplashScreen完整代码:
class SplashScreen extends StatefulWidget { const SplashScreen({Key? key}) : super(key: key); @override State<SplashScreen> createState() => _SplashScreenState(); } class _SplashScreenState extends State<SplashScreen> { String token = ''; @override void initState() { SharedPreferences.getInstance().then((pref) { String token = pref.getString('token') ?? ''; setState(() { this.token = token; }); }); super.initState(); } @override Widget build(BuildContext context) { Timer(Duration(seconds: 3), () { Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => MenuScreen())); }); return Scaffold( backgroundColor: const Color(0xff193C68), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( 'assets/strivright_logo.png', width: MediaQuery.of(context).size.width * 0.75), SizedBox(height: MediaQuery.of(context).size.height * 0.2), const CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(Colors.white)), ], ), ), ); } }
问题原因
异常由两个问题叠加导致:
- 定时器位置错误:跳转用的Timer写在了
build方法内部。Flutter的build方法会在组件首次构建、setState触发更新、父组件重绘等多个时机反复执行,每执行一次build就会新建一个3秒延迟的Timer,最终会生成多个排队的跳转任务。 - 路由方法特性与失效context问题:
pushReplacement执行后会将当前SplashScreen路由从导航栈中移除,对应的组件Element会被从组件树卸载,持有的context失效。当第一个Timer执行完跳转后,后续排队的Timer再触发时,传入的context已经是被卸载的无效状态,Navigator.of(context)内部尝试通过context查找挂载的NavigatorState时拿到null,触发空检查操作符异常。
两个特殊现象的原因:
- 按钮点击无报错:点击事件触发时context处于正常挂载状态,且不会重复创建多个跳转任务,执行时context有效。
- 换成
push无报错:push是在当前路由上方压入新路由,原SplashScreen路由仍保留在导航栈中,即使后续Timer重复执行,context对应的Element未被完全销毁,不会触发空值异常(但仍会重复压入多个MenuScreen页面,属于隐藏逻辑bug)。
解决方案
- 将Timer跳转逻辑从
build方法移到initState生命周期中,保证SplashScreen整个生命周期内只会创建一次定时器,避免重复生成跳转任务。 - 执行跳转前增加
mounted状态判断,组件已销毁时不执行跳转逻辑,避免使用失效context。 - 在
dispose生命周期中取消未执行的定时器,避免内存泄漏和无效任务残留。
修正后的SplashScreen代码如下:
class _SplashScreenState extends State<SplashScreen> { String token = ''; Timer? _navigateTimer; @override void initState() { super.initState(); // 初始化读取本地存储token SharedPreferences.getInstance().then((pref) { token = pref.getString('token') ?? ''; if (mounted) { setState(() { this.token = token; }); } }); // 定时器仅初始化一次 _navigateTimer = Timer(const Duration(seconds: 3), () { // 校验组件挂载状态再执行跳转 if (mounted) { Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const MenuScreen()) ); } }); } @override void dispose() { // 组件销毁时取消定时器 _navigateTimer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xff193C68), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( 'assets/strivright_logo.png', width: MediaQuery.of(context).size.width * 0.75), SizedBox(height: MediaQuery.of(context).size.height * 0.2), const CircularProgressIndicator(valueColor: AlwaysStoppedAnimation<Color>(Colors.white)), ], ), ), ); } }
内容的提问来源于stack exchange,提问作者RJB
相关产品推荐
相关产品推荐

