Flutter: 找不到 ScaffoldMessenger 组件

18

我试图在Flutter中的按钮单击事件上创建一个snackbar,但是出现了异常,称未找到ScaffoldMessenger小部件。相同的代码在Flutter 示例 中似乎可以正常工作。我是否漏掉了什么?谢谢。

这是我的main.dart文件:

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

我遇到了这个异常

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


2
你会尝试这样做吗? scaffoldMessengerKey.currentState.showSnackBar(mySnackBar); scaffoldMessengerKey.currentState.hideCurrentSnackBar(mySnackBar); scaffoldMessengerKey.currentState.removeCurrentSnackBar(mySnackBar); - EngineSense
10个回答

19

问题 / 为什么会发生这种情况

回答最初的问题:“我在这里漏掉了什么?”:

ScaffoldMessenger.of(context) 使用的是不在(即非MaterialApp的子代)context (并且找不到任何ScaffoldMessenger。)

本地Flutter静态方法<SomeClassName>.of(context)沿着widget树向上查找名为<SomeClassName>的继承小部件类型的父级。因此,ScaffoldMessenger.of(context)正在寻找一个ScaffoldMessenger父级。1

MaterialApp小部件为我们提供了一个ScaffoldMessenger,因为它将其子项包装在ScaffoldMessenger中。以下是MaterialApp“builder”方法的摘录:

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

上面的child很可能是您提供给MaterialApp的最顶层小部件。只有该小部件的build方法及以下内容才能从其父级继承中找到来自MaterialAppScaffoldMessenger

原问题中的.showSnackBar()在查找MyApp的上下文/父级继承,而不是MaterialApp

MyApp(context)
 -> MaterialApp(gets MyApp context) + other Widget(gets MyApp context) (no ScaffoldMessenger avail!)
   -> kids (gets MaterialApp context, thus ScaffoldMessenger above)

原始问题的代码

很容易混淆我们正在使用哪个context

在原始代码片段中,我们正在使用此级别上最接近的可见context,即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')));
                  }

尽管Scaffold > Form > ElevatedButton小部件在视觉上看起来比MaterialApp“低/在下方”,但它们当前位于MyApp的build(BuildContext context)方法中,因此当前正在使用MyApp的context

要使用MaterialApp的上下文,需要从MaterialApp内部调用Scaffold/Form/ElevatedButton,并且因此获取其context需要在build(BuildContext context)方法内部。

避免上述问题的一种方法是保持MyApp/MaterialApp非常简单,并在下一级,例如在以下示例中的HomePage中开始编码:

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

}

或者我们可以将home: widget包装在Builder widget(文档)中,那么在Builder中的每个子元素都将使用MaterialApp's context,该context具有第一个可用的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
      }),
    );
  }

}

正如其他答案中提到的,我们可以使用一个 scaffoldMessengerKey 参数,并使用 GlobalKey<ScaffoldMessengerState>,如在引入 ScaffoldMessenger 时在 迁移指南 中所述。


1通过 .of() 查找的 InheritedWidget 类型实际上是 _ScaffoldMessengerScope,它包含了 ScaffoldMessengerState 字段,该字段是提供我们感兴趣的 SnackBar API 方法(例如 .showSnackBar())的对象。


15

EngineSense所建议,创建一个全局键。

final _messangerKey = GlobalKey<ScaffoldMessengerState>();

并将此添加到按钮的onPressed方法中

_messangerKey.currentState.showSnackBar(
                        SnackBar(content: Text('Processing Data')));

这里是更新后的更改内容,供参考。

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


3
您可以将SnackBarGlobal定义为类,并将其设置为静态,以便在整个应用程序中使用它。
这是一个小例子,在不同的小部件中更改导航,在每个小部件中都可以触发SnackBarGlobal。在DartPad上查看示例代码。
您还可以查看文档,其中解释了错误“Scaffold widgets require a ScaffoldMessenger widget ancestor”。
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      scaffoldMessengerKey: SnackbarGlobal.key,
      routes: routes,
      initialRoute: 'pageOne',
    );
  }
}

