Navigator的正確打開方式

引言

在使用Flutter進行頁面間跳轉時,Flutter官方給的建議是使用NavigatorNavigator也很友好的提供了pushpushNamedpop等靜態方法供咱們選擇使用。這些接口的使用方法都不算難,可是咱們會常常碰到下面這個異常。bash

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

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.app

翻譯過來的意思是路由跳轉功能所需的context沒有包含Navigator。路由跳轉功能所需的context對應的widget必須是Navigator這個widget的子類。less

到底是啥意思呢?讓人看得是一頭霧水啊。沒有什麼高深的知識是一個例子解決不了的,下面咱們將經過一個例子來探究這個異常的來龍去脈。ide

一個例子

下面這個例子將經過點擊搜索🔍按鈕,實現跳轉到搜索頁的功能。源碼分析

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("搜索頁"),
    );
  }
}
複製代碼

上面這個例子是有問題的,當咱們點擊首頁的搜索🔍按鈕時,在控制檯上會打印出上面所提到的異常信息。post

咱們將上面的例子稍微作一下轉換。ui

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。這個時候,讓咱們再次點擊搜索🔍按鈕,能夠看到從首頁正常的跳轉到了搜索頁面。 this

源碼分析

異常問題解決了,可是解決的有點糊里糊塗,有點莫名其妙。下面咱們將從源碼入手,完全搞清楚該問題的一個來龍去脈。spa

咱們就從點擊搜索🔍按鈕這個動做開始分析。點擊搜索🔍按鈕時,調用了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

相關文章
相關標籤/搜索