flutter源码解析之路由管理

461 阅读17分钟

Flutter 的路由系统是用于管理应用中的页面导航和视图的机制。它支持多种导航方式和路由管理方式,包括基本的 Navigator 类、显式声明的路由表、命名路由、以及更复杂的自定义路由等。

MaterialApp

MaterialApp 是 flutter 提供的APP入口类。本章, 我们重点关注跟路由相关的内容。 涉及到路由定义, 页面跳转, 路由传参, 路由监听。

MaterialApp MaterialApp({
  Key? key,
  GlobalKey<NavigatorState>? navigatorKey,    // 导航键
  GlobalKey<ScaffoldMessengerState>? scaffoldMessengerKey,    //主要是管理 Scaffolds
  Widget? home,    // 应用程序默认路由的小部件,用来定义当前应用打开的时候,所显示的界面
  Map<String, Widget Function(BuildContext)> routes = const <String, WidgetBuilder>{},    // 应用程序的顶级路由表
  String? initialRoute,    // 如果构建了导航器,则显示的第一个路由的名称
  Route<dynamic>? Function(RouteSettings)? onGenerateRoute,    // 应用程序导航到指定路由时使用的路由生成器回调
  List<Route<dynamic>> Function(String)? onGenerateInitialRoutes,    //生成初始化路由
  Route<dynamic>? Function(RouteSettings)? onUnknownRoute,    // 当 onGenerateRoute 无法生成路由(initialRoute除外)时调用
  List<NavigatorObserver> navigatorObservers = const <NavigatorObserver>[],    // 为该应用程序创建的导航器的观察者列表
  Widget Function(BuildContext, Widget?)? builder,    // 应用程序的顶级路由表
  String title = '',    // 设备用于为用户识别应用程序的单行描述
  String Function(BuildContext)? onGenerateTitle,    // 如果非空,则调用此回调函数来生成应用程序的标题字符串,否则使用标题。
  Color? color,    // 在操作系统界面中应用程序使用的主色。
  ThemeData? theme,    // 应用程序小部件使用的颜色。
  ThemeData? darkTheme,    //暗黑模式主题颜色
  ThemeData? highContrastTheme,    //系统请求“高对比度”使用的主题
  ThemeData? highContrastDarkTheme,    //系统请求“高对比度”暗黑模式下使用的主题颜色
  ThemeMode? themeMode = ThemeMode.system,    //使用哪种模式的主题(默认跟随系统)
  Duration themeAnimationDuration = kThemeAnimationDuration,    //主体动画持续时间
  Curve themeAnimationCurve = Curves.linear,    //主体动画曲线
  Locale? locale,    // 此应用程序本地化小部件的初始区域设置基于此值。
  Iterable<LocalizationsDelegate<dynamic>>? localizationsDelegates,    // 这个应用程序本地化小部件的委托。
  Locale? Function(List<Locale>?, Iterable<Locale>)? localeListResolutionCallback,  // 这个回调负责在应用程序启动时以及用户更改设备的区域设置时选择应用程序的区域设置。
  Locale? Function(Locale?, Iterable<Locale>)? localeResolutionCallback,    //监听系统语言切换事件
  Iterable<Locale> supportedLocales = const <Locale>[Locale('en', 'US')],  // 此应用程序已本地化的地区列表
  bool debugShowMaterialGrid = false,  // 打开绘制基线网格材质应用程序的网格纸覆盖
  bool showPerformanceOverlay = false,  // 打开性能叠加
  bool checkerboardRasterCacheImages = false,  // 打开栅格缓存图像的棋盘格
  bool checkerboardOffscreenLayers = false,    // 打开渲染到屏幕外位图的图层的棋盘格
  bool showSemanticsDebugger = false,    // 打开显示框架报告的可访问性信息的覆盖
  bool debugShowCheckedModeBanner = true,      // 在选中模式下打开一个小的“DEBUG”横幅,表示应用程序处于选中模式
  Map<ShortcutActivator, Intent>? shortcuts,    //应用程序意图的键盘快捷键的默认映射。
  Map<Type, Action<Intent>>? actions,    //包含和定义用户操作的映射
  String? restorationScopeId,    //应用程序状态恢复的标识符
  ScrollBehavior? scrollBehavior,    //统一滚动行为设置,设置后子组件将返回对应的滚动行为
  bool useInheritedMediaQuery = false,    //如果为true,则将使用继承的 MediaQuery。如果它不可用或者是错误的,那么将从窗口构建一个。默认为false。
})

