Navigator的正确打开方式

引言

在使用Flutter进行页面间跳转时,Flutter官方给的建议是使用NavigatorNavigator也很友好的提供了pushpushNamedpop等静态方法供我们选择使用。这些接口的使用方法都不算难,但是我们会经常碰到下面这个异常。

Navigator operation requested with a context that does not include a Navigator.

The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.

翻译过来的意思是路由跳转功能所需的context没有包含Navigator。路由跳转功能所需的context对应的widget必须是Navigator这个widget的子类。

究竟是啥意思呢?让人看得是一头雾水啊。没有什么高深的知识是一个例子解决不了的,下面我们将通过一个例子来探究这个异常的前因后果。

一个例子

下面这个例子将通过点击搜索🔍按钮,实现跳转到搜索页的功能。

import'package:flutter/material.dart';

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

/// 首页
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(    /// Scaffold start
        body: Center(
          child: IconButton(
            icon: Icon(
          	 Icons.search,
        	),
            onPressed: () {
              Navigator.push(context, MaterialPageRoute(builder: (context) {
                return SearchPage();
              }));
            },
          )
        ),
      ),   /// Scaffold end
    );
  }
}

/// 搜索页
class SearchPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("搜索"),
      ),
      body: Text("搜索页"),
    );
  }
}

上面这个例子是有问题的,当我们点击首页的搜索🔍按钮时,在控制台上会打印出上面所提到的异常信息。

我们将上面的例子稍微做一下转换。

import'package:flutter/material.dart';

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

/// 首页
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: AppPage(),
    );
  }
}

/// 将第一个例子中的Scaffold包裹在AppPage里面
class AppPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
          child: IconButton(
            icon: Icon(
              Icons.search,
            ),
            onPressed: () {
              Navigator.push(context, MaterialPageRoute(builder: (context) {
                return SearchPage();
              }));
            },
          )
      ),
    );
  }
}

/// 搜索页
class SearchPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("搜索"),
      ),
      body: Text("搜索页"),
    );
  }
}

和第一个例子相比较,我们将MaterialApphome属性对应的widget(Scaffold)单独拎出来放到AppPage这个widget里面,然后让MaterialApphome属性引用改为AppPage。这个时候,让我们再次点击搜索🔍按钮,可以看到从首页正常的跳转到了搜索页面。

源码分析

异常问题解决了,但是解决的有点糊里糊涂,有点莫名其妙。下面我们将从源码入手,彻底搞清楚该问题的一个前因后果。

我们就从点击搜索🔍按钮这个动作开始分析。点击搜索🔍按钮时,调用了Navigatorpush方法。

static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {return Navigator.of(context).push(route);
}

push方法调用了Navigatorof方法。

static NavigatorState of(
  BuildContext context, {
  bool rootNavigator =false,
  bool nullOk = false,
}) {
  final NavigatorState navigator = rootNavigator
      ? context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>())
      : context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
  assert(() {
    if (navigator == null && !nullOk) {
      throw FlutterError(
        'Navigator operation requested with a context that does not include a Navigator.\n'
        'The context used to push or pop routes from the Navigator must be that of a '
        'widget that is a descendant of a Navigator widget.'
      );
    }
    return true;
  }());
  return navigator;
}

of方法判断navigator为空,而且nullOkfalse时,就会抛出一个FlutterError的错误。看一下错误信息,这不正是我们要寻找的异常问题么?nullOk默认是false的,那也就是说当navigator为空时,就会抛出该异常。

那我们就找找看,为什么navigator会为空。继续往上看,navigator是由context执行不同的方法返回的。由于我们并没有主动赋值rootNavigator,因此navigator是由context执行ancestorStateOfType方法返回的。

BuildContext-1

上面所说的context是一个BuildContext类型对象,而BuildContext是一个接口类,其最终的实现类是Element。所以在BuildContext声明的ancestorStateOfType接口方法,在Element中可以找到其实现方法。

在讲解ElementancestorStateOfType方法前,我们要知道WidgetElement的对应关系,可以参考一下这篇文章 Flutter之Widget层级介绍。在这里可以简单的认为每一个Widget对应一个Element

