Flutter/Dart: setState() called after dispose() error called when the user re-open app

Viewed 15

I made a simple app where I implement Firebase Authentication and whenever the user exit the app and re-open the app the the StreamBuilder check if the authState has data. If it has the user re-direct to homepage but if not then the Login page is shown

The issue is when ever the user redirect to homeScreen after closing and re-opening the app the setState() called after dispose() error shown in the console with stack trace.

Here is my LoginScreen that is shown in stack trace when the exception was thrown:

class LoginScreen extends StatefulWidget {
  static const rout = '/login-screen';
  const LoginScreen({super.key});

  @override
  State<LoginScreen> createState() => _LoginScreenState();
}

class _LoginScreenState extends State<LoginScreen> {
  final Map<String, String> _loginData = {'email': '', 'password': ''};
  GlobalKey topWidgetKey = GlobalKey();
  GlobalKey bottomWidgetKey = GlobalKey();
  double topWidgetHeight = 0.0;
  double bottomWidgetHeight = 0.0;
  double spacer = 0.0;
  var isLoading = false;
  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      setState(() {
        final topWidgetKeyContextt = topWidgetKey.currentContext;
        if (topWidgetKeyContextt != null) {
          final box = topWidgetKeyContextt.findRenderObject() as RenderBox;
          topWidgetHeight = box.size.height;
        }
        final bottomWidgetKeyContextt = bottomWidgetKey.currentContext;
        if (bottomWidgetKeyContextt != null) {
          final box = bottomWidgetKeyContextt.findRenderObject() as RenderBox;
          bottomWidgetHeight = box.size.height;
        }
        spacer = MediaQuery.of(context).size.height -
            AppBar().preferredSize.height -
            topWidgetHeight -
            bottomWidgetHeight -
            MediaQuery.of(context).viewPadding.top -
            MediaQuery.of(context).viewPadding.bottom;
      });
    });
    super.initState();
  }

  final _emailController = TextEditingController();
  final _form = GlobalKey<FormState>();
  final _passwordController = TextEditingController();
  var visible = true;
  final emailVerificationSyntax = RegExp(
      r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+");

  void _errorDialog(String message) {
    showDialog(
      context: context,
      builder: ((ctx) => AlertDialog(
            title: const Text("An Error Accourd"),
            content: Text(message),
            actions: <Widget>[
              TextButton(
                  onPressed: () {
                    Navigator.of(ctx).pop();
                  },
                  child: const Text("Okay"))
            ],
          )),
    );
  }

  void push() {
    Navigator.pushAndRemoveUntil(
        context,
        MaterialPageRoute(
          builder: (_) => const MyApp(),
        ),
        (route) => false);
  }

  Future<void> _saveData() async {
    final isValid = _form.currentState!.validate();
    if (!isValid) {
      return;
    }
    _form.currentState!.save();
    setState(() {
      isLoading = true;
    });

    try {
      await Login().signInUser(_loginData['email'].toString(),
          _loginData['password'].toString(), context);
      push();
    } catch (e) {
      var errorMessage = 'Authentication Failed';
      if (e.toString().contains('INVALID_EMAIL')) {
        errorMessage = 'The email adress is not valid';
      } else if (e.toString().contains('EMAIL_NOT_FOUND')) {
        errorMessage = 'No User found with this Email';
      } else if (e.toString().contains('INVALID_PASSWORD')) {
        errorMessage = 'Invalid Password';
      }
      _errorDialog(errorMessage);
    }
    setState(() {
      isLoading = false;
    });
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color.fromRGBO(255, 243, 18, 3),
      body: SingleChildScrollView(
        child: Form(
          key: _form,
          child: Column(children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(top: 90),
              child: Image.asset('assets/images/Logo.png'),
            ),
            const SizedBox(
              height: 10,
            ),
            const SafeArea(
              child: Padding(
                padding: EdgeInsets.only(left: 3, right: 250, top: 10),
                child: Text(
                  "Login",
                  style: TextStyle(
                      color: Colors.black,
                      fontWeight: FontWeight.bold,
                      fontSize: 40),
                  //  textAlign: TextAlign.center,
                ),
              ),
            ),
            const Padding(
              padding: EdgeInsets.only(left: 20, right: 200, top: 3),
              child: Text(
                "Please Login to continue",
                style: TextStyle(
                    color: Colors.black,
                    fontWeight: FontWeight.normal,
                    fontSize: 15),
                //  textAlign: TextAlign.center,
              ),
            ),
            const SizedBox(
              height: 15,
            ),
            Card(
              shape: const RoundedRectangleBorder(
                borderRadius: BorderRadius.all(
                  Radius.circular(20),
                ),
              ),
              shadowColor: Colors.grey,
              elevation: 10,
              child: TextFormField(
                controller: _emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: const InputDecoration(
                  labelText: 'Email-Address',
                  prefixIcon: Icon(
                    (Icons.person),
                  ),
                  border: OutlineInputBorder(borderSide: BorderSide.none),
                ),
                onSaved: (newValue) {
                  _loginData['email'] = newValue!;
                },
                validator: (value) {
                  if (!emailVerificationSyntax.hasMatch(value as String)) {
                    return "Incorrect Email-Adress Syntax";
                  }
                  if (value.isEmpty) {
                    return 'Please Enter Your Email Adress';
                  }
                  return null;
                },
              ),
            ),
            const SizedBox(
              height: 15,
            ),
            Card(
              shape: const RoundedRectangleBorder(
                borderRadius: BorderRadius.all(
                  Radius.circular(20),
                ),
              ),
              shadowColor: Colors.grey,
              elevation: 10,
              child: TextFormField(
                controller: _passwordController,
                obscureText: visible,
                keyboardType: TextInputType.streetAddress,
                decoration: InputDecoration(
                  labelText: 'Password',
                  prefixIcon: const Icon(
                    (Icons.password),
                  ),
                  suffixIcon: IconButton(
                      onPressed: () {
                        setState(() {
                          visible = !visible;
                        });
                      },
                      icon: const Icon(Icons.visibility)),
                  border: const OutlineInputBorder(borderSide: BorderSide.none),
                ),
                validator: (value) {
                  if (value!.isEmpty) {
                    return "Please Enter Your Passowrd";
                  }
                  return null;
                },
                onSaved: (newValue) {
                  _loginData['password'] = newValue!;
                },
              ),
            ),
            const SizedBox(
              height: 15,
            ),
            isLoading
                ? const CircularProgressIndicator()
                : ElevatedButton(
                    onPressed: () => _saveData(),
                    style: ElevatedButton.styleFrom(
                        //  backgroundColor: Color.fromARGB(255, 246, 214, 4)),
                        backgroundColor: Colors.white),
                    child: const Text(
                      'Login',
                      style: TextStyle(
                        color: Colors.black,
                        fontSize: 30,
                        fontWeight: FontWeight.normal,
                      ),
                    ),
                  ),
            const SizedBox(
              height: 100,
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Padding(
                  padding: EdgeInsets.only(bottom: 20),
                  child: Text(
                    "Don't have an account?",
                    style: TextStyle(color: Colors.black, fontSize: 18),
                  ),
                ),
                Padding(
                  padding: const EdgeInsets.only(bottom: 20),
                  child: TextButton(
                      onPressed: () {
                        Navigator.of(context)
                            .pushReplacementNamed(SignUpScreen.signUpRout);
                      },
                      child: const Text(
                        "Sign Up",
                        style: TextStyle(
                            color: Colors.black,
                            fontSize: 18,
                            fontWeight: FontWeight.bold),
                      )),
                )
              ],
            )
          ]),
        ),
      ),
    );
  }
}