命名路由routes

所谓“命名路由”(Named Route)即有名字的路由,我们可以先给路由起一个名字,然后就可以通过路由名字直接打开新的路由了,这为路由管理带来了一种直观、简单的方式。

1. 路由表

要想使用命名路由,我们必须先提供并注册一个路由表(routing table),这样应用程序才知道哪个名字与哪个路由组件相对应。其实注册路由表就是给路由起名字,路由表的定义如下:

Map<String, WidgetBuilder> routes;

它是一个Map,key为路由的名字,是个字符串;value是个builder回调函数,用于生成相应的路由widget。我们在通过路由名字打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder回调函数,然后调用该回调函数生成路由widget并返回。

2. 注册路由表

路由表的注册方式很简单,在如下示例中,在MyApp类的build方法中找到MaterialApp,添加routes属性,代码如下:

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
      routes: {
        /// 每次跳转路由时, flutter源码会检查routes表。routes表查出来的是null, 会再检查onGenerateRoute
        '/home': (context) => Container(),
        '/tip2': (context) {
          /// 命名路由参数传递。第一种方式, 是在这里。第二种种方式,是在TipRoute的build方法里
          /// https://book.flutterchina.club/chapter2/flutter_router.html#_2-4-5-%E5%91%BD%E5%90%8D%E8%B7%AF%E7%94%B1
          return TipRoute(text: ModalRoute.of(context)!.settings.arguments?.toString() ?? '');
        },
      },
    );
  }
}

每次跳转路由时, flutter源码会检查routes表。routes表查出来的是null, 会再检查onGenerateRoute

3. 通过路由名打开新路由页

要通过路由名称来打开新路由,可以使用Navigator 的pushNamed方法:

Future pushNamed(BuildContext context, String routeName,{Object arguments})

Navigator 除了pushNamed方法,还有pushReplacementNamed等其他管理命名路由的方法,读者可以自行查看API文档。接下来我们通过路由名来打开新的路由页,修改TextButtononPressed回调代码,改为:

onPressed: () {
  Navigator.pushNamed(context, "new_page");
  //Navigator.push(context,
  //  MaterialPageRoute(builder: (context) {
  //  return NewRoute();
  //}));  
},

4. 命名路由参数传递

在Flutter最初的版本中,命名路由是不能传递参数的,后来才支持了参数;下面展示命名路由如何传递并获取路由参数:

我们先注册一个路由:

routes:{
   "new_page":(context) => TipRoute(),
  } ,

在路由页通过RouteSetting对象获取路由参数:

class TipRoute extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    //获取路由参数  
    var args=ModalRoute.of(context).settings.arguments;
    //...省略无关代码
  }
}

在打开路由时传递参数

Navigator.of(context).pushNamed("new_page", arguments: "hi");

5. 适配

假设我们也想将上面路由传参示例中的TipRoute路由页注册到路由表中,以便也可以通过路由名来打开它。但是,由于TipRoute接受一个text 参数,我们如何在不改变TipRoute源码的前提下适配这种情况?其实很简单:

MaterialApp(
  ... //省略无关代码
  routes: {
   "tip2": (context){
     return TipRoute(text: ModalRoute.of(context)!.settings.arguments);
   },
 }, 
);

路由生成钩子onGenerateRoute

假设我们要开发一个电商App,当用户没有登录时可以看店铺、商品等信息,但交易记录、购物车、用户个人信息等页面需要登录后才能看。为了实现上述功能,我们需要在打开每一个路由页前判断用户登录状态!如果每次打开路由前我们都需要去判断一下将会非常麻烦,那有什么更好的办法吗?答案是有!

MaterialApp有一个onGenerateRoute属性,它在打开命名路由时可能会被调用,之所以说可能,是因为当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。onGenerateRoute回调签名如下:

Route<dynamic> Function(RouteSettings settings)

有了onGenerateRoute回调,要实现上面控制页面权限的功能就非常容易:我们放弃使用路由表,取而代之的是提供一个onGenerateRoute回调,然后在该回调中进行统一的权限控制,如:

MaterialApp(
  ... //省略无关代码
  onGenerateRoute:(RouteSettings settings){
	  return MaterialPageRoute(builder: (context){
		   String routeName = settings.name;
       // 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
       // 引导用户登录;其他情况则正常打开路由。
     }
   );
  }
);

