使用不包含Navigator的上下文请求Navigator操作

171

我正在尝试在一个 onTap 事件中启动一个新的屏幕,但是我得到了以下错误:

要求使用不包含导航器的上下文进行导航操作。

我用来导航的代码是:

onTap: () { Navigator.of(context).pushNamed('/settings'); },

我已经在我的应用程序中设置了一个路由,如下所示:

routes: <String, WidgetBuilder>{
    '/settings': (BuildContext context) => new SettingsPage(),
},

我尝试使用股票示例应用程序复制代码。我查看了Navigator和Route文档,但无法弄清如何使上下文包括Navigator。在onTap中使用的context是从传递到build方法中的参数引用的:

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

SettingsPage是以下类:

class SettingsPage extends Navigator {

Widget buildAppBar(BuildContext context) {
  return new AppBar(
    title: const Text('Settings')
  );
}

@override
Widget build(BuildContext context) {
  return new Scaffold(
    appBar: buildAppBar(context),
  );
 }
}

这个错误是由于在您的小部件树中插入了MaterialApp或WidgetApp小部件引起的。尝试在main方法中添加MaterialApp小部件,然后将其余代码放在Stateless或Stateful小部件中,问题将得到解决。 - Muhammad Afzal
处理这个问题的最佳方法之一是在另一个问题的回答中描述的:https://stackoverflow.com/a/73433546/5365164 - undefined
21个回答

363

简而言之: 将需要访问Navigator的小部件包装到一个Builder中,或将该子树提取到一个类中。并使用新的BuildContext来访问Navigator


这个错误与目标无关。它是因为你使用了一个没有包含导航器实例作为父级的上下文。

那我怎么创建导航器实例呢?

通常情况下,你可以在 Widget 树中插入一个 MaterialAppWidgetsApp 来实现。虽然你也可以手动使用 Navigator 直接创建,但不建议这样做。然后,这些 widget 的所有子代都可以使用 Navigator.of(context) 访问 NavigatorState 了。

等等,我已经有一个 MaterialApp/WidgetsApp 了!

这很可能是情况所在。但是当你使用的上下文是 MaterialApp/WidgetsApp 的父级时,仍然会发生此错误。

这是因为当你执行 Navigator.of(context) 时,它将从与所使用的 context 关联的 widget 开始,然后向上遍历 widget 树,直到它找到一个 Navigator 或者没有更多的 widget 可以迭代为止。

如果是第一种情况,那就没问题了。如果是第二种情况,就会抛出异常。

导航器操作请求的上下文中不包含导航器。
那么,我该如何修复它呢?
首先,让我们重现这个错误:
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Center(
        child: RaisedButton(
          child: Text("Foo"),
          onPressed: () => Navigator.pushNamed(context, "/"),
        ),
      ),
    );
  }
}

这个示例创建了一个按钮,当点击时会尝试转到'/',但实际上会抛出一个异常。

请注意,在这里

onPressed: () => Navigator.pushNamed(context, "/"),

我们使用了由MyAppbuild传递的context

问题是,MyApp实际上是MaterialApp的父级,因为它是实例化MaterialApp的小部件!因此,MyAppBuildContext没有MaterialApp作为父级。

为了解决这个问题,我们需要使用不同的context

在这种情况下,最简单的解决方案是引入一个新的小部件作为MaterialApp的子级,然后使用该小部件的上下文来进行Navigator调用。

有几种方法可以实现这一点。您可以将home提取到自定义类中:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: MyHome()
    );
  }
}

class MyHome extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Center(
      child: RaisedButton(
        child: Text("Foo"),
        onPressed: () => Navigator.pushNamed(context, "/"),
      ),
    );
  }
}

或者您可以使用Builder

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Builder(
        builder: (context) => Center(
          child: RaisedButton(
            child: Text("Foo"),
            onPressed: () => Navigator.pushNamed(context, "/"),
          ),
        ),
      ),
    );
  }
}

