Flutter使用Provider执行pop时空检查运算符作用于空值报错
Flutter 登录页pop操作偶发空指针错误修复
原有实现代码
我有一个登录按钮,点击后会执行如下函数:
static Future loginPress(BuildContext context, TextEditingController emailController, TextEditingController passwordController) async { Loader(context); try { await MyUser.signIn(emailController.text.trim(), passwordController.text.trim()); Navigator.of(context).pop(); Navigator.of(context).pushReplacementNamed(Routes.authPage); } on CustomException catch (e) { Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message.toString())) ); } return null; }
上述代码中调用的Loader加载组件实现逻辑如下:
Future<dynamic> Loader(BuildContext context) { return showDialog( context: context, builder: (context){ return Center(child: CircularProgressIndicator(color: COLOR_PRIMARY),); } ); }
第一段代码中调用的MyUser.signIn方法实现如下:
static Future signIn(String email, String password) async { if(email.isEmpty || password.isEmpty) throw CustomException('Either email or password field is empty'); try { await FirebaseAuth.instance.signInWithEmailAndPassword(email: email, password: password); } on FirebaseAuthException catch (e) { print(e.message); throw CustomException('Wrong password or email'); } }
报错日志
_dirty:false _hadUnsatisfiedDependencies:false _inDirtyList:false _inheritedWidgets:null _lifecycleState:_ElementLifecycle (_ElementLifecycle.defunct) _owner:BuildOwner _parent:null _slot:null _state:null _widget:null debugDoingBuild:false debugIsDefunct:true depth:121 dirty:false hashCode:138 owner:BuildOwner renderObject:null runtimeType:Type (StatefulElement) size:<Cannot get size of inactive element. In order for an element to have a valid size, the element must be active, which means it is part of the tree. Instead, this element is in the _ElementLifecycle.defunct state. The size getter was called for the following element: StatefulElement#0008a(DEFUNCT) #0 Element.size.<anonymous closure> (package:flutter/src/widgets/framework.dart:4029:9) #1 Element.size (package:flutter/src/widgets/framework.dart:4062:6) #2 StatefulElement.Eval () #3 AuthViewModel.loginPress (package:better_health/view_model/auth_view_model.dart:49:17) <asynchronous suspension>> slot:null state:<Null check operator used on a null value #0 StatefulElement.state (package:flutter/src/widgets/framework.dart:4877:44) #1 StatefulElement.Eval () #2 AuthViewModel.loginPress (package:better_health/view_model/auth_view_model.dart:49:17) <asynchronous suspension>> widget:<Null check operator used on a null value
问题表现
在上述第一段代码中执行Navigator.of(context).pop()操作时,会偶发*"Null check operator used on a null value"*错误,该问题仅在部分场景下出现,其余场景功能可正常运行。
问题根因
这个错误的核心原因是异步操作执行期间,传入的BuildContext已经失效:
- 调用
Loader(context)弹出加载弹窗后,代码会执行await MyUser.signIn(...)等待登录请求返回,这个等待过程如果出现用户手动按返回键、页面因其他逻辑被销毁(组件unmount、路由被提前弹出)的情况,原来传入的context对应的Element就会进入defunct(失效)状态,和组件树完全解绑。 - 这时候再调用
Navigator.of(context)获取导航器实例会得到空值,后续执行pop、pushReplacementNamed等操作时就会触发空指针报错。 - 问题偶发的原因很明确:只有登录请求等待时间较长(比如弱网环境)、用户有时间在请求返回前操作关闭当前页面时才会触发;网络状态好、请求返回快的时候用户来不及操作,功能就表现正常。
修复方案
- 第一步:所有await异步操作执行完成后,调用context相关API前必须先校验当前组件是否仍处于挂载状态,禁止对失效context执行操作。
- 第二步:加载弹窗默认设置为不可手动关闭,避免用户在请求过程中点击返回/遮罩层关闭页面,从场景上减少context提前失效的概率。
- 第三步:修改对应代码实现:
首先修改登录方法的逻辑:
static Future loginPress(BuildContext context, TextEditingController emailController, TextEditingController passwordController) async { // 进入方法先校验context有效性 if (!context.mounted) return; // 弹出加载框,禁止加载期间通过返回键/点击遮罩关闭 Loader(context, barrierDismissible: false); try { await MyUser.signIn(emailController.text.trim(), passwordController.text.trim()); // 异步请求返回后,再次校验context是否有效 if (!context.mounted) return; // 关闭加载弹窗 Navigator.of(context).pop(); // 路由跳转前第三次校验 if (!context.mounted) return; Navigator.of(context).pushReplacementNamed(Routes.authPage); } on CustomException catch (e) { // 异常分支同样先校验context有效性 if (!context.mounted) return; Navigator.of(context).pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.message.toString())) ); } return null; }
同步修改Loader组件,支持控制弹窗是否可手动关闭:
Future<dynamic> Loader(BuildContext context, {bool barrierDismissible = true}) { return showDialog( context: context, barrierDismissible: barrierDismissible, builder: (context){ return const Center(child: CircularProgressIndicator(color: COLOR_PRIMARY),); } ); }
注意:
context.mounted是Flutter 3.7及以上版本提供的内置校验属性,如果你的项目使用的Flutter版本低于3.7,在State类中可以直接调用内置的mounted属性判断状态,不要在跨异步的场景中不加校验直接使用context。
内容的提问来源于stack exchange,提问作者FiFi
相关产品推荐
相关产品推荐