注意,onGenerateRoute 只会对命名路由生效。

WidgetsApp

WidgetsApp(
  key: GlobalObjectKey(this),
  navigatorKey: widget.navigatorKey,
  navigatorObservers: widget.navigatorObservers!,
  pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
    return MaterialPageRoute<T>(settings: settings, builder: builder);
  },
  home: widget.home,
  routes: widget.routes!,
  initialRoute: widget.initialRoute,
  onGenerateRoute: widget.onGenerateRoute,
  onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
  onUnknownRoute: widget.onUnknownRoute,
  onNavigationNotification: widget.onNavigationNotification,
  builder: _materialBuilder,
  title: widget.title,
  onGenerateTitle: widget.onGenerateTitle,
  textStyle: _errorTextStyle,
  color: materialColor,
  locale: widget.locale,
  localizationsDelegates: _localizationsDelegates,
  localeResolutionCallback: widget.localeResolutionCallback,
  localeListResolutionCallback: widget.localeListResolutionCallback,
  supportedLocales: widget.supportedLocales,
  showPerformanceOverlay: widget.showPerformanceOverlay,
  showSemanticsDebugger: widget.showSemanticsDebugger,
  debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
  inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
  shortcuts: widget.shortcuts,
  actions: widget.actions,
  restorationScopeId: widget.restorationScopeId,
);

根路由initialRoute和home页面

/// {@template flutter.widgets.widgetsApp.home}
/// The widget for the default route of the app ([Navigator.defaultRouteName],
/// which is `/`).
///
/// This is the route that is displayed first when the application is started
/// normally, unless [initialRoute] is specified. It's also the route that's
/// displayed if the [initialRoute] can't be displayed.
///
/// To be able to directly call [Theme.of], [MediaQuery.of], etc, in the code
/// that sets the [home] argument in the constructor, you can use a [Builder]
/// widget to get a [BuildContext].
///
/// If [home] is specified, then [routes] must not include an entry for `/`,
/// as [home] takes its place.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [builder] must not be null.
///
/// The difference between using [home] and using [builder] is that the [home]
/// subtree is inserted into the application below a [Navigator] (and thus
/// below an [Overlay], which [Navigator] uses). With [home], therefore,
/// dialog boxes will work automatically, the [routes] table will be used, and
/// APIs such as [Navigator.push] and [Navigator.pop] will work as expected.
/// In contrast, the widget returned from [builder] is inserted _above_ the
/// app's [Navigator] (if any).
/// {@endtemplate}
///
/// If this property is set, the [pageRouteBuilder] property must also be set
/// so that the default route handler will know what kind of [PageRoute]s to
/// build.
final Widget? home;
  • home 属性定义了应用程序的默认路由(/)的组件。这是应用程序启动时首先显示的页面。

  • 如果没有指定 initialRoutehome 就会成为启动时显示的第一个页面。如果 initialRoute 无法显示(例如指定的路由不存在),也会显示 home 页面。

  • 如果指定了 home,那么 routes 中不应该包含 / 路由,因为 home 会取代它。

不指定home,会怎样?

1. 指定了 initialRouteroutes

如果没有指定 home,但指定了 initialRouteroutes,Flutter 会根据这些配置来启动应用程序。

  • initialRoute:

    • 如果设置了 initialRoute,Flutter 会将这个路由作为应用的启动路由。应用会从 initialRoute 对应的页面开始显示。
  • routes:

    • 如果 routes 中包含了 initialRoute 对应的路由,Flutter 会加载这个路由。
    • 如果没有设置 initialRoute,Flutter 会将 routes 中的根路径 / 作为启动路由并加载对应的页面。
MaterialApp(
  initialRoute: '/login',
  routes: {
    '/': (context) => HomePage(),
    '/login': (context) => LoginPage(),
  },
);

2. 使用了 onGenerateRoute

如果没有指定 home,但配置了 onGenerateRoute 回调,Flutter 会在启动时调用 onGenerateRoute 来生成初始路由。

MaterialApp(
  onGenerateRoute: (settings) {
    if (settings.name == '/') {
      return MaterialPageRoute(builder: (context) => HomePage());
    }
    // 处理其他路由
    return MaterialPageRoute(builder: (context) => UnknownPage());
  },
);

在这个例子中,onGenerateRoute 回调会处理所有路由请求,包括初始路由。如果没有找到对应的路由名称,可以返回一个默认的页面(如 UnknownPage)。

