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

huangapple go评论93阅读模式
英文:

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

问题

我有使用Provider包显示SnackBar消息的问题。我收到的错误消息是:

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…

以下是我的代码,我认为所有与所需逻辑相关的类都包括在内。我无法理解为什么Future<String>不可用,或者在user_log_in_provider.dartsignIn方法中出现了什么错误。我还尝试通过user_log_in_provider.dart中的变量_errorMessage来显示signIn方法中的errorMessage,然后检查此消息是否不为空。通过这种方式,代码可以运行,但会延迟显示一条消息。例如,第一次登录失败(错误的电子邮件格式)->没有显示消息。第二次登录失败(错误的密码)->显示了带有错误的电子邮件格式的消息。

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();
  }
}
英文:

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&#39;s ancestor is unsafe.
At this point the state of the widget&#39;s element tree is no longer stable.
To safely refer to a widget&#39;s ancestor in its dispose() method, save a reference to the ancestor by calling dependOnInheritedWidgetOfExactType() in the widget&#39;s didChangeDependencies() method.
#0      Element._debugCheckStateIsActiveForAncestorLookup.&lt;anonymous closure&gt; (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.&lt;anonymous closure&gt;.&lt;anonymous closure&gt; (package:zvjs_app/screens/login_screen.dart:74:38)
&lt;asynchronous suspension&gt;
#5      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:182&lt;…&gt;

Bellow is my code, I think all classes that are part of logic that is needed. I can't understand why Future<String> 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() =&gt; runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) =&gt; UserLogIn.instance()),
        ChangeNotifierProvider.value(value: Accommodations()),
      ],
      child: MaterialApp(
        title: &#39;ZVJS&#39;,
        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) =&gt; RegistrationScreen(),
          MainScreen.routeName: (context) =&gt; MainScreen(),
          LoginScreen.routeName: (context) =&gt; MyHomePage(),
        },
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Consumer&lt;UserLogIn&gt;(
      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 = &#39;/loginScreen&#39;;

  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&lt;UserLogIn&gt;(context).status == Status.Authenticating
          ? SpinnerCustom(Constants.loggingIn)
          : Center(
              child: SingleChildScrollView(
                child: Column(
                  children: &lt;Widget&gt;[
                    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) =&gt; ButtonCustom(
                        text: Constants.logIn,
                        onPressed: () async {
                          var provider = Provider.of&lt;UserLogIn&gt;(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 =&gt; _status;

  FirebaseUser get user =&gt; _user;

  String get errorMessage =&gt; _errorMessage;

  Future&lt;String&gt; 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&lt;void&gt; _onAuthStateChanged(FirebaseUser firebaseUser) async {
    if (firebaseUser == null) {
      _status = Status.Unauthenticated;
    } else {
      _user = firebaseUser;
      _status = Status.Authenticated;
    }
    notifyListeners();
  }
}

答案1

得分: 10

问题:

您之所以会出现错误,是因为这段代码:

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

Scaffold.of(context) 试图查找不再在其上方的小部件树中的脚手架。

问题是如何出现的:

  1. 登录调用是异步触发的:String message = await provider.signIn(...);
  2. 当等待调用时,按钮小部件的父级可能已更改,或者按钮本身可能已从树中删除。
  3. 然后,当调用 Scaffold.of(ctx).showSnackbar(...) 时,它现在正在尝试在不存在的小部件树中查找一个脚手架。

解决方案:

有几种解决方案。其中之一是使用全局脚手架,它包装了您的每个路由。然后可以使用该脚手架键来显示 Snackbar。

以下是如何实现的:

在 MaterialApp 构建器中添加一个脚手架。 确保使用全局键。

final globalScaffoldKey = GlobalKey&lt;ScaffoldState&gt;();

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

      ...

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

然后,您可以使用该键通过全局函数显示 Snackbar:

 void showSnackbar(String message) {
    var currentScaffold = globalScaffoldKey.currentState;
    currentScaffold.hideCurrentSnackBar(); // 如果有可见的 snackbar,请在显示新 snackbar 之前将其隐藏。
    currentScaffold.showSnackBar(SnackBar(content: Text(message)));
  }

使用方法如下,您可以安全地从代码的任何位置调用它:

showSnackbar(&#39;我的 Snackbar 消息&#39;)
英文:

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&lt;ScaffoldState&gt;();

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(&#39;My Snackbar Message&#39;)

huangapple
  • 本文由 发表于 2020年1月3日 21:42:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/59579640.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定