9
这个答案假设您想在主页上使用单个小部件。但是,这种解决方案不能扩展到使用路由器的情况,并且因此需要为每个路由实现某种基础小部件。例如,如果您需要在常用小部件中初始化监听器以使用Navigator,则此方法无法工作,因为没有通用小部件适用于所有路由。小部件会被复制,您的监听器也会被复制。 - Daniel Allen
非常感谢你的详细解释! - Alexandre Cavalcante

62

我曾经遇到过同样的问题。而我找到的解决方案非常简单:

void main() {
  runApp(
   MaterialApp(
    home: YourApp(),
    ),
  );
}

2
这是最简单的解决方案。直截了当! - vin shaba
3
在小部件树中,我们难道不应该只使用一次MatrialApp小部件吗? - Ahmad Khan
这对我有用。我对Flutter还不熟悉。 - undefined

41

确保你当前的父部件与MaterialApp不处于同一层级

错误的做法

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          centerTitle: true,
          title: Text('Title'),
        ),
        body: Center(
            child: Padding(
          padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
          child: RaisedButton(
              onPressed: () {
                //wrong way: use context in same level tree with MaterialApp
                Navigator.push(context,
                    MaterialPageRoute(builder: (context) => ScanScreen()));
              },
              child: const Text('SCAN')),
        )),
      ),
    );
  }
}

正确的方式

void main() => runApp(MaterialApp(
      title: "App",
      home: HomeScreen(),
    ));

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        centerTitle: true,
        title: Text('Title'),
      ),
      body: Center(
          child: Padding(
        padding: EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
        child: RaisedButton(
            onPressed: () {
            //right way: use context in below level tree with MaterialApp
              Navigator.push(context,
                  MaterialPageRoute(builder: (context) => ScanScreen()));
            },
            child: const Text('SCAN')),
      )),
    );
  }
}

1
最佳解释 - Ankit Chawla
如何传递多个参数并在另一个屏幕上获取/使用它们?请提供建议。谢谢。 - Kamlesh
@Kamlesh 只需通过您的小部件(类)的构造函数传递它。 - Mahmoud Abu Alheja
谢谢您的快速回复。您能否通过添加不同类型的2个参数来更新您的答案?非常感谢。 - Kamlesh
@Kamlesh,抱歉回复晚了。为了直接回答上面的问题,我不能编辑它。如果你仍然需要帮助,我可以联系你 :) - Mahmoud Abu Alheja

21

就像使用Scaffold一样,您可以使用GlobalKey。它不需要上下文。

final _navKey = GlobalKey<NavigatorState>();

void _navigateToLogin() {
  _navKey.currentState.popUntil((r) => r.isFirst);
  _navKey.currentState.pushReplacementNamed(LoginRoute.name);
}

@override
Widget build(BuildContext context) {
  return MaterialApp(
    navigatorKey: _navKey,
    ...
  );
}

这应该是被接受的答案。如果你想从MyApp外部推送新路由,那么这就是方法。 - Raveesh G S
我在做这个时遇到了一个错误:“如果未使用home、routes、onGenerateRoute或onUnknownRoute提供路由,则必须提供构建器属性的非空回调函数,并且其他与导航器相关的属性navigatorKey、initialRoute和navigatorObservers必须具有它们的初始值(分别为null、null和空列表)。" - SalahAdDin
如何传递多个参数并在另一个屏幕上获取/使用它们?请提供建议。谢谢。 - Kamlesh

14

我为Flutter应用程序的路由设置了这个简单的示例:

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: new MyHomePage(),
      routes: <String, WidgetBuilder>{
        '/settings': (BuildContext context) => new SettingsPage(),
      },
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('TestProject'),
      ),
      body: new Center(
        child: new FlatButton(
          child: const Text('Go to Settings'),
          onPressed: () => Navigator.of(context).pushNamed('/settings')
        )
      )
    );
  }
}

class SettingsPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        appBar: new AppBar(
          title: new Text('SettingsPage'),
        ),
        body: new Center(
            child: new Text('Settings')
        )
    );
  }
}

请注意,SettingsPage继承自StatelessWidget而不是Navigator。我无法重现您的错误。

这个例子对你构建应用有帮助吗?如果你需要任何其他帮助,请告诉我。