3. 使用了 builder

如果没有指定 homeinitialRouteroutes,而是指定了 builder,Flutter 会构建并显示 builder 返回的组件。这个组件将被插入到应用的 Navigator 之上。

MaterialApp(
  builder: (context, child) {
    return MyCustomPage();
  },
);

在这个例子中,MyCustomPage 将是应用启动时显示的页面。

4. 以上配置都没有设置

如果既没有指定 home,也没有指定 initialRouteroutesonGenerateRoutebuilder,应用将无法正常启动,因为 Flutter 不知道应该加载哪个页面。这种情况下,Flutter 可能会抛出异常,提示缺少必需的启动配置。

总结

  • 如果没有指定 home,Flutter 将优先使用 initialRouteroutes 配置来确定启动页面。
  • 如果也没有设置 initialRoute,Flutter 将尝试加载 routes 中的根路径 / 对应的页面。
  • 如果使用了 onGenerateRoute,Flutter 会根据路由名称调用该回调生成页面。
  • 如果只设置了 builder,则使用 builder 返回的组件作为启动页面。
  • 如果以上配置都没有,应用将无法启动并可能抛出异常。

_WidgetsAppState

build方法中, 生成导航Navigator

routing = FocusScope(
  debugLabel: 'Navigator Scope',
  autofocus: true,
  child: Navigator(
    clipBehavior: Clip.none,
    restorationScopeId: 'nav',
    key: _navigator,
    initialRoute: _initialRouteName,
    onGenerateRoute: _onGenerateRoute,
    onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
      ? Navigator.defaultGenerateInitialRoutes
      : (NavigatorState navigator, String initialRouteName) {
        return widget.onGenerateInitialRoutes!(initialRouteName);
      },
    onUnknownRoute: _onUnknownRoute,
    observers: widget.navigatorObservers!,
    routeTraversalEdgeBehavior: kIsWeb ? TraversalEdgeBehavior.leaveFlutterView : TraversalEdgeBehavior.parentScope,
    reportsRouteUpdateToEngine: true,
  ),
);

_onGenerateRoute生成路由钩子的源码:

Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
  final String? name = settings.name;
  final WidgetBuilder? pageContentBuilder = name == Navigator.defaultRouteName && widget.home != null
      ? (BuildContext context) => widget.home!
      : widget.routes![name];

  if (pageContentBuilder != null) {
    assert(
      widget.pageRouteBuilder != null,
      'The default onGenerateRoute handler for WidgetsApp must have a '
      'pageRouteBuilder set if the home or routes properties are set.',
    );
    final Route<dynamic> route = widget.pageRouteBuilder!<dynamic>(
      settings,
      pageContentBuilder,
    );
    return route;
  }
  if (widget.onGenerateRoute != null) {
    return widget.onGenerateRoute!(settings);
  }
  return null;
}

源码中, 可以看出。先判断当前路由name根路由名称是否一致。如果一致,再判断根页面是否存在, 如果存在, 就返回根页面

如果不存在, 就去命名路由表routes中查找外部配置的路由名称:页面。如果存在就返回查找到的页面的路由

如果不存在, 就调用onGenerateRoute方法, 从路由钩子里面查找当前页面对应的路由

如果不存在, 就返回null。随后, 程序会执行_onUnknownRoute方法, 加载用户配置的未知路由的页面

_onUnknownRoute未知路由源码:

Route<dynamic> _onUnknownRoute(RouteSettings settings) {
  assert(() {
    if (widget.onUnknownRoute == null) {
      throw FlutterError(
        'Could not find a generator for route $settings in the $runtimeType.\n'
        'Make sure your root app widget has provided a way to generate \n'
        'this route.\n'
        'Generators for routes are searched for in the following order:\n'
        ' 1. For the "/" route, the "home" property, if non-null, is used.\n'
        ' 2. Otherwise, the "routes" table is used, if it has an entry for '
        'the route.\n'
        ' 3. Otherwise, onGenerateRoute is called. It should return a '
        'non-null value for any valid route not handled by "home" and "routes".\n'
        ' 4. Finally if all else fails onUnknownRoute is called.\n'
        'Unfortunately, onUnknownRoute was not set.',
      );
    }
    return true;
  }());
  final Route<dynamic>? result = widget.onUnknownRoute!(settings);
  assert(() {
    if (result == null) {
      throw FlutterError(
        'The onUnknownRoute callback returned null.\n'
        'When the $runtimeType requested the route $settings from its '
        'onUnknownRoute callback, the callback returned null. Such callbacks '
        'must never return null.',
      );
    }
    return true;
  }());
  return result!;
}