再结合上面第一个例子,context就是MyAppbuild方法中的contextMyApp是一个StatelessWidget,而StatelessWidget对应着StatelessElement

在最初讲BuildContext的时候谈到,contextBuildContext类型,而其最终实现类是Element。所以,我们接着看ElementancestorStateOfType方法。

State ancestorStateOfType(TypeMatcher matcher) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    Element ancestor = _parent;while (ancestor != null) {
      if (ancestor is StatefulElement && matcher.check(ancestor.state)) /// 直到找到一个StatefuleElement对象并通过matcher的State校验
        break;
      ancestor = ancestor._parent;
    }
    final StatefulElement statefulAncestor = ancestor;
    return statefulAncestor?.state;
}

ancestorStateOfType做的事情并不复杂,主要是沿着其父类一直往上回溯,直到找到一个StatefulElement类型并且通过matcherState校验的一个Element对象,然后将该对象的State对象返回。

结合Navigatorof方法,这里的matcher对象为TypeMatcher<NavigatorState>()

问题:那么当前StatelessElement_parent是什么呢?这就要从入口方法main开始说起了。

main方法

我们知道main()方法是程序的入口方法。

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

main方法通过调用runApp方法接收一个widget

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..attachRootWidget(app)
    ..scheduleWarmUpFrame();
}

runApp方法中调用了attachRootWidget方法。这里的参数app就是MyApp这个widget

void attachRootWidget(Widget rootWidget) {
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription:'[root]',
      child: rootWidget, ///这里的rootWidget是MyApp
    ).attachToRenderTree(buildOwner, renderViewElement);
}

attachRootWidget方法中又调用了RenderObjectToWidgetAdapterattachToRenderTree方法。这里的RenderObjectToWidgetAdapter实际上是一个Widget,而返回的_renderViewElementElement。也就是说这相当于App的顶部Widget和其对应的顶部Element

注意第一次调用时,attachToRenderTree方法的renderViewElement参数为null,而且rootWidget(MyApp)是作为RenderObjectToWidgetAdapter的子Widget传递进去。

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {if (element == null) {
      owner.lockState(() {
        element = createElement();
        assert(element != null);
        element.assignOwner(owner);
      });
      owner.buildScope(element, () {
        element.mount(null, null);
      });
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element;
}

elementnull,则通过调用createElement创建element对象。

RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);

element对象类型为RenderObjectToWidgetElement,然后调用了mount方法,将两个空对象传递进去。也就是说RenderObjectToWidgetElement对象的父Elementnull。记住这一点,后面会用到这个结论。

说到这里,我们得出一个结论:

App的顶部Widget和其对应的顶部Element分别是RenderObjectToWidgetAdapterRenderObjectToWidgetElement,它的子WidgetMyApp

也就是说,MyApp这个Widget对应的Element,其父ElementRenderObjectToWidgetElement。这个结论回答了BuildContext-1这一小节最后提出的那个问题。

BuildContext-2

让我们再次回到BuildContextancestorStateOfType方法,也就是ElementancestorStateOfType方法。

State ancestorStateOfType(TypeMatcher matcher) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    Element ancestor = _parent;while (ancestor != null) {
      if (ancestor is StatefulElement && matcher.check(ancestor.state))
        break;
      ancestor = ancestor._parent;
    }
    final StatefulElement statefulAncestor = ancestor;
    return statefulAncestor?.state;
}

main方法这一小节的结论我们得知,由于当前的ElementMyApp对应的Element,那么_parent就是RenderObjectToWidgetElement,进入while循环,由于RenderObjectToWidgetElement并不是StatefulElement类型,则继续找到RenderObjectToWidgetElement的父Element。从main方法这一小节的分析可知,RenderObjectToWidgetElement的父Elementnull,从而推出while循环,继而ancestorStateOfType返回null

也就是说Navigatorof方法中的navigatornull