class SnackbarGlobal {
  static GlobalKey<ScaffoldMessengerState> key =
      GlobalKey<ScaffoldMessengerState>();

  static void show(String message) {
    key.currentState!
      ..hideCurrentSnackBar()
      ..showSnackBar(SnackBar(content: Text(message)));
  }
}

class MyPage extends StatelessWidget {
  final String title;
  final String route;

  MyPage(this.title, this.route);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: [
            TextButton(
              child: Text('Go to $route'),
              onPressed: () {
                Navigator.pushNamed(context, route);
              },
            ),
            TextButton(
              child: Text('Show SnackBar in $title'),
              onPressed: () {
                SnackbarGlobal.show('This is a SnackBar in $title');
              },
            ),
          ],
        ),
      ),
    );
  }
}

final routes = <String, WidgetBuilder>{
  'pageOne': (BuildContext context) => MyPage('My Page One', 'pageTwo'),
  'pageTwo': (BuildContext context) => MyPage('My Page Two', 'pageThree'),
  'pageThree': (BuildContext context) => MyPage('My Page Three', 'pageOne'),
};

1
我希望我能给这个答案点赞+100,因为如果不将关键字设为全局,它就有点无用了。然而,没有其他与ScaffoldMessenger相关的答案讨论如何从“任何地方”使用它,这当然是整个重点。 - Chris Nadovich

3

scaffoldMessengerKey.currentState.showSnackBar(mySnackBar); scaffoldMessengerKey.currentState.hideCurrentSnackBar(mySnackBar); scaffoldMessengerKey.currentState.removeCurrentSnackBar(mySnackBar);

scaffoldMessengerKey.currentState 可以用于控制一个 ScaffoldMessenger widget 中的 SnackBar 组件,showSnackBar() 方法可以显示一个 SnackBar,hideCurrentSnackBar() 方法可以隐藏当前正在显示的 SnackBar,removeCurrentSnackBar() 方法可以将当前正在显示的 SnackBar 从 Snackbar 队列中移除。


1
我遇到了同样的问题,并发现了一种奇怪的行为。
以下代码是从显示一个 Snackbar复制的,它能正常工作。
import 'package:flutter/material.dart';

void main() => runApp(const SnackBarDemo());

class SnackBarDemo extends StatelessWidget {
  const SnackBarDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SnackBar Demo',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('SnackBar Demo'),
        ),
        body: const SnackBarPage(),
      ),
    );
  }
}

class SnackBarPage extends StatelessWidget {
  const SnackBarPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Center(
      child: ElevatedButton(
        onPressed: () {
          final snackBar = SnackBar(
            content: const Text('Yay! A SnackBar!'),
            action: SnackBarAction(
              label: 'Undo',
              onPressed: () {
                // Some code to undo the change.
              },
            ),
          );

          // Find the ScaffoldMessenger in the widget tree
          // and use it to show a SnackBar.
          ScaffoldMessenger.of(context).showSnackBar(snackBar);
        },
        child: const Text('Show SnackBar'),
      ),
    );
  }
}

然而,当我移除 SnackBarPage 类,并将其代码复制到 build() 方法中直接放在 SnackBarDemo 类中时,就像下面这样。
import 'package:flutter/material.dart';

void main() => runApp(const SnackBarDemo());

class SnackBarDemo extends StatelessWidget {
  const SnackBarDemo({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'SnackBar Demo',
      home: Scaffold(
          appBar: AppBar(
            title: const Text('SnackBar Demo'),
          ),
          body: Center(
            child: ElevatedButton(
              onPressed: () {
                final snackBar = SnackBar(
                  content: const Text('Yay! A SnackBar!'),
                  action: SnackBarAction(
                    label: 'Undo',
                    onPressed: () {
                      // Some code to undo the change.
                    },
                  ),
                );

                // Find the ScaffoldMessenger in the widget tree
                // and use it to show a SnackBar.
                ScaffoldMessenger.of(context).showSnackBar(snackBar);
              },
              child: const Text('Show SnackBar'),
            ),
          )),
    );
  }
}

