skip to Main Content

I have app with a simple login system based in named routes and Navigator. When login is successful, the loggin route si pop from stack and the first route (home) is pushed, using Navigator.popAndPushNamed,'/first'). When the user is logged the routes of the app (except from login route) are correctly push and pop from stack to allow a smooth navigation. When the user decides to log out, all routes are removed from stack and the login route is pushed, using Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false). All of that is working fine, but the problem is the user logs again, because the first route (a statefulwidget) is being associated with its previous State which was previously disposed, so the mounted property is false. That’s generating that the State properties not being correctly initialized and the error "setState() calls after dispose()" is being shown.
It’s an example of the login system based in named routes and Navigator that I’m using in my app.

import 'dart:async';
import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'Named Routes Demo',
      initialRoute: '/',
      routes: {
        '/': (context) => const LoginScreen(),
        '/first': (context) => FirstScreen(),
        '/second': (context) => const SecondScreen(),
      },
    ),
  );
}

class LoginScreen extends StatelessWidget {
  const LoginScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Loggin screen'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Navigator.popAndPushNamed(context, '/first'),
          child: const Text('Launch app'),
        ),
      ),
    );
  }
}

class FirstScreen extends StatefulWidget {
  const FirstScreen({super.key});
  FirstState createState() => FirstState();
}

class FirstState extends State<FirstScreen> {
  int cont;
  Timer? t;
  final String a;

  FirstState() : cont = 0, a='a' {
    debugPrint("Creando estado de First screen");
  }

  @override
  void initState() {
    super.initState();
    debugPrint("Inicializando estado de First Screen");
    cont = 10;
    t = Timer.periodic(Duration(seconds: 1), (timer) => setState(() => cont++));
  }

  @override
  void dispose() {
    debugPrint("Eliminando estado de First Screen");
    cont = 0;
    t?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('First Screen'),
      ),
      body: Center(child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text("Contador: $cont"),
            SizedBox(height: 50,),
            ElevatedButton(
              // Within the `FirstScreen` widget
              onPressed: () {
                // Navigate to the second screen using a named route.
                Navigator.pushNamed(context, '/second');
              },
              child: const Text('Go to second screen'),
            ),
          ]
        )
      ),
    );
  }
}

class SecondScreen extends StatelessWidget {
  const SecondScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Second Screen'),
      ),
      body: Center(child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                // Within the SecondScreen widget
                onPressed: () {
                  // Navigate back to the first screen by popping the current route
                  // off the stack.
                  Navigator.pop(context);
                },
                child: const Text('Go back'),
              ),
              ElevatedButton(
                // Within the SecondScreen widget
                onPressed: () {
                  // Navigate back to the first screen by popping the current route
                  // off the stack.
                  Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false);
                },
                child: const Text('Logout'),
              )
            ]
        )
      )
    );
  }
}

However, the example is not showing the described error, so I’m suspecting the cause could be an uncaught exception during the state dispose.
I’m using Flutter 3.7.12 (Dart 2.19.6). I’ve not updated to avoid to restructure code to be compatible with Dart 3 (null safety). Another detail is that the error appears sometimes and mainly in Android.

2

Answers


  1. Chosen as BEST ANSWER

    The problem was that I was not closing the subscriptions to the Firebase Cloud Messaging (FCM) streams. Thanks to this post I could discover that State objects remain alive after they are disposed, so if you leave active a timer, stream subscription or sth like that, it will continue executing and generating results. So it is very important to close or cancel that kind of State properties. With respect to FCM stream subscriptions, they should be handled as following:

    class _AppState extends State<_App> {
    
        @override
        void initState() {
            ...
            FirebaseMessaging.instance.getInitialMessage().then(handleInteraction);
            _suscrStreamFCMAppBackgnd = FirebaseMessaging.onMessageOpenedApp.listen(handleInteraction);
            FirebaseMessaging.onBackgroundMessage(procesarNotificacion);
            _suscrStreamFCMAppForegnd = FirebaseMessaging.onMessage.listen(_procesarNotificacionAppPrimerPlano);
            for (final topic in TOPICS_FIREBASE) {
              FirebaseMessaging.instance.subscribeToTopic(topic);
            }
            ...
        }
        
        @override
        void dispose() {
            ...
            _suscrStreamFCMAppBackgnd?.cancel();
            _suscrStreamFCMAppForegnd?.cancel();
            ...
        }
    }
    

    So, the old State (unmounted) hadn't been reassociated to the StatefulWidget object, but it remained alive and executing code in the background beacuse of the stream subscriptions.


  2. You are probably using setState in some async method and with some unlucky timing your setState call happens after some delay while the state object is already disposed.

    You can use the mounted flag inside of your async method to check if the state is still mounted instead of manually cancelling all futures, etc inside of dispose.

    Without seeing your concrete code, it’s tough to say exactly when and where your error is happening.

    Regarding your example, there is nothing wrong with it and flutter will never associate a widget with a dismounted state object.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search