生成未知页面的路由

Navigator

Navigator(
  clipBehavior: Clip.none,
  restorationScopeId: 'nav',
  key: _navigator,
  initialRoute: _initialRouteName,
  onGenerateRoute: _onGenerateRoute,
  onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
    ? Navigator.defaultGenerateInitialRoutes
    : (NavigatorState navigator, String initialRouteName) {
      return widget.onGenerateInitialRoutes!(initialRouteName);
    },
  onUnknownRoute: _onUnknownRoute,
  observers: widget.navigatorObservers!,
  routeTraversalEdgeBehavior: kIsWeb ? TraversalEdgeBehavior.leaveFlutterView : TraversalEdgeBehavior.parentScope,
  reportsRouteUpdateToEngine: true,
)

initialRoute => _initialRouteName

// If window.defaultRouteName isn't '/', we should assume it was set
// intentionally via `setInitialRoute`, and should override whatever is in
// [widget.initialRoute].
String get _initialRouteName => WidgetsBinding.instance.platformDispatcher.defaultRouteName != Navigator.defaultRouteName
  ? WidgetsBinding.instance.platformDispatcher.defaultRouteName
  : widget.initialRoute ?? WidgetsBinding.instance.platformDispatcher.defaultRouteName;

主要是用来确定 Flutter 应用的初始路由(即应用启动时加载的第一个页面)。它在 WidgetsBinding 中获取了默认的路由名称,并根据不同的情况确定最终的初始路由名称。

  • WidgetsBinding.instance.platformDispatcher.defaultRouteName:

    • 这是获取应用的默认路由名称。defaultRouteName 是 Flutter 在启动应用时所使用的默认路由。通常它是 '/',表示应用的根路径。
    • 如果你在运行时通过 setInitialRoute 方法改变了初始路由,这个值就会被更新为你设置的值。
  • Navigator.defaultRouteName:

    • 这个常量的值为 '/',表示应用的默认根路由。

总结:

  • 如果 defaultRouteName 不等于 '/',说明启动时可能有通过 setInitialRoute 设置的初始路由,代码会优先使用这个值。

  • 如果 defaultRouteName 等于 '/',则会检查 widget.initialRoute,使用它作为初始路由;如果 widget.initialRoute 也未设置,那么最后使用 '/' 作为初始路由。

pushNamed静态方法, 入栈

static Future<T?> pushNamed<T extends Object?>(
  BuildContext context,
  String routeName, {
  Object? arguments,
}) {
  return Navigator.of(context).pushNamed<T>(routeName, arguments: arguments);
}

push静态方法, 入栈

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

of静态方法, 获取NavigatorState

/// The state from the closest instance of this class that encloses the given
/// context.
///
/// Typical usage is as follows:
///
dart 这个用法重要
/// ```
/// Navigator.of(context)
///   ..pop()
///   ..pop()
///   ..pushNamed('/settings');
/// ```
///
/// If `rootNavigator` is set to true, the state from the furthest instance of
/// this class is given instead. Useful for pushing contents above all
/// subsequent instances of [Navigator].
///
/// If there is no [Navigator] in the given `context`, this function will throw
/// a [FlutterError] in debug mode, and an exception in release mode.
///
/// This method can be expensive (it walks the element tree).
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) {
    navigator = context.findRootAncestorStateOfType<NavigatorState>() ?? navigator;
  } else {
    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!;
}

NavigatorState

void initState() {
  super.initState();
  assert(_debugCheckPageApiParameters());
  添加导航监听
  for (final NavigatorObserver observer in widget.observers) {
    assert(observer.navigator == null);
    NavigatorObserver._navigators[observer] = this;
  }
  _effectiveObservers = widget.observers;

  // We have to manually extract the inherited widget in initState because
  // the current context is not fully initialized.
  final HeroControllerScope? heroControllerScope = context
    .getElementForInheritedWidgetOfExactType<HeroControllerScope>()
    ?.widget as HeroControllerScope?;
  _updateHeroController(heroControllerScope?.controller);

  if (widget.reportsRouteUpdateToEngine) {
    SystemNavigator.selectSingleEntryHistory();
  }

  ServicesBinding.instance.accessibilityFocus.addListener(_recordLastFocus);
  _history.addListener(_handleHistoryChanged);
}