1
是的,这很有帮助!我在 MaterialApp 的构造函数中设置了 home: new Scaffold(appBar: new AppBar(title: new Text('GroupUp'),) 而不是创建另一个 Widget。(至少我认为这是问题所在) - juliusspencer
你在哪里使用了 onTap 方法? - Rainer Wittmann
4
也许你正在遇到 https://github.com/flutter/flutter/issues/15919 这个问题。 - Kira
如何传递多个参数并在另一个屏幕上获取/使用它们?请提供建议。谢谢。 - Kamlesh

12
你应该在 main.dart 文件中重写你的代码。
void main() => runApp(MyApp());

TO

(翻译:TO)
void main() {
  runApp(MaterialApp(
  title: 'Your title',
  home: MyApp(),));}

关键是将主页属性设置为你的第一页 这对我有用,希望能帮到未来的某个人


4

一种完整且经过测试的解决方案:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:my-app/view/main-view.dart';

class SplashView extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        home: Builder(
          builder: (context) => new _SplashContent(),
        ),
        routes: <String, WidgetBuilder>{
          '/main': (BuildContext context) => new MainView()}
    );
  }
}

class _SplashContent extends StatefulWidget{

  @override
  _SplashContentState createState() => new _SplashContentState();
}

class _SplashContentState extends State<_SplashContent>
    with SingleTickerProviderStateMixin {

  var _iconAnimationController;
  var _iconAnimation;

  startTimeout() async {
    var duration = const Duration(seconds: 3);
    return new Timer(duration, handleTimeout);
  }

  void handleTimeout() {
    Navigator.pushReplacementNamed(context, "/main");
  }

  @override
  void initState() {
    super.initState();

    _iconAnimationController = new AnimationController(
        vsync: this, duration: new Duration(milliseconds: 2000));

    _iconAnimation = new CurvedAnimation(
        parent: _iconAnimationController, curve: Curves.easeIn);
    _iconAnimation.addListener(() => this.setState(() {}));

    _iconAnimationController.forward();

    startTimeout();
  }

  @override
  Widget build(BuildContext context) {
    return new Center(
        child: new Image(
          image: new AssetImage("images/logo.png"),
          width: _iconAnimation.value * 100,
          height: _iconAnimation.value * 100,
        )
    );
  }
}

如何传递多个参数并在另一个屏幕上获取/使用它们?请提供建议。谢谢。 - Kamlesh

4
根据这篇评论,如果你的导航器位于Material上下文中,使用navigator push会出现错误。如果你创建一个新的小部件并将其分配给material app home,则导航器将起作用。 以下方法行不通
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text("Title"),
        ),
        body: new Center(child: new Text("Click Me")),
        floatingActionButton: new FloatingActionButton(
          child: new Icon(Icons.add),
          backgroundColor: Colors.orange,
          onPressed: () {
            print("Clicked");
            Navigator.push(
              context,
              new MaterialPageRoute(builder: (context) => new AddTaskScreen()),
            );
          },
        ),
      ),
    );
  }
}

这将会起作用

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        home: new HomeScreen());
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Title"),
      ),
      body: new Center(child: new Text("Click Me")),
      floatingActionButton: new FloatingActionButton(
        child: new Icon(Icons.add),
        backgroundColor: Colors.orange,
        onPressed: () {
          print("Clicked");
          Navigator.push(
            context,
            new MaterialPageRoute(builder: (context) => new AddTaskScreen()),
          );
        },
      ),
    );
  }
}

如何传递多个参数并在另一个屏幕上获取/使用它们?请提供建议。谢谢。 - Kamlesh

3

简单易懂

不要使用这种普通的代码

`runApp(BasicBankingSystem());`

使用MaterialApp进行包装

runApp(MaterialApp(home: BasicBankingSystem()));

你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

2

我曾经遇到同样的问题,并通过从MaterialApp中移除home并改用initialRoute来解决。

return MaterialApp(
      debugShowCheckedModeBanner: false,
      initialRoute: '/',
      routes: {
        '/': (context) => MyApp(),
        '/settings': (context) => SettingsPage(),
      },
    );


而且
onTap: () => {
               Navigator.pushNamed(context, "/settings")
                },


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