1

In my Log in Screen, I encounter a problem due to BuildContext within an async block. When user wlick on "Log in" button (Se connecter in French) : I start an async procedure (check connectivity, check if user credentials are ok...) but I also open "dialog boxes" to display loading indicator, or a sort of a popup (My alert info) to tell user if there was a problem. I get indication that I shouldn't use "context" when inside an async clock.

Container(
                        margin: const EdgeInsets.only(right: 10),
                        child: ElevatedButton(
                            style: ElevatedButton.styleFrom(
                              minimumSize: const Size(20, 45),
                              backgroundColor: Colors.blue[900],
                              foregroundColor: Colors.white,
                            ),
                            onPressed: () async {
                              if  (await _connectivity.checkConnectivity() ==
                                  ConnectivityResult.none) {
                                showDialog(
                                  context: context,
                                  builder: (ctx) => AlertDialog(
                                    title: const Text(
                                      'Oops... ',
                                      style: TextStyle(fontSize: 22.0),
                                    ),
                                    content: const Text(
                                      'Il faut te connecter à Internet !',
                                      style: TextStyle(fontSize: 18.0),
                                    ),
                                    actions: [
                                      TextButton(
                                        onPressed: () {
                                          Navigator.pushReplacementNamed(
                                              context, StartingScreen.id);
                                        },
                                        child: const Text(
                                          'OK',
                                          style: TextStyle(fontSize: 18.0),
                                        ),
                                      ),
                                    ],
                                  ),
                                );
                              } else {
                                DialogBuilder(context).showLoadingIndicator(
                                    heading: 'Patientez ...',
                                    text: 'Téléchargement des bases',
                                    color: Colors.black45);
                                String accountVerif =
                                await uD.checkAccountId(
                                    emailController.text,
                                    passWordController.text);
                                print('Voici Account Verif : $accountVerif');
                                if (accountVerif == '') {
                                  await uD.downLoadUserInfo();
                                  final prefs =
                                  await SharedPreferences.getInstance();
                                  prefs.setString('email',
                                      emailController.text.toLowerCase());
                                  prefs.setString(
                                      'passWord', passWordController.text);
                                  DialogBuilder(context).hideOpenDialog();
                                  Navigator.pushReplacementNamed(
                                      context, HomeScreen.id);
                                } else {
                                  setState(() {
                                    emailController.clear();
                                    passWordController.clear();
                                  });
                                  Navigator.pop(context);
                                 // myAlertInfo(context,
                                //      icone: Icons.warning,
                                 //     alerte: 'Oops... ',
                                 //     text:
                                  //    'Ce compte n\'existe pas, ou le mot de passe est incorrect.',
                                //      bouton1: 'OK',
                                 //     function1: () =>
                                 //         Navigator.pop(context));
                                }
                              }
                            },
                            child: Text('Se connecter',
                                style: TextStyle(fontSize: uD.mediumFont))),
                      ),

How could I review the code to prevent this problem ? Thanks for your help :)

SylvainJack
  • 1,007
  • 1
  • 8
  • 27

2 Answers2

2

When you are using an async method, you should check if state of widget is mounted in a StatefulWidget like this:

() async {
            if (await _connectivity.checkConnectivity() ==
                ConnectivityResult.none && mounted) {
              showDialog(
                context: context,
                builder: (ctx) =>
                    AlertDialog(
                      title: const Text(
                        'Oops... ',
                        style: TextStyle(fontSize: 22.0),
                      ),
                      content: const Text(
                        'Il faut te connecter à Internet !',
                        style: TextStyle(fontSize: 18.0),
                      ),
                      actions: [
                        TextButton(
                          onPressed: () {
                            Navigator.pushReplacementNamed(
                                context, StartingScreen.id);
                          },
                          child: const Text(
                            'OK',
                            style: TextStyle(fontSize: 18.0),
                          ),
                        ),
                      ],
                    ),
              );
            } else {
              if (mounted) {
                DialogBuilder(context).showLoadingIndicator(
                    heading: 'Patientez ...',
                    text: 'Téléchargement des bases',
                    color: Colors.black45);
                String accountVerif =
                await uD.checkAccountId(
                    emailController.text,
                    passWordController.text);
                print('Voici Account Verif : $accountVerif');
                if (accountVerif == '') {
                  await uD.downLoadUserInfo();
                  final prefs =
                  await SharedPreferences.getInstance();
                  prefs.setString('email',
                      emailController.text.toLowerCase());
                  prefs.setString(
                      'passWord', passWordController.text);
                  if (mounted) {
                    DialogBuilder(context).hideOpenDialog();
                    Navigator.pushReplacementNamed(
                        context, HomeScreen.id);
                  }
                } else {
                  setState(() {
                    emailController.clear();
                    passWordController.clear();
                  });
                  Navigator.pop(context);
                  
                  // myAlertInfo(context,
                  //      icone: Icons.warning,
                  //     alerte: 'Oops... ',
                  //     text:
                  //    'Ce compte n\'existe pas, ou le mot de passe est incorrect.',
                  //      bouton1: 'OK',
                  //     function1: () =>
                  //         Navigator.pop(context));
                }
              }
            }
          }
Mahdi
  • 21
  • 7
0

It is quite important to check if the widget is mounted or not before performing operations using BuildContext.

Consider this scenario:

  • You have a screen with two buttons A and B
  • When you press A, After 2 seconds, the current screen is popped off the navigator (Disposed) and replaced by a new screen ScreenA.
  • When you press B, The same thing happens but ScreenB replaces the current one.

The code for onClickA looks like:

Future<void> _onClickA() async {
    await Future.delayed(const Duration(seconds: 2));
    await Navigator.of(context).pushReplacement([Your ScreenA route here]);
}

The code for onClickB is similar:

Future<void> _onClickB() async {
    await Future.delayed(const Duration(seconds: 2));
    await Navigator.of(context).pushReplacement([Your ScreenB route here]);
}

The problem arises when the user presses both A and B

When the user presses A, and presses B after a second, both functions get executed. "A" was pressed first so ScreenA appears after 2 seconds but, the previous screen is disposed because we used the pushReplacement method.

But, the _onClickB is still executing. After 2 seconds, it will try to navigate to the ScreenB with the context of a page that has been disposed. Hence, the error.

It is always good to check if the screen is mounted before doing operations using BuildContext. For it, you have to use a StatefulWidget. In a stateful widget's state class, you get the boolean property mounted. You can use it to check if the screen is still not disposed.

This is the safer version:

Future<void> _onClickA() async {
    await Future.delayed(const Duration(seconds: 2));
    if (mounted) {
        await Navigator.of(context).pushReplacement([Your ScreenA route here]);
    }
}

and

Future<void> _onClickB() async {
    await Future.delayed(const Duration(seconds: 2));
    if (mounted) {
        await Navigator.of(context).pushReplacement([Your ScreenB route here]);
    }
}

Now, even if the user presses both A and B, the navigator does not try to push the ScreenB as the calling screen has been disposed.

Afridi Kayal
  • 2,112
  • 1
  • 5
  • 15