web-dev-qa-db-fra.com

Flutter - La recherche de l'ancêtre d'un widget désactivé n'est pas sécurisée avec le package Provider, l'authentification FireStore

J'ai un problème avec l'affichage du message via SnackBar à l'aide du package Provider. Le message d'erreur que je reçois est:

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<…>

Ci-dessous est mon code, je pense que toutes les classes qui font partie de la logique nécessaire. Je ne peux pas comprendre pourquoi Future n'est pas "disponible" ou ce que signifie l'erreur dans user_log_in_provider.Dart dans la méthode sigIn. J'ai également essayé d'afficher errorMessage de la méthode sigIn via la variable _errorMessage que vous pouvez voir dans user_log_in_provider.Dart, puis vérifier si ce message n'est pas nul. De cette façon, le code s'exécute mais il affiche un message retardé. Pour e. échec de la première connexion (format d'e-mail incorrect) -> aucun message affiché. La deuxième connexion a échoué (mot de passe incorrect) -> un message avec un format de courrier électronique incorrect est affiché.

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();
  }
}
9
Sam

Problème:

Vous obtenez l'erreur à cause de ce code:

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

La Scaffold.of(context) tente de rechercher l'échafaudage dans une arborescence de widgets qui n'est plus au-dessus de lui.

Voici comment le problème se pose:

  1. L'appel de connexion est déclenché de manière asynchrone: String message = await provider.signIn(...);
  2. Pendant l'attente de l'appel, les parents du widget bouton peuvent avoir changé, ou le bouton lui-même peut avoir été supprimé de l'arborescence.
  3. Ensuite, lorsque Scaffold.of(ctx).showSnackbar(...) est appelé, il tente maintenant de rechercher un échafaudage dans une arborescence de widgets qui n'existe pas.

Solution:

Il existe quelques solutions. L'un d'eux consiste à utiliser un échafaudage global qui enveloppe chacun de vos itinéraires. Cette clé d'échafaudage peut ensuite être utilisée pour afficher les barres-collations.

Voici comment cela pourrait être fait:

Ajoutez un échafaudage à votre générateur MaterialApp. Assurez-vous d'utiliser la clé globale.

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,
        );
      },
...

Vous pouvez ensuite utiliser cette touche pour afficher les barres-collations via une fonction globale:

 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)));
  }

L'utilisation ressemblerait à ceci, et vous pouvez l'appeler en toute sécurité depuis n'importe où dans votre code:

showSnackbar('My Snackbar Message')
2
mskolnick