I am trying to create a snackbar on the click of a button in flutter, but getting exception that No ScaffoldMessenger
widget found. The same code seems to work properly in the Flutter sample mentioned in docs. Am I missing something here? Thanks.
Here's my main.dart file
import 'package:flutter/material.dart';
void main() => runApp(MyAppWidget());
class MyAppWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyAppWidget> {
final _inputKey = GlobalKey<FormState>();
final _messangerKey = GlobalKey<ScaffoldMessengerState>();
String inputText = "";
String appendString() {
setState(() {
inputText += inputText;
});
return inputText;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
scaffoldMessengerKey: _messangerKey,
home: Scaffold(
appBar: AppBar(
title: Text("Assignment"),
),
body: Form(
key: _inputKey,
child: Column(
children: [
TextFormField(
validator: (inputString) {
inputText = inputString;
if (inputString.length < 5) {
return 'Please enter a longer string';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_inputKey.currentState.validate()) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Processing Data')));
}
},
child: Text("Enter"),
),
Text(appendString())
],
),
),
),
);
}
}
Here's the exception I am getting
════════ Exception caught by gesture ═══════════════════════════════════════════
The following assertion was thrown while handling a gesture:
No ScaffoldMessenger widget found.
MyAppWidget widgets require a ScaffoldMessenger widget ancestor.
Handler: "onTap"
#4 _InkResponseState._handleTap
package:flutter/…/material/ink_well.dart:991
#3 _MyAppState.build.<anonymous closure>
package:assignment_1/main.dart:48
#2 ScaffoldMessenger.of
package:flutter/…/material/scaffold.dart:224
#1 debugCheckHasScaffoldMessenger
package:flutter/…/material/debug.dart:153
#0 debugCheckHasScaffoldMessenger.<anonymous closure>
package:flutter/…/material/debug.dart:142
When the exception was thrown, this was the stack
Typically, the ScaffoldMessenger widget is introduced by the MaterialApp at the top of your application widget tree.
renderObject: RenderView#a5074
[root]
The ancestors of this widget were
state: _MyAppState#a2cb9
Restarted application in 691ms.
════════ Exception caught by gesture ═══════════════════════════════════════════
The following assertion was thrown while handling a gesture:
No ScaffoldMessenger widget found.
MyAppWidget widgets require a ScaffoldMessenger widget ancestor.
The specific widget that could not find a ScaffoldMessenger ancestor was: MyAppWidget
To answer the original question: "What am I missing here?":
ScaffoldMessenger.of(context)
is using a context
not underneath (i.e. not a child of) MaterialApp
. (And there is no ScaffoldMessenger
to be found above.)
Native Flutter static methods <SomeClassName>.of(context)
walk up the widget tree looking for an InheritedWidget
type parent named <SomeClassName>
. So ScaffoldMessenger.of(context)
is looking for a ScaffoldMessenger
parent.1
MaterialApp
widget provides us a ScaffoldMessenger
because it wraps its children in a ScaffoldMessenger
. Here's an excerpt of MaterialApp's
"builder" method:
Widget _materialBuilder(BuildContext context, Widget? child) {
return ScaffoldMessenger( // <<<< <<<< hello Mr. ScaffoldMessenger
key: widget.scaffoldMessengerKey,
child: AnimatedTheme(
data: theme,
child: widget.builder != null
? Builder(
builder: (BuildContext context) {
return widget.builder!(context, child); // your kid
},
)
: child ?? const SizedBox.shrink(),
),
);
}
The child
above is likely your top-most widget that you provided to MaterialApp
. Only that widget's build method and below will find the ScaffoldMessenger
from MaterialApp
in its parent ancestry.
The original question's .showSnackBar()
was looking up MyApp's
context / parent ancestry, not MaterialApp's
.
MyApp(context)
-> MaterialApp(gets MyApp context) + other Widget(gets MyApp context) (no ScaffoldMessenger avail!)
-> kids (gets MaterialApp context, thus ScaffoldMessenger above)
It's really easy to mistake which context
we're using.
In the original code snippet, we're using the closest visible context
at this level, which is MyApp's
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) { // <<<< this MyApp context...
return MaterialApp(
scaffoldMessengerKey: _messangerKey,
home: Scaffold(
appBar: AppBar(
title: Text("Assignment"),
),
body: Form(
key: _inputKey,
child: Column(
children: [
TextFormField(
validator: (inputString) {
inputText = inputString;
if (inputString.length < 5) {
return 'Please enter a longer string';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_inputKey.currentState.validate()) {
ScaffoldMessenger.of(context).showSnackBar( // is this context <<<
SnackBar(content: Text('Processing Data')));
}
Even though the Scaffold > Form > ElevatedButton
widgets appear physically "lower/underneath" MaterialApp
, they're currently within MyApp's build(BuildContext context)
method and thus are currently using MyApp's
context
.
To use the context of MaterialApp
, Scaffold/Form/ElevatedButton
need to be called from inside a build(BuildContext context)
method that's inside MaterialApp
& thus gets its context
.
One way to avoid the above pitfall is to keep MyApp/MaterialApp
very simple and start our coding another level down, say in HomePage
in the example below:
class MyApp extends StatelessWidget {
const MyApp({Key? key, required this.bindings}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const HomePage(), // <<< put your stuff in HomePage's build() method
);
}
}
Alternatively we can wrap the home:
widget in a Builder
widget (docs) and every child within that Builder
will then be using MaterialApp's context
, which has the first available ScaffoldMessenger
.
class MyApp extends StatelessWidget {
const MyApp({Key? key, required this.bindings}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Builder(builder: (BuildContext context) {
// stuff here gets MaterialApp context
}),
);
}
}
And as mentioned in other answers we can use a scaffoldMessengerKey
arg using a GlobalKey<ScaffoldMessengerState>
as described in the migration guide when ScaffoldMessenger
was introduced.
1 The InheritedWidget
type looked up by this .of()
is actually _ScaffoldMessengerScope
, which contains the ScaffoldMessengerState
field, which is the object providing the SnackBar
API methods we're interested in, such as .showSnackBar()
as suggested by EngineSense, Created one Global key
final _messangerKey = GlobalKey<ScaffoldMessengerState>();
and added this in the onPressed method of button
_messangerKey.currentState.showSnackBar(
SnackBar(content: Text('Processing Data')));
Here's the updated changes for reference.
import 'package:flutter/material.dart';
void main() => runApp(MyAppWidget());
class MyAppWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyAppWidget> {
final _inputKey = GlobalKey<FormState>();
final _messangerKey = GlobalKey<ScaffoldMessengerState>();
String inputText = "";
String appendString() {
setState(() {
inputText += inputText;
});
return inputText;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
scaffoldMessengerKey: _messangerKey,
home: Scaffold(
appBar: AppBar(
title: Text("Assignment"),
),
body: Form(
key: _inputKey,
child: Column(
children: [
TextFormField(
validator: (inputString) {
inputText = inputString;
if (inputString.length < 5) {
return 'Please enter a longer string';
}
return null;
},
),
ElevatedButton(
onPressed: () {
if (_inputKey.currentState.validate()) {
_messangerKey.currentState.showSnackBar(
SnackBar(content: Text('Processing Data')));
}
},
child: Text("Enter"),
),
Text(appendString())
],
),
),
),
);
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With