static NavigatorState of(
  BuildContext context, {
  bool rootNavigator =false,
  bool nullOk = false,
}) {
  final NavigatorState navigator = rootNavigator
      ? context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>())
      : context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
  assert(() {
    if (navigator == null && !nullOk) {
      throw FlutterError(
        'Navigator operation requested with a context that does not include a Navigator.\n'
        'The context used to push or pop routes from the Navigator must be that of a '
        'widget that is a descendant of a Navigator widget.'
      );
    }
    return true;
  }());
  return navigator;
}

这样便满足了navigator == null && !nullOk这个条件,所以就抛出了FlutterError异常。

分析到了这里,我们算是回答了第一个例子为什么会抛出FlutterError异常的原因,接下来我们分析一下为什么修改后的例子不会抛出FluterError异常。

Navigator的正确打开方式

static NavigatorState of(
  BuildContext context, {
  bool rootNavigator =false,
  bool nullOk = false,
}) {
  final NavigatorState navigator = rootNavigator
      ? context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>())
      : context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
  assert(() {
    if (navigator == null && !nullOk) {
      throw FlutterError(
        'Navigator operation requested with a context that does not include a Navigator.\n'
        'The context used to push or pop routes from the Navigator must be that of a '
        'widget that is a descendant of a Navigator widget.'
      );
    }
    return true;
  }());
  return navigator;
}

在上面Navigatorof方法中,我们了解到在nullOk默认为false的情况下,为了保证不抛出FlutterError异常,必须保证navigator不为空。也就是说context.ancestorStateOfType必须返回一个NavigatorState类型的navigator

上面已经分析了MyApp这个Widget对应的Element,其父ElementRenderObjectToWidgetElement

那么我们从MyApp这个Widget出发,分析一下其子Widget树。

从修改后的例子可以看出,MyApp的子WidgetMaterialApp。而MaterialApp的子WidgetMaterialAppbuild方法决定。

Widget build(BuildContext context) {
    Widget result = WidgetsApp(
      key: GlobalObjectKey(this),
      navigatorKey: widget.navigatorKey,
      navigatorObservers: _navigatorObservers,
        pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) =>
            MaterialPageRoute<T>(settings: settings, builder: builder),
      home: widget.home,
      routes: widget.routes,
      initialRoute: widget.initialRoute,
      onGenerateRoute: widget.onGenerateRoute,
      onUnknownRoute: widget.onUnknownRoute,
      builder: (BuildContext context, Widget child) {
        // Use a light theme, dark theme, or fallback theme.
        ThemeData theme;
        final ui.Brightness platformBrightness = MediaQuery.platformBrightnessOf(context);if (platformBrightness == ui.Brightness.dark && widget.darkTheme != null) {
          theme = widget.darkTheme;
        } else if (widget.theme != null) {
          theme = widget.theme;
        } else {
          theme = ThemeData.fallback();
        }

        return AnimatedTheme(
          data: theme,
          isMaterialAppTheme: true,
          child: widget.builder != null
              ? Builder(
                  builder: (BuildContext context) {
                    // Why are we surrounding a builder with a builder?
                    //
                    // The widget.builder may contain code that invokes
                    // Theme.of(), which should return the theme we selected
                    // above in AnimatedTheme. However, if we invoke
                    // widget.builder() directly as the child of AnimatedTheme
                    // then there is no Context separating them, and the
                    // widget.builder() will not find the theme. Therefore, we
                    // surround widget.builder with yet another builder so that
                    // a context separates them and Theme.of() correctly
                    // resolves to the theme we passed to AnimatedTheme.
                    return widget.builder(context, child);
                  },
                )
              : child,
        );
      },
      title: widget.title,
      onGenerateTitle: widget.onGenerateTitle,
      textStyle: _errorTextStyle,
      // The color property is always pulled from the light theme, even if dark
      // mode is activated. This was done to simplify the technical details
      // of switching themes and it was deemed acceptable because this color
      // property is only used on old Android OSes to color the app bar in
      // Android's switcher UI.
      //
      // blue is the primary color of the default theme
      color: widget.color ?? widget.theme?.primaryColor ?? Colors.blue,
      locale: widget.locale,
      localizationsDelegates: _localizationsDelegates,
      localeResolutionCallback: widget.localeResolutionCallback,
      localeListResolutionCallback: widget.localeListResolutionCallback,
      supportedLocales: widget.supportedLocales,
      showPerformanceOverlay: widget.showPerformanceOverlay,
      checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
      checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
      showSemanticsDebugger: widget.showSemanticsDebugger,
      debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
      inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) {
        return FloatingActionButton(
          child: const Icon(Icons.search),
          onPressed: onPressed,
          mini: true,
        );
      },
    );

    assert(() {
      if (widget.debugShowMaterialGrid) {
        result = GridPaper(
          color: const Color(0xE0F9BBE0),
          interval: 8.0,
          divisions: 2,
          subdivisions: 1,
          child: result,
        );
      }
      return true;
    }());

    return ScrollConfiguration(
      behavior: _MaterialScrollBehavior(),
      child: result,
    );
}

