showMaterialBanner doesnt hide on change route Looking up a deactivated widget's ancestor is unsafe

Viewed 676

Cant close Material banner after closing the page that call it.

I two screen LoginScreen RecoverScreen, from LoginScreen the user open RecoverScreen to send recover password's email, if sent was done sucesfully i show MaterialBanner and pop RecoverScreen, in LoginScreen when i clic "Close" button in my Materialbanner i got the following error:

════════ Exception caught by gesture ═══════════════════════════════════════════ The following assertion was thrown while handling a gesture: Looking up a deactivated widget's ancestor is unsafe.

At this point the state of the widget's element tree is no longer stable.

To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

When the exception was thrown, this was the stack dart-sdk/lib/internal/js_dev_runtime/private/ddc_runtime/errors.dart 251:49 throw packages/flutter/src/widgets/framework.dart 4032:9 packages/flutter/src/widgets/framework.dart 4045:14 [_debugCheckStateIsActiveForAncestorLookup]

ScaffoldMessenger.of(context).showMaterialBanner(
                  MaterialBanner(
                    padding: EdgeInsets.all(20),
                    content: Text('Recover password email was sended.'),
                    leading: Icon(Icons.info),
                    backgroundColor: Colors.green,
                    actions: <Widget>[
                      TextButton(
                        child: Text(
                          'Close',
                          style: TextStyle(color: Colors.white),
                        ),
                        onPressed: () {
                          ScaffoldMessenger.of(context)
                              .hideCurrentMaterialBanner();
                        },
                      ),
                    ],
                  ),
                );

Try this full example:

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: LoginScreen(),
    ),
  );
}

class LoginScreen extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: ElevatedButton(
        onPressed: () {
          Navigator.push(
            context,
            MaterialPageRoute(
              builder: (context) => RecoverScreen(),
            ),
          );
        },
        child: const Text('Recover password'),
      )),
    );
  }
}

class RecoverScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: ElevatedButton(
        onPressed: () {
          Navigator.of(context).pop();
          ScaffoldMessenger.of(context).showMaterialBanner(
            MaterialBanner(
              padding: const EdgeInsets.all(20),
              content: const Text('Recover password email was sended.'),
              leading: const Icon(Icons.info),
              backgroundColor: Colors.green,
              actions: <Widget>[
                TextButton(
                  child: const Text(
                    'Cerrar',
                    style: TextStyle(color: Colors.white),
                  ),
                  onPressed: () {
                    ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
                  },
                ),
              ],
            ),
          );
        },
        child: const Text('Send Password'),
      )),
    );
  }
}

I know that in this case i can use a snackbar, but its only a example and i want to know how to deal with this problem.

3 Answers

Try wrapping the TextButton with Builder and pass the context of it instead of already closed RecoverScreen's.

              actions: <Widget>[
                Builder(
                  builder: (context) => TextButton(
                    child: const Text(
                      'Cerrar',
                      style: TextStyle(color: Colors.white),
                    ),
                    onPressed: () {
                      ScaffoldMessenger.of(context).hideCurrentMaterialBanner();
                    },
                  ),
                ),
              ],

Before route change call ScaffoldMessenger.of(context).clearMaterialBanners();

Like, here wrap Scaffold widget by WillPopScope,

Sample Code:

 WillPopScope(
   onWillPop: (){
     ScaffoldMessenger.of(context).clearMaterialBanners();
     Navigator.pop(context);
     return;
   },
   child: ........
 }

Here is how I solved it in Flutter 3.0.5

@override
Widget build(BuildContext context) {
  return WillPopScope(
    onWillPop: () {
      var c = Completer<bool>();
      try {
        ScaffoldMessenger.of(context).clearMaterialBanners();
        ScaffoldMessenger.of(context).clearSnackBars();
        c.complete(true);
      } catch (e) {
        c.completeError(e);
      }
      return c.future;
    },
    child: Scaffold(...),
  );
}

Related