Flutter - Navigator

Navigator 在Flutter中是用来做路由管理的,简单的说就是负责页面之间跳转的工作。页面之间的跳转是通过Navigator和Route共同管理完成的。

  • Route 是页面的抽象,主要负责创建对应的界面,接收参数,响应 Navigator 打开和关闭;
  • Navigator 则会维护一个路由栈管理 Route,Route 打开即入栈,Route 关闭即出栈,还可以直接替换栈内的某一个 Route。
    根据是否需要提前注册页面标识符,Flutter 中的路由管理可以分为两种方式:
  • 基本路由:无需提前注册,在页面切换时需要自己构造页面实例。
  • 命名路由:需要提前注册页面标识符,在页面切换时通过标识符直接打开新的路由。
下面我分别看一两种路由的使用方式

基本路由

在Flutter中导航到一个新页面,我们需要创建一个MaterialPageRoute实例,调用Navigator.push方法,MaterialPageRoute 实例作为参数传入将新页面压入栈的顶部。
MaterialPageRoute 是一种路由模板,定义了路由创建及切换过度动画的相关配置,可以针对不同的平台实现与平台页面切换动画风格一致的路由切换动画。
基本路由使用代码如下:


class FirstScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      //打开页面
      onPressed: ()=> Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen()));
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      // 回退页面
      onPressed: ()=> Navigator.pop(context)
    );
  }
}

运行结果如下:


基本路由示例.gif

以上是最简单的路由使用方式,我们在开发中往往会涉及到页面之间传参的场景,从一个页面调到下一个页面传参可以通过属性或者构造函数的方式都可以实现,当页面返回到上一个页面在Flutter 中是如何传参的,请看下面代码:

class FirstPage extends StatefulWidget {

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

class _FirstPageState extends State<FirstPage> {
  var backData;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text("FirstPage")),
      body: Center(
        child: Column(
          children: [
            Text("${backData??'等待返回数据...'}"),
            ElevatedButton(
              child: Text("Next Page"),
              onPressed: () {
                _navigateSecondPage(context);
              },
            )
          ],
        ),
      ),
    );
  }
  void _navigateSecondPage(BuildContext context) async {
    // ignore: avoid_print
    print('执行了_navigateSecondPage');
    final result =
    await Navigator.push(context, MaterialPageRoute(builder: (context) {
      return SecondPage();
    }));
    // ignore: avoid_print
    print('FirstPage收到数据:$result');
    setState(() {
      backData = result;
    });
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text("SecondPage"),
      ),
      body: Center(
        child: ElevatedButton(
          child: const Text("Go Back!"),
          onPressed: () {
            _backCurrentPage(context);
          },
        ),
      ),
    );
  }

  //退出当前页面,返回到上一级页面
  void _backCurrentPage(BuildContext context) {
    print('执行了_backCurrentPage');

    ///只有执行了这个方法,上级页面才会收到返回的数据
    Navigator.pop(context, '我是来自SecondPage的数据');
  }
}

运行结果如下:


基本路由传参示例.gif

命名路由

基本路由使用方式相对简单灵活,适用于应用中页面不多的场景。而在应用中页面比较多的情况下,再使用基本路由方式,那么每次跳转到一个新的页面,我们都要手动创建 MaterialPageRoute 实例,初始化页面,然后调用 push 方法打开它,还是比较麻烦的。所以,Flutter 提供了另外一种方式来简化路由管理,即命名路由。我们给页面起一个名字,然后就可以直接通过页面名字打开它了。

要想通过名字来指定页面切换,我们必须先给应用程序 MaterialApp 提供一个页面名称映射规则,即路由表 routes,这样 Flutter 才知道名字与页面 Widget 的对应关系。

路由表实际上是一个 Map,其中 key 值对应页面名字,而 value 值则是一个 WidgetBuilder 回调函数,我们需要在这个函数中创建对应的页面。而一旦在路由表中定义好了页面名字,我们就可以使用 Navigator.pushNamed 来打开页面了。