然后它将不再起作用,并引发“找不到ScaffoldMessenger小部件。 SnackBarDemo小部件需要一个ScaffoldMessenger小部件祖先。”,看起来只有当包装在某些小部件类中时,ScaffoldMessenger才起作用。我不知道为什么。

似乎您应该将ScaffoldMessenger从MaterialApp小部件中获取,并将ScaffoldMessenger分离到另一个类小部件中,使其不在MaterialApp小部件中。 - Ahmed Soliman
对于未来的旅行者,代码在构建方法中直接运行不起作用的原因是因为该上下文实际上位于 MaterialApp 的一层之上(这也是 ScaffoldMessenger 来自的地方)。当您将该代码包装在另一层中时,即 SnackBarPage 中,上下文会深入一层,现在包括了 Widget 树中的 MaterialApp。在 MaterialApp 中使用 context 可以从 SnackBarDemo 中看到。 在 SnackBarPage 中使用 context 可以看到 MaterialApp 及以上内容,这正是您所需要的。 - Jeff Neet

0

对我而言可行的方法是为 Scaffold() 创建一个独立的 StatefulWidget(如下所示的MyScaffold)。这在我的情况下运作得很好。

import 'package:flutter/material.dart';

void main() => runApp(MyAppWidget());

class MyAppWidget extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return _MyAppState();
  }
}

class _MyAppState extends State<MyAppWidget> {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: MyScaffold());
  }
}

class MyScaffold extends StatefulWidget {
  const MyScaffold({Key? key}) : super(key: key);

  @override
  _MyScaffoldState createState() => _MyScaffoldState();
}

class _MyScaffoldState extends State<MyScaffold> {
  String inputText = "";

  String appendString() {
    setState(() {
      inputText += inputText;
    });
    return inputText;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Assignment"),
      ),
      body: Form(
        child: Column(
          children: [
            TextFormField(
              validator: (inputString) {
                inputText = inputString!;
                if (inputString.length < 5) {
                  return 'Please enter a longer string';
                }
                return null;
              },
            ),
            ElevatedButton(
              onPressed: () {
                ScaffoldMessenger.of(context)
                    .showSnackBar(SnackBar(content: Text('Processing Data')));
              },
              child: Text("Enter"),
            ),
            Text(appendString())
          ],
        ),
      ),
    );
  }
}

这是我的第一个答案,所以我不太了解格式设置。


0
只需在构建器小部件下使用onPress,并将构建器上下文传递给ScaffoldMessenger。

0
Flutter在GitHub上的画廊显示该功能将进行升级,并且当他们再次将flutter主分支与更新后的分支合并时,它将得到更新。目前,您只能使用Scaffold或在键变量中编写手动代码后将其实现为关键字。

1
你好!能否添加到 Github 问题的链接? - Mol0ko

0
解决方案是将脚手架放在一个单独的小部件中,并将 snackbar 放在脚手架内,如下所示。
import 'package:flutter/material.dart';

void main() {
runApp(const MyApp());
}

class MyApp extends StatefulWidget {
const MyApp({super.key});

@override
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return MaterialApp(
    debugShowCheckedModeBanner: false,
    home: HomePage()
);
}
}
class HomePage extends StatelessWidget {
 const HomePage({super.key});

@override
Widget build(BuildContext context) {
return Scaffold(

  appBar: AppBar(
    title: Text("My App"),
    actions: [
      IconButton(
          onPressed: (){
            ScaffoldMessenger.of(context).showSnackBar(
                SnackBar(
                    content: Text('Snackbar')
                )
            );
          },
          icon: Icon(Icons.favorite)
      )
    ],
  ),
);

} }


-1
使用这个:
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: _content ,duration: Duration(milliseconds: 200),));

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接