英文:
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.dart
的signIn
方法中出现了什么错误。我还尝试通过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'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<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() => 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();
}
}
答案1
得分: 10
问题:
您之所以会出现错误,是因为这段代码:
Scaffold.of(ctx).showSnackBar(SnackBar(
content: Text(message),
));
Scaffold.of(context)
试图查找不再在其上方的小部件树中的脚手架。
问题是如何出现的:
- 登录调用是异步触发的:
String message = await provider.signIn(...);
- 当等待调用时,按钮小部件的父级可能已更改,或者按钮本身可能已从树中删除。
- 然后,当调用
Scaffold.of(ctx).showSnackbar(...)
时,它现在正在尝试在不存在的小部件树中查找一个脚手架。
解决方案:
有几种解决方案。其中之一是使用全局脚手架,它包装了您的每个路由。然后可以使用该脚手架键来显示 Snackbar。
以下是如何实现的:
在 MaterialApp 构建器中添加一个脚手架。 确保使用全局键。
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,
);
},
...
然后,您可以使用该键通过全局函数显示 Snackbar:
void showSnackbar(String message) {
var currentScaffold = globalScaffoldKey.currentState;
currentScaffold.hideCurrentSnackBar(); // 如果有可见的 snackbar,请在显示新 snackbar 之前将其隐藏。
currentScaffold.showSnackBar(SnackBar(content: Text(message)));
}
使用方法如下,您可以安全地从代码的任何位置调用它:
showSnackbar('我的 Snackbar 消息')
英文:
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:
- The login call is fired off asynchronously:
String message = await provider.signIn(...);
- 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.
- 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')
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论