Here is the Error shown in console:

The following assertion was thrown during a scheduler callback:
setState() called after dispose(): _LoginScreenState#0f444(lifecycle state: defunct, not mounted)

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.

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.
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 object during dispose().

When the exception was thrown, this was the stack
#0      State.setState.<anonymous closure>
package:flutter/…/widgets/framework.dart:1078
#1      State.setState
package:flutter/…/widgets/framework.dart:1113
#2      _LoginScreenState.initState.<anonymous closure>
package:masaba_tul_eelaaf/…/LoginPage/login_screen.dart:25
#3      SchedulerBinding._invokeFrameCallback
package:flutter/…/scheduler/binding.dart:1175
#4      SchedulerBinding.handleDrawFrame
package:flutter/…/scheduler/binding.dart:1113
#5      SchedulerBinding.scheduleWarmUpFrame.<anonymous closure>
package:flutter/…/scheduler/binding.dart:881
(elided 4 frames from class _RawReceivePortImpl, class _Timer, and dart:async-patch)
1 Answers

Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree.

As suggested by the error message:

if(mounted) {
  setState((){
    ...
  });
}

Edit: Explanation:

setState marks a widget as "needs rebuilding", thus making it reexecute the build method on the next possible occasion. If however the widget itself is no longer in the widget tree for example because you closed and reopened the app or you have a dialog that got dissmissed and is no longer visible, calling setState on that widget causes an error because it tries to rebuild something that cannot be built (because the instance is not "mounted"/in the widget tree). This usually happens when setState is called by a future or some kind of callback that might complete at a time after the dispose method of the widget is called.

Related