You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter报setState() called after dispose()错误如何修复

问题现象

调用setState修改变量时抛出如下异常:

Unhandled Exception: setState() called after dispose(): _SplashState#27d6e(lifecycle state: defunct, not mounted, ticker inactive)
E/flutter (12289): This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback.
E/flutter (12289): The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.
E/flutter (12289): This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this State object during dispose().

该异常直接导致屏幕无法正常接收通知推送。
业务逻辑是在initState生命周期里实现通知监听、Firebase Token获取、SharedPreference数据写入,要把通知携带的参数存到变量里,实现点击通知跳转页面时能读到更新后的值,对应的业务代码如下:

class Splash extends StatefulWidget {
  const Splash({Key? key}) : super(key: key);
  @override
  _SplashState createState() => _SplashState();
}

class _SplashState extends State<Splash> with SingleTickerProviderStateMixin {
  late AnimationController controller;
  late Animation heartbeatAnimation;

@override
  void initState() {
    super.initState();
 controller = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 500));
    heartbeatAnimation =
        Tween<double>(begin: 100.0, end: 250.0).animate(controller);
    controller.forward().whenComplete(() {
      controller.reverse();
    });


    Future.delayed(const Duration(seconds: 3), () async {
      FirebaseMessaging.onMessage.listen((RemoteMessage message) {
        print("onMessage Clicked!");
       
        String getId = message.data["id"];
        print("ride id get:    ${message.data["id"]}");
        if (mounted) {     
          setState(() {
            bookedRideId = int.parse(getId);   //this line is not working, error points here
          });
        }
        RemoteNotification? notification = message.notification;
        AndroidNotification? android = message.notification?.android;
        if (notification != null && android != null) {
          flutterLocalNotificationsPlugin.show(
              notification.hashCode,
              notification.title,
              notification.body,
              NotificationDetails(
                android: AndroidNotificationDetails(
                  channel.id,
                  channel.name,
                  icon: android.smallIcon,
                ),
              ));
        }
      });
    
      getToken();

      FirebaseMessaging.onBackgroundMessage(
          _firebaseMessagingBackgroundHandler);

      // assign channel (required after android 8)
      await flutterLocalNotificationsPlugin
          .resolvePlatformSpecificImplementation<
              AndroidFlutterLocalNotificationsPlugin>()
          ?.createNotificationChannel(channel);

      // initialize notification for android
      var initialzationSettingsAndroid =
          AndroidInitializationSettings('@mipmap/ic_launcher');
      var initializationSettings =
          InitializationSettings(android: initialzationSettingsAndroid);
      flutterLocalNotificationsPlugin.initialize(initializationSettings);

      final NotificationAppLaunchDetails? notificationAppLaunchDetails =
          await flutterLocalNotificationsPlugin
              .getNotificationAppLaunchDetails();

      
      payload = notificationAppLaunchDetails!.payload;
      if (payload != null) {
        routeToGo = '/second';
        navigatorKey.currentState?.pushNamed('/second');
      }

      await flutterLocalNotificationsPlugin.initialize(initializationSettings,
          onSelectNotification: selectNotification);

      FirebaseMessaging.onMessageOpenedApp
          .listen((RemoteMessage message) async {
        print(message.notification!.body != null);
        if (message.notification!.body != null) {
          print("=======================rideid=====================");
          print(rideId);
          navigatorKey.currentState?.pushNamed('/second');
        }
      });

     

      SharedPreferences prefs = await SharedPreferences.getInstance();
      
      emailAddress = prefs.getString('email').toString();
      token = prefs.getString('token').toString();
      emailAddress == "null" && token == "null"
          ? Navigator.of(context).pushAndRemoveUntil(
              MaterialPageRoute(builder: (context) => const PreLogin()),
              (route) => false)
          : role == "driver"
              ? Navigator.of(context).pushAndRemoveUntil(
                  MaterialPageRoute(builder: (context) => DriverHome()),
                  (route) => false)
              : Navigator.of(context).pushAndRemoveUntil(
                  MaterialPageRoute(builder: (context) => const UserHome()),
                  (route) => false);
    });

   
  }

  @override
  void dispose() {
    controller.dispose();

    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: Scaffold(
          resizeToAvoidBottomInset: false,
          body: Container(
            decoration: const BoxDecoration(
              image: DecorationImage(
                image: AssetImage(splashBgImage),
                fit: BoxFit.fill,
              ),
            ),
            child: const Logo(),
          )),
    );
  }
}
问题根因
  • 启动页Splash本身是过渡页面,3秒延迟后会自动执行路由跳转,跳转后Splash组件就被从组件树移除,对应的State状态直接进入disposed销毁状态。
  • 你把Firebase消息监听、通知点击监听全注册在了Splash页的initState里,注册后没有保存订阅引用、也没在页面销毁时取消订阅。Splash页销毁后,这些监听回调还在后台运行,一旦收到新消息触发回调,哪怕你写了mounted判断,也存在时序问题:判断完mounted为true到执行setState的间隙,页面刚好被销毁跳转,就会触发这个报错。
  • 额外逻辑缺陷:全局的通知监听逻辑不应该放在启动页这种短生命周期页面里,启动页销毁后监听虽然还在运行,但你存在Splash页State里的变量(比如bookedRideId)会跟着State一起被回收,后续跳转页面根本拿不到存在这个变量里的值,完全达不到跨页面传参的目的。
