Flutter - Looking up a deactivated widget's ancestor is unsafe with Provider package, FireStore authentication

Sam picture Sam · Jan 3, 2020 · Viewed 8.6k times · Source

I have an problem with showing message via SnackBar using Provider package. The error message I get is:

VERBOSE-2:ui_dart_state.cc(157)] Unhandled Exception: 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.
#0      Element._debugCheckStateIsActiveForAncestorLookup.<anonymous closure> (package:flutter/src/widgets/framework.dart:3508:9)
#1      Element._debugCheckStateIsActiveForAncestorLookup (package:flutter/src/widgets/framework.dart:3522:6)
#2      Element.findAncestorStateOfType (package:flutter/src/widgets/framework.dart:3641:12)
#3      Scaffold.of (package:flutter/src/material/scaffold.dart:1313:42)
#4      LoginScreen.build.<anonymous closure>.<anonymous closure> (package:zvjs_app/screens/login_screen.dart:74:38)
<asynchronous suspension>
#5      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:182<…>

Bellow is my code, I think all classes that are part of logic that is needed. I can't understand why Future isn't "available" or what error means in user_log_in_provider.dart in sigIn method. I also tried to show errorMessage from sigIn method via variable _errorMessage which you can see in user_log_in_provider.dart and then check if this message isn't null. In this way code runs but it is showing one message delayed. For e. first login failed(wrong email format) -> no message shown. Second login failed(wrong password) -> message with wrong email format is shown.

main.dart

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => UserLogIn.instance()),
        ChangeNotifierProvider.value(value: Accommodations()),
      ],
      child: MaterialApp(
        title: 'ZVJS',
        theme: ThemeData(
            primarySwatch: Colors.blue,
            buttonTheme: ButtonThemeData(
              buttonColor: Colors.blue[300],
              padding: EdgeInsets.symmetric(
                vertical: 8.0,
                horizontal: 16.0,
              ),
            )),
        home: MyHomePage(),
        routes: {
          RegistrationScreen.routeName: (context) => RegistrationScreen(),
          MainScreen.routeName: (context) => MainScreen(),
          LoginScreen.routeName: (context) => MyHomePage(),
        },
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Consumer<UserLogIn>(
      builder: (context, user, _) {
        switch (user.status) {
          case Status.Uninitialized:
//            return Splash();
          case Status.Unauthenticated:
          case Status.Authenticating:
            return LoginScreen(
                emailController: _emailController,
                passwordController: _passwordController);
          case Status.Authenticated:
            return MainScreen();
          default:
            return ErrorPage();
        }
      },
    );
  }
}

login_screen.dart

class LoginScreen extends StatelessWidget {
  static const routeName = '/loginScreen';

  final _emailController;
  final _passwordController;

  LoginScreen(
      {@required TextEditingController emailController,
      @required TextEditingController passwordController})
      : this._emailController = emailController,
        this._passwordController = passwordController;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(Constants.logInPageTitle),
      ),
      body: Provider.of<UserLogIn>(context).status == Status.Authenticating
          ? SpinnerCustom(Constants.loggingIn)
          : Center(
              child: SingleChildScrollView(
                child: Column(
                  children: <Widget>[
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 8.0),
                      child: TextFieldCustom(
                        text: Constants.email,
                        controller: _emailController,
                        icon: Icon(Icons.email),
                        textInputType: TextInputType.emailAddress,
                      ),
                    ),
                    const SizedBox(height: 20),
                    Padding(
                      padding: const EdgeInsets.symmetric(horizontal: 8.0),
                      child: TextFieldCustom(
                        text: Constants.password,
                        controller: _passwordController,
                        icon: Icon(Icons.lock),
                        textInputType: TextInputType.visiblePassword,
                      ),
                    ),
                    const SizedBox(height: 10),
                    Builder(
                      builder: (ctx) => ButtonCustom(
                        text: Constants.logIn,
                        onPressed: () async {
                          var provider = Provider.of<UserLogIn>(ctx, listen: false);
                          String message = await provider.signIn(
                                _emailController.text,
                                _passwordController.text);
                          if (message != null) {
                            Scaffold.of(ctx).showSnackBar(SnackBar(
                              content: Text(message),
                            ));
                          }
                        },
                      ),
                    ),
                  ],
                ),
              ),
            ),
    );
  }
}

user_log_in_provider.dart

enum Status { Uninitialized, Authenticated, Authenticating, Unauthenticated }

class UserLogIn with ChangeNotifier {
  FirebaseAuth _auth;
  FirebaseUser _user;
  Status _status = Status.Uninitialized;
  String _errorMessage;

  UserLogIn.instance() : _auth = FirebaseAuth.instance {
    _auth.onAuthStateChanged.listen(_onAuthStateChanged);
  }

  Status get status => _status;

  FirebaseUser get user => _user;

  String get errorMessage => _errorMessage;

  Future<String> signIn(String email, String password) async {
    try {
      _status = Status.Authenticating;
      notifyListeners();
      await _auth.signInWithEmailAndPassword(email: email, password: password);
      return null;
    } catch (e) {
      _errorMessage = e.message;
      print(_errorMessage);
      _status = Status.Unauthenticated;
      notifyListeners();
      return e.message;
    }
  }

  Future<void> _onAuthStateChanged(FirebaseUser firebaseUser) async {
    if (firebaseUser == null) {
      _status = Status.Unauthenticated;
    } else {
      _user = firebaseUser;
      _status = Status.Authenticated;
    }
    notifyListeners();
  }
}

Answer

mskolnick picture mskolnick · Jun 12, 2020

Problem:

You are getting the error because of this code:

 Scaffold.of(ctx).showSnackBar(SnackBar(
  content: Text(message),
 ));

The Scaffold.of(context) is attempting to look up the scaffold in a widget tree that is no longer above it.

Here is how the issue is arising:

  1. The login call is fired off asynchronously: String message = await provider.signIn(...);
  2. While the call is being awaited, the parents of the button widget may have changed, or the button itself may have been removed from the tree.
  3. Then, when Scaffold.of(ctx).showSnackbar(...) is called, it is now attempting to look up a scaffold in a widget tree that doesn't exist.

Solution:

There are a few solutions. One of them is to use a global scaffold which wraps each of your routes. That scaffold key can then be used to show snackbars.

Here is how that could be done:

Add a scaffold to your MaterialApp builder. Make sure to use the global key.

final globalScaffoldKey = GlobalKey<ScaffoldState>();

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(

      ...

      child: MaterialApp(
       builder: (context, child) {
        return Scaffold(
          key: globalScaffoldKey,
          body: child,
        );
      },
...

You could then use that key to show snackbars through a global function:

 void showSnackbar(String message) {
    var currentScaffold = globalScaffoldKey.currentState;
    currentScaffold.hideCurrentSnackBar(); // If there is a snackbar visible, hide it before the new one is shown.
    currentScaffold.showSnackBar(SnackBar(content: Text(message)));
  }

Usage would look like this, and you can safely call it from anywhere in your code:

showSnackbar('My Snackbar Message')