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();
}
}
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:
String message = await provider.signIn(...);
Scaffold.of(ctx).showSnackbar(...)
is called, it is now attempting to look up a scaffold in a widget tree that doesn't exist.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')