下面的代码演示了命名路由的使用方法:在 MaterialApp 完成了页面的名字 second_page 及页面的初始化方法注册绑定,后续我们就可以在代码中以 second_page 这个名字打开页面了:

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

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routes: {
        '/page1':(context) => FirstPage(),
        '/page2':(context) => SecondPage(),
      },
      home: HomePage(),
    );
  }
}

class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('FirstPage'),
      ),
      body: Container(
        color: Colors.white,
        alignment: Alignment.center,
        child: ElevatedButton(
          onPressed: () {
            Navigator.pushNamed(context, '/page2');
          },
          child: const Text(
            'page1',
            style: TextStyle(fontSize: 30),
          ),
        ),
      ),
    );
  }
}

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('SecondPage')),
      body: Container(
        alignment: Alignment.center,
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Text(
            'page2',
            style: TextStyle(fontSize: 30),
          ),
        ),
      ),
    );
  }
}

运行结果如下:


命名路由.gif

我们可以看出命名路由使用起来很简单并不复杂,不过由于路由的注册和使用都采用字符串来标识,这就会带来一个隐患:如果我们打开了一个不存在的路由会怎么办?

也许你会想到,我们可以约定使用字符串常量去定义、使用路由,但我们无法避免通过接口数据下发的错误路由标识符场景。面对这种情况,无论是直接报错或是不响应错误路由,都不是一个用户体验良好的解决办法。更好的办法是,对用户进行友好的错误提示,比如跳转到一个统一的 NotFoundScreen 页面,也方便我们对这类错误进行统一收集、上报。在注册路由表时,Flutter 提供了 UnknownRoute 属性,我们可以对未知的路由标识符进行统一的页面跳转处理。下面的代码演示了如何注册错误路由处理。和基本路由的使用方法类似,我们只需要返回一个固定的页面即可。


MaterialApp(
    ...
    //注册路由
    routes:{
      "second_page":(context)=>SecondPage(),
    },
    //错误路由处理,统一返回UnknownPage
    onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),
);

//使用错误名字打开页面
Navigator.pushNamed(context,"unknown_page");

运行一下代码,可以看到,我们的应用不仅可以处理正确的页面路由标识,对错误的页面路由标识符也可以统一跳转到固定的错误处理页面了。

dc007d9b1313c88a22aa27b3e1f5a897.gif

命名路由的接收回传参数的方式同基本路由方式,但是push传参方式更加方便,如下演示代码:

//传参方式
Navigator.pushNamed(context, '/page2', arguments: {"key":"value"});
//取参方式
ModalRoute.of(context).settings.arguments;

以上我们分别介绍了Navigator 的基本路由和命名路由的使用方式,当然push 也好,pop 也好Flutter 为我们开发者提供了很多丰富的push 或pop 的相关API,例如pushAndRemoveUntil、popUntil 等方法可以根据实际的开发需求去选择。

源码探究

上面我们从基本使用上对Navigator有了基本的了解,接下来我们从源码实现上去探究一下Navigator是如何实现路由管理的。