修复方案
  • 把全局通知相关逻辑从Splash页挪出去:Firebase消息监听、通知插件初始化、Token获取这类全局生命周期的逻辑,放到应用根级别的位置(比如MyApp的State、或者全局的通知服务类里),不要放在启动页这种临时页面里。通知携带的参数不要存在Splash页的局部变量里,统一存到SharedPreference或者全局状态管理中,跳转页面时直接从全局存储读值,就不会出现页面销毁值丢失的问题。
  • 所有流订阅必须手动取消:不管把监听逻辑放在哪个页面,FirebaseMessaging.onMessage.listen、FirebaseMessaging.onMessageOpenedApp.listen这两个方法返回的StreamSubscription对象一定要存成变量,在dispose生命周期里调用cancel()方法取消订阅,避免页面销毁后回调还在触发。
  • 补全安全校验:所有调用setState的位置,不要在判断完mounted后隔了大段异步逻辑再调用setState,异步任务结束后要先再校验一次mounted再操作UI。
  • 修正现有代码的时序问题:你现在的逻辑是先注册消息监听,再执行3秒后的跳转,相当于监听刚注册完,页面马上就要跳走,必然会出现监听还在、页面已经销毁的情况。

核心修复代码示例

首先在State里增加订阅变量存储:

// 新增两个订阅实例存储
StreamSubscription<RemoteMessage>? _messageSub;
StreamSubscription<RemoteMessage>? _messageOpenAppSub;

注册监听时保存返回的订阅对象,同时提前校验挂载状态:

// 替换原有FirebaseMessaging.onMessage.listen逻辑
_messageSub = FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  print("onMessage Clicked!");
  String getId = message.data["id"];
  print("ride id get:    ${message.data["id"]}");
  // 提前拦截,未挂载直接返回
  if (!mounted) return;
  setState(() {
    bookedRideId = int.parse(getId);
  });
  // 剩余通知展示逻辑保持不变
  RemoteNotification? notification = message.notification;
  AndroidNotification? android = message.notification?.android;
  if (notification != null && android != null) {
    flutterLocalNotificationsPlugin.show(
        notification.hashCode,
        notification.title,
        notification.body,
        NotificationDetails(
          android: AndroidNotificationDetails(
            channel.id,
            channel.name,
            icon: android.smallIcon,
          ),
        ));
  }
});

// 替换原有FirebaseMessaging.onMessageOpenedApp.listen逻辑
_messageOpenAppSub = FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) async {
  if (!mounted) return;
  print(message.notification!.body != null);
  if (message.notification!.body != null) {
    print("=======================rideid=====================");
    print(rideId);
    navigatorKey.currentState?.pushNamed('/second');
  }
});

重写dispose方法,释放所有资源:

@override
void dispose() {
  controller.dispose();
  // 取消所有活跃订阅
  _messageSub?.cancel();
  _messageOpenAppSub?.cancel();
  super.dispose();
}

注意:以上只是临时解决报错的方案,长期必须把全局通知逻辑从启动页迁出,不然就算不报错,存在启动页里的bookedRideId在页面跳转后也会被回收,后续页面无法读取到正确值。

内容的提问来源于stack exchange,提问作者TimeToCode

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 05:57:13