Navigator的正確打開方式

引言

在使用Flutter進(jìn)行頁面間跳轉(zhuǎn)時(shí),Flutter官方給的建議是使用Navigator。Navigator也很友好的提供了push、pushNamedpop等靜態(tài)方法供我們選擇使用。這些接口的使用方法都不算難,但是我們會(huì)經(jīng)常碰到下面這個(gè)異常。

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.

翻譯過來的意思是路由跳轉(zhuǎn)功能所需的context沒有包含Navigator。路由跳轉(zhuǎn)功能所需的context對應(yīng)的widget必須是Navigator這個(gè)widget的子類。

究竟是啥意思呢?讓人看得是一頭霧水啊。沒有什么高深的知識是一個(gè)例子解決不了的,下面我們將通過一個(gè)例子來探究這個(gè)異常的前因后果。

一個(gè)例子

下面這個(gè)例子將通過點(diǎn)擊搜索??按鈕,實(shí)現(xiàn)跳轉(zhuǎn)到搜索頁的功能。

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("搜索頁"),
    );
  }
}
image

上面這個(gè)例子是有問題的,當(dāng)我們點(diǎn)擊首頁的搜索??按鈕時(shí),在控制臺(tái)上會(huì)打印出上面所提到的異常信息。

我們將上面的例子稍微做一下轉(zhuǎn)換。

import 'package:flutter/material.dart';

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

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

/// 將第一個(gè)例子中的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("搜索頁"),
    );
  }
}

和第一個(gè)例子相比較,我們將MaterialApphome屬性對應(yīng)的widget(Scaffold)單獨(dú)拎出來放到AppPage這個(gè)widget里面,然后讓MaterialApphome屬性引用改為AppPage。這個(gè)時(shí)候,讓我們再次點(diǎn)擊搜索??按鈕,可以看到從首頁正常的跳轉(zhuǎn)到了搜索頁面。

image

源碼分析

異常問題解決了,但是解決的有點(diǎn)糊里糊涂,有點(diǎn)莫名其妙。下面我們將從源碼入手,徹底搞清楚該問題的一個(gè)前因后果。

我們就從點(diǎn)擊搜索??按鈕這個(gè)動(dòng)作開始分析。點(diǎn)擊搜索??按鈕時(shí),調(diào)用了Navigatorpush方法。

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

push方法調(diào)用了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時(shí),就會(huì)拋出一個(gè)FlutterError的錯(cuò)誤。看一下錯(cuò)誤信息,這不正是我們要尋找的異常問題么?nullOk默認(rèn)是false的,那也就是說當(dāng)navigator為空時(shí),就會(huì)拋出該異常。

那我們就找找看,為什么navigator會(huì)為空。繼續(xù)往上看,navigator是由context執(zhí)行不同的方法返回的。由于我們并沒有主動(dòng)賦值rootNavigator,因此navigator是由context執(zhí)行ancestorStateOfType方法返回的。

BuildContext-1

上面所說的context是一個(gè)BuildContext類型對象,而BuildContext是一個(gè)接口類,其最終的實(shí)現(xiàn)類是Element。所以在BuildContext聲明的ancestorStateOfType接口方法,在Element中可以找到其實(shí)現(xiàn)方法。

在講解ElementancestorStateOfType方法前,我們要知道WidgetElement的對應(yīng)關(guān)系,可以參考一下這篇文章 Flutter之Widget層級介紹。在這里可以簡單的認(rèn)為每一個(gè)Widget對應(yīng)一個(gè)Element。

再結(jié)合上面第一個(gè)例子,context就是MyAppbuild方法中的context。MyApp是一個(gè)StatelessWidget,而StatelessWidget對應(yīng)著StatelessElement

在最初講BuildContext的時(shí)候談到,contextBuildContext類型,而其最終實(shí)現(xiàn)類是Element。所以,我們接著看ElementancestorStateOfType方法。

State ancestorStateOfType(TypeMatcher matcher) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    Element ancestor = _parent;
    while (ancestor != null) {
      if (ancestor is StatefulElement && matcher.check(ancestor.state)) /// 直到找到一個(gè)StatefuleElement對象并通過matcher的State校驗(yàn)
        break;
      ancestor = ancestor._parent;
    }
    final StatefulElement statefulAncestor = ancestor;
    return statefulAncestor?.state;
}

ancestorStateOfType做的事情并不復(fù)雜,主要是沿著其父類一直往上回溯,直到找到一個(gè)StatefulElement類型并且通過matcherState校驗(yàn)的一個(gè)Element對象,然后將該對象的State對象返回。