添加导航监听 for (final NavigatorObserver observer in widget.observers) { assert(observer.navigator == null); NavigatorObserver._navigators[observer] = this; }

push

Future<T?> push<T extends Object?>(Route<T> route) {
  _pushEntry(_RouteEntry(route, pageBased: false, initialState: _RouteLifecycle.push));
  return route.popped;
}

pushNamed入栈

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

_routeNamed

根据路由名称, 生成路由

Route<T?>? _routeNamed<T>(String name, { required Object? arguments, bool allowNull = false }) {
  assert(!_debugLocked);
  if (allowNull && widget.onGenerateRoute == null) {
    return null;
  }
  assert(() {
    if (widget.onGenerateRoute == null) {
      throw FlutterError(
        'Navigator.onGenerateRoute was null, but the route named "$name" was referenced.\n'
        'To use the Navigator API with named routes (pushNamed, pushReplacementNamed, or '
        'pushNamedAndRemoveUntil), the Navigator must be provided with an '
        'onGenerateRoute handler.\n'
        'The Navigator was:\n'
        '  $this',
      );
    }
    return true;
  }());
  final RouteSettings settings = RouteSettings(
    name: name,
    arguments: arguments,
  );
  Route<T?>? route = widget.onGenerateRoute!(settings) as Route<T?>?;
  if (route == null && !allowNull) {
    assert(() {
      if (widget.onUnknownRoute == null) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('Navigator.onGenerateRoute returned null when requested to build route "$name".'),
          ErrorDescription(
            'The onGenerateRoute callback must never return null, unless an onUnknownRoute '
            'callback is provided as well.',
          ),
          DiagnosticsProperty<NavigatorState>('The Navigator was', this, style: DiagnosticsTreeStyle.errorProperty),
        ]);
      }
      return true;
    }());
    route = widget.onUnknownRoute!(settings) as Route<T?>?;
    assert(() {
      if (route == null) {
        throw FlutterError.fromParts(<DiagnosticsNode>[
          ErrorSummary('Navigator.onUnknownRoute returned null when requested to build route "$name".'),
          ErrorDescription('The onUnknownRoute callback must never return null.'),
          DiagnosticsProperty<NavigatorState>('The Navigator was', this, style: DiagnosticsTreeStyle.errorProperty),
        ]);
      }
      return true;
    }());
  }
  assert(route != null || allowNull);
  return route;
}

先调用路由钩子onGenerateRoute生成路由。如果不存在, 就调用onUnknownRoute, 生成未知页面路由

NavigatorObserver导航监听

navigatorObservers 是 Flutter 中的一个重要属性,允许你监视和响应导航事件。通过实现 NavigatorObserver,你可以跟踪路由的推送、弹出、替换等操作,进而执行自定义逻辑,比如日志记录、分析、权限检查等。

navigatorObservers 的使用

navigatorObservers 是一个 List<NavigatorObserver>,可以将多个 NavigatorObserver 添加到 Navigator 中。每当导航操作发生时,Navigator 会通知这些观察者。

主要的导航事件监听方法

NavigatorObserver 是一个抽象类,它定义了几个可以重写的方法,用于监听不同的导航事件:

  1. void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) :

    • 当一个新的路由被推送到导航栈时调用。
    • route 是当前被推送的路由。
    • previousRoute 是之前的路由,如果没有之前的路由则为 null
  2. void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) :

    • 当一个路由从导航栈中弹出时调用。
    • route 是当前被弹出的路由。
    • previousRoute 是之前的路由。
  3. void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) :

    • 当一个路由被从导航栈中移除时调用,而不是通过 pop 操作。
    • route 是被移除的路由。
    • previousRoute 是之前的路由。
  4. void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) :

    • 当一个路由被替换时调用。
    • newRoute 是替换后的新路由。
    • oldRoute 是被替换掉的旧路由。
  5. void didStartUserGesture(Route<dynamic> route, Route<dynamic>? previousRoute) :

    • 当用户开始手势导航时调用(例如,从屏幕边缘滑动返回)。
    • route 是当前正在展示的路由。
    • previousRoute 是之前的路由。
  6. void didStopUserGesture() :

    • 当用户结束手势导航时调用。