Navigator是什么?下面截取了Navigator的构造方法,我们从下面的源码中可以看出Navigator就是一个StatefulWidget。
class Navigator extends StatefulWidget {
  /// Creates a widget that maintains a stack-based history of child widgets.
  ///
  /// The [onGenerateRoute], [pages], [onGenerateInitialRoutes],
  /// [transitionDelegate], [observers]  arguments must not be null.
  ///
  /// If the [pages] is not empty, the [onPopPage] must not be null.
  const Navigator({
    Key? key,
//Page 继承RouteSettings 记录了Route 的设置信息  (String? name, Object? arguments)
    this.pages = const <Page<dynamic>>[],
    this.onPopPage,
    this.initialRoute, // 导航栈中的第一个Route 的name,也就是根Route Name
    this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes,

//创建Route 的函数 传参是RouteSettings  
//Route<dynamic>? Function(RouteSettings settings)
    this.onGenerateRoute,

    this.onUnknownRoute,
    this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(),//页面切换过度动画
    this.reportsRouteUpdateToEngine = false,
    this.observers = const <NavigatorObserver>[],// Navigator 的监听对象
    this.restorationScopeId,
  })

static NavigatorState of(
    BuildContext context, {
    bool rootNavigator = false,
  }) {
    // Handles the case where the input context is a navigator element.
    NavigatorState? navigator;
    if (context is StatefulElement && context.state is NavigatorState) {
        navigator = context.state as NavigatorState;
    }
    if (rootNavigator) {
      ///从context 中 拿到StatefulWidget 的根 state,Navigator 本身就是一个StatefulWidget
      navigator = context.findRootAncestorStateOfType<NavigatorState>() ?? navigator;
    } else {
      ///从context 中 拿到最近的StatefulWidget 的 state,Navigator 本身就是一个StatefulWidget
      navigator = navigator ?? context.findAncestorStateOfType<NavigatorState>();
    }

    assert(() {
      if (navigator == null) {
        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!;
  }

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

@optionalTypeArgs
  Future<T?> pushNamed<T extends Object?>(
    String routeName, {
    Object? arguments,
  }) {
    return push<T>(_routeNamed<T>(routeName, arguments: arguments)!);
  }

@optionalTypeArgs
  static void pop<T extends Object?>(BuildContext context, [ T? result ]) {
    Navigator.of(context).pop<T>(result);
  }

static void popUntil(BuildContext context, RoutePredicate predicate) {
    Navigator.of(context).popUntil(predicate);
  }
}

上面的代码中只截取了部分源码,详细源码请自行去sdk中查看,经过我对源码的层层探究下面对push 的流程进行一下概括。

push流程

Navigator 调用上层的push 方法MaterialPageRoute作为参数传入,首先回去调用of方法获取到NavigatorState,然后NavigatorState 去调用自己真正的push方法,push方法里调用_pushEntry方法,会把MaterialPageRoute生成一个_RouteEntry对象传入_pushEntry方法里,里面主要做了三件事_history.add(entry); _flushHistoryUpdates();_afterNavigation(entry.route); 把RouteEntry 对象加入到_hidtory list 中;更新history 中entry 的信息;性能工具做性能优化处理。
其中核心的push处理是在_flushHistoryUpdates 方法中处理,这里贴出关键代码

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
    // Clean up the list, sending updates to the routes that changed. Notably,
    // we don't send the didChangePrevious/didChangeNext updates to those that
    // did not change at this point, because we're not yet sure exactly what the
    // routes will be at the end of the day (some might get disposed).
    int index = _history.length - 1;
    _RouteEntry? next;
    _RouteEntry? entry = _history[index];
    _RouteEntry? previous = index > 0 ? _history[index - 1] : null;
    bool canRemoveOrAdd = false; // Whether there is a fully opaque route on top to silently remove or add route underneath.
    Route<dynamic>? poppedRoute; // The route that should trigger didPopNext on the top active route.
    bool seenTopActiveRoute = false; // Whether we've seen the route that would get didPopNext.
    final List<_RouteEntry> toBeDisposed = <_RouteEntry>[];
    while (index >= 0) {
      switch (entry!.currentState) {
        case _RouteLifecycle.add:
          entry.handleAdd(
            navigator: this,
            previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
          );
          continue;
        case _RouteLifecycle.adding:
          if (canRemoveOrAdd || next == null) {
            entry.didAdd(
              navigator: this,
              isNewFirst: next == null,
            );
            continue;
          }
          break;
        case _RouteLifecycle.push:
        case _RouteLifecycle.pushReplace:
        case _RouteLifecycle.replace:
          entry.handlePush(
            navigator: this,
            previous: previous?.route,
            previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
            isNewFirst: next == null,//
          );
          if (entry.currentState == _RouteLifecycle.idle) {
            continue;
          }
          break;
        case _RouteLifecycle.pushing: // Will exit this state when animation completes.
          if (!seenTopActiveRoute && poppedRoute != null)
            entry.handleDidPopNext(poppedRoute);
          seenTopActiveRoute = true;
          break;
        case _RouteLifecycle.idle:
          if (!seenTopActiveRoute && poppedRoute != null)
            entry.handleDidPopNext(poppedRoute);
          seenTopActiveRoute = true;
          // This route is idle, so we are allowed to remove subsequent (earlier)
          // routes that are waiting to be removed silently:
          canRemoveOrAdd = true;
          break;
        case _RouteLifecycle.pop:
          if (!seenTopActiveRoute) {
            if (poppedRoute != null)
              entry.handleDidPopNext(poppedRoute);
            poppedRoute = entry.route;
          }
          entry.handlePop(
            navigator: this,
            previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
          );
          assert(entry.currentState == _RouteLifecycle.popping);
          canRemoveOrAdd = true;
          break;
        case _RouteLifecycle.popping:
          // Will exit this state when animation completes.
          break;
        case _RouteLifecycle.remove:
          if (!seenTopActiveRoute) {
            if (poppedRoute != null)
              entry.route.didPopNext(poppedRoute);
            poppedRoute = null;
          }
          entry.handleRemoval(
            navigator: this,
            previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
          );
          continue;
        case _RouteLifecycle.removing:
          if (!canRemoveOrAdd && next != null) {
            // We aren't allowed to remove this route yet.
            break;
          }
          entry.currentState = _RouteLifecycle.dispose;
          continue;
        case _RouteLifecycle.dispose:
          // Delay disposal until didChangeNext/didChangePrevious have been sent.
          toBeDisposed.add(_history.removeAt(index));
          entry = next;
          break;
        case _RouteLifecycle.disposed:
        case _RouteLifecycle.staging:
          assert(false);
          break;
      }
      index -= 1;
      next = entry;
      entry = previous;
      previous = index > 0 ? _history[index - 1] : null;
    }

    // Informs navigator observers about route changes.
    _flushObserverNotifications();

    // Now that the list is clean, send the didChangeNext/didChangePrevious
    // notifications.
    _flushRouteAnnouncement();

    // Announces route name changes.
    if (widget.reportsRouteUpdateToEngine) {
      final _RouteEntry? lastEntry = _history.cast<_RouteEntry?>().lastWhere(
        (_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e), orElse: () => null,
      );
      final String? routeName = lastEntry?.route.settings.name;
      if (routeName != _lastAnnouncedRouteName) {
        SystemNavigator.routeUpdated(
          routeName: routeName,
          previousRouteName: _lastAnnouncedRouteName,
        );
        _lastAnnouncedRouteName = routeName;
      }
    }

    // Lastly, removes the overlay entries of all marked entries and disposes
    // them.
    for (final _RouteEntry entry in toBeDisposed) {
      for (final OverlayEntry overlayEntry in entry.route.overlayEntries)
        overlayEntry.remove();
      entry.dispose();
    }
    if (rearrangeOverlay) {
      overlay?.rearrange(_allRouteOverlayEntries);
    }
    if (bucket != null) {
      _serializableHistory.update(_history);
    }
  }

从上面的源码里看出
entry.handlePush(
navigator: this,
previous: previous?.route,
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
isNewFirst: next == null,//
);
这个handlePush 里做了具体页面如何跳转状态如何改变的工作。其内部大致实现逻辑是这样的,entry 是flush 方法遍历history的当前的RouteEntry 对象,里面的route 会持有NavigatState,然后 currentState 设置成 _RouteLifecycle.pushing 状态,之后再去调用navigator._flushHistoryUpdates(); 内部会调用entry.handleDidPopNext(poppedRoute);
以上是一个push 的大致的流程描述,下面是我整理的一张方法调用的图

Navigator方法流程图.png

pop 流程也是一个类似的过程,比push 要简单一些,拿到NavigatorState后直接flushHistory,把route 做出栈操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,496评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,407评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,632评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,180评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,198评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,165评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,052评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,910评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,324评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,542评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,711评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,424评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,017评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,668评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,823评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,722评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,611评论 2 353

推荐阅读更多精彩内容