結(jié)合Navigatorof方法,這里的matcher對象為TypeMatcher<NavigatorState>()。

問題:那么當(dāng)前StatelessElement_parent是什么呢?這就要從入口方法main開始說起了。

main方法

我們知道m(xù)ain()方法是程序的入口方法。

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

main方法通過調(diào)用runApp方法接收一個(gè)widget

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

runApp方法中調(diào)用了attachRootWidget方法。這里的參數(shù)app就是MyApp這個(gè)widget。

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

attachRootWidget方法中又調(diào)用了RenderObjectToWidgetAdapterattachToRenderTree方法。這里的RenderObjectToWidgetAdapter實(shí)際上是一個(gè)Widget,而返回的_renderViewElementElement。也就是說這相當(dāng)于App的頂部Widget和其對應(yīng)的頂部Element。

注意第一次調(diào)用時(shí),attachToRenderTree方法的renderViewElement參數(shù)為null,而且rootWidget(MyApp)是作為RenderObjectToWidgetAdapter的子Widget傳遞進(jìn)去。

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,則通過調(diào)用createElement創(chuàng)建element對象。

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

element對象類型為RenderObjectToWidgetElement,然后調(diào)用了mount方法,將兩個(gè)空對象傳遞進(jìn)去。也就是說RenderObjectToWidgetElement對象的父Elementnull。記住這一點(diǎn),后面會(huì)用到這個(gè)結(jié)論。

說到這里,我們得出一個(gè)結(jié)論:

App的頂部Widget和其對應(yīng)的頂部Element分別是RenderObjectToWidgetAdapterRenderObjectToWidgetElement,它的子WidgetMyApp

也就是說,MyApp這個(gè)Widget對應(yīng)的Element,其父ElementRenderObjectToWidgetElement。這個(gè)結(jié)論回答了BuildContext-1這一小節(jié)最后提出的那個(gè)問題。

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方法這一小節(jié)的結(jié)論我們得知,由于當(dāng)前的ElementMyApp對應(yīng)的Element,那么_parent就是RenderObjectToWidgetElement,進(jìn)入while循環(huán),由于RenderObjectToWidgetElement并不是StatefulElement類型,則繼續(xù)找到RenderObjectToWidgetElement的父Element。從main方法這一小節(jié)的分析可知,RenderObjectToWidgetElement的父Elementnull,從而推出while循環(huán),繼而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這個(gè)條件,所以就拋出了FlutterError異常。

分析到了這里,我們算是回答了第一個(gè)例子為什么會(huì)拋出FlutterError異常的原因,接下來我們分析一下為什么修改后的例子不會(huì)拋出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默認(rèn)為false的情況下,為了保證不拋出FlutterError異常,必須保證navigator不為空。也就是說context.ancestorStateOfType必須返回一個(gè)NavigatorState類型的navigator

上面已經(jīng)分析了MyApp這個(gè)Widget對應(yīng)的Element,其父ElementRenderObjectToWidgetElement。

那么我們從MyApp這個(gè)Widget出發(fā),分析一下其子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

以此類推分析下去,得到下面一條樹干(前一個(gè)Widget是后一個(gè)Widget的父Widget):

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

而這里的AnimatedTheme就是上面MaterialAppbuild方法中定義的AnimatedTheme。那么它的子Widget(child屬性)就是WidgetsAppbuilder屬性傳遞進(jìn)來的。而builder屬性是在WidgetsApp對應(yīng)的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方法中調(diào)用了widget.builder屬性,我們重點(diǎn)關(guān)注第二個(gè)參數(shù),它是一個(gè)Navigator類型的Widget,正是這個(gè)參數(shù)傳遞過去并作為了AnimatedTheme的子Widget。結(jié)合上面Navigatorof方法邏輯,我們知道必須找到一個(gè)NavigatorState類型的對象。這里的Navigator就是一個(gè)StatefulWidget類型,并且對應(yīng)著一個(gè)NavigatorState類型對象。

如果我們繼續(xù)往下分析,就能看到這樣的一條完整樹干:

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

大家也可以通過調(diào)試的方法來驗(yàn)證上述的結(jié)論,如下圖所示。

image

image

image

由于這條樹干太長,因此只截取了部分??梢钥吹缴喜糠值捻敹耸?code>AppPage,下部分的底端是MyApp,而中間是Navigator。

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

所以由上述的分析得出如下結(jié)論:

如果在Widget中需要使用Navigator導(dǎo)航,則必須將該Widget必須作為MaterialApp的子Widget,并且context(實(shí)際上是Element)也必須是MaterialApp對應(yīng)的context的子context。

參考文章

Flutter | 深入理解BuildContext

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容