NavigatorState中, 封装路由观察的内部类结构。_NavigatorObservation 及其子类 _NavigatorPushObservation_NavigatorPopObservation_NavigatorRemoveObservation_NavigatorReplaceObservation 提供了一种结构化的方法来封装路由事件,并在适当的时候通知 NavigatorObserver

abstract class _NavigatorObservation {
  _NavigatorObservation(
    this.primaryRoute,
    this.secondaryRoute,
  );
  final Route<dynamic> primaryRoute;
  final Route<dynamic>? secondaryRoute;

  void notify(NavigatorObserver observer);
}

class _NavigatorPushObservation extends _NavigatorObservation {
  _NavigatorPushObservation(
    super.primaryRoute,
    super.secondaryRoute,
  );

  @override
  void notify(NavigatorObserver observer) {
    observer.didPush(primaryRoute, secondaryRoute);
  }
}

class _NavigatorPopObservation extends _NavigatorObservation {
  _NavigatorPopObservation(
    super.primaryRoute,
    super.secondaryRoute,
  );

  @override
  void notify(NavigatorObserver observer) {
    observer.didPop(primaryRoute, secondaryRoute);
  }
}

class _NavigatorRemoveObservation extends _NavigatorObservation {
  _NavigatorRemoveObservation(
    super.primaryRoute,
    super.secondaryRoute,
  );

  @override
  void notify(NavigatorObserver observer) {
    observer.didRemove(primaryRoute, secondaryRoute);
  }
}

class _NavigatorReplaceObservation extends _NavigatorObservation {
  _NavigatorReplaceObservation(
    super.primaryRoute,
    super.secondaryRoute,
  );

  @override
  void notify(NavigatorObserver observer) {
    observer.didReplace(newRoute: primaryRoute, oldRoute: secondaryRoute);
  }
}
  • _NavigatorObservation 是一个抽象类,封装了两个路由:primaryRoutesecondaryRoute

    • primaryRoute 是主要的路由,通常是当前正在进行操作的路由。
    • secondaryRoute 是辅助路由,可能是先前的路由或相关的其他路由。
  • 这个类包含一个抽象方法 notify,它需要在子类中实现,用于通知 NavigatorObserver

工作机制

  1. 事件封装:每次路由状态发生变化时,Navigator 会根据具体的路由操作(推送、弹出、移除、替换等)创建相应的 _NavigatorObservation 子类实例。
  2. 事件通知:创建 _NavigatorObservation 实例后,Navigator 会调用其 notify 方法,通知所有注册的 NavigatorObserver
  3. 回调执行:在 notify 方法中,对应的 NavigatorObserver 回调方法(如 didPushdidPop 等)会被调用,执行开发者在这些回调中定义的逻辑。

源码中的应用场景

这些类在 Navigator 实现中发挥重要作用,它们为路由事件的通知机制提供了一个结构化和模块化的实现。这种设计使得 Navigator 可以轻松地管理和扩展不同类型的路由事件,而不需要重复实现通知逻辑。

通过将不同类型的路由事件封装到不同的 _NavigatorObservation 子类中,Navigator 能够保持代码的清晰性和扩展性,同时为开发者提供了灵活的导航状态监听机制。

RouteSettings

class RouteSettings {
  /// Creates data used to construct routes.
  const RouteSettings({
    this.name,
    this.arguments,
  });

  /// The name of the route (e.g., "/settings").
  ///
  /// If null, the route is anonymous.
  final String? name;

  /// The arguments passed to this route.
  ///
  /// May be used when building the route, e.g. in [Navigator.onGenerateRoute].
  final Object? arguments;

  @override
  String toString() => '${objectRuntimeType(this, 'RouteSettings')}(${name == null ? 'none' : '"$name"'}, $arguments)';
}

RouteSettings 是 Flutter 路由系统中的一个关键类,它用于携带与路由相关的信息,特别是在自定义路由生成和页面导航时。RouteSettings 包含了两个主要属性:namearguments,它们共同描述了当前路由的基本信息。

  • name:

    • 类型:String?
    • 描述:表示路由的名称或路径。这个属性通常用于区分不同的页面或视图。
  • arguments:

    • 类型:Object?
    • 描述:可以携带任意类型的参数,用于在页面之间传递数据。典型用例包括传递 ID、用户信息、状态等。
const RouteSettings({
  this.name,
  this.arguments,
});

参考资料

路由管理

Flutter 秘籍