直接看到最后的return,返回了ScrollConfiguration。也就是说MaterialApp的子WidgetScrollConfiguration。而ScrollConfigurationchild赋值为result对象,这里的resultWidgetsApp,从而得到ScrollConfiguration的子WidgetWidgetsApp

以此类推分析下去,得到下面一条树干(前一个Widget是后一个Widget的父Widget):

MyApp->MaterialApp->ScrollConfiguration->WidgetsApp->DefaultfocusTraversal->MediaQuery->Localizations->Builder->Title->AnimatedTheme

而这里的AnimatedTheme就是上面MaterialAppbuild方法中定义的AnimatedTheme。那么它的子Widget(child属性)就是WidgetsAppbuilder属性传递进来的。而builder属性是在WidgetsApp对应的WidgetsAppStatebuild方法用到。

Widget build(BuildContext context) {
    Widget navigator;if (_navigator != null) {
      navigator = Navigator(
        key: _navigator,
        // If window.defaultRouteName isn't '/', we should assume it was set
        // intentionally via `setInitialRoute`, and should override whatever
        // is in [widget.initialRoute].
        initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName
            ? WidgetsBinding.instance.window.defaultRouteName
            : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
        onGenerateRoute: _onGenerateRoute,
        onUnknownRoute: _onUnknownRoute,
        observers: widget.navigatorObservers,
      );
    }

    Widget result;
    if (widget.builder != null) {
      result = Builder(
        builder: (BuildContext context) {
          return widget.builder(context, navigator);
        },
      );
    } else {
      assert(navigator != null);
      result = navigator;
    }

    ...省略

    return DefaultFocusTraversal(
      policy: ReadingOrderTraversalPolicy(),
      child: MediaQuery(
        data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
        child: Localizations(
          locale: appLocale,
          delegates: _localizationsDelegates.toList(),
          child: title,
        ),
      ),
    );
}

可以看到,在WidgetsAppStatebuild方法中调用了widget.builder属性,我们重点关注第二个参数,它是一个Navigator类型的Widget,正是这个参数传递过去并作为了AnimatedTheme的子Widget。结合上面Navigatorof方法逻辑,我们知道必须找到一个NavigatorState类型的对象。这里的Navigator就是一个StatefulWidget类型,并且对应着一个NavigatorState类型对象。

如果我们继续往下分析,就能看到这样的一条完整树干:

MyApp->MaterialApp->ScrollConfiguration->WidgetsApp->DefaultfocusTraversal->MediaQuery->Localizations->Builder->Title->AnimatedTheme->Navigator->......->AppPage

大家也可以通过调试的方法来验证上述的结论,如下图所示。

由于这条树干太长,因此只截取了部分。可以看到上部分的顶端是AppPage,下部分的底端是MyApp,而中间是Navigator

由于MaterialApp的子Widget必定包含Navigator,而MaterialApphome属性返回的Widget必定是Navigator的子Widget

所以由上述的分析得出如下结论:

如果在Widget中需要使用Navigator导航,则必须将该Widget必须作为MaterialApp的子Widget,并且context(实际上是Element)也必须是MaterialApp对应的context的子context

参考文章

Flutter | 深入理解BuildContext

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值