GetX路由管理
代码仓库: https://github.com/FangJZSP/getx_study.git
Flutter路由基础
Route
Route代表应用中页面,包含页面布局/逻辑/生命周期等信息。通常flutter中的Route继承PageRoute。
PageRoute是一个抽象类,可以用于Navigator的页面。PageRoute包含了页面构建方法、过渡动画以及页面生命周期回调。
// 使用MaterialPageRoute创建一个新页面 MaterialPageRoute(builder: (context) => NewPage());
// 使用CupertinoPageRoute创建一个新页面 CupertinoPageRoute(builder: (context) => NewPage());
Navigator
Navigator顾名思义导航器。
在flutter中Navigator是一个管理应用页面栈的组件,它负责处理页面之间的跳转、导航以及参数传递等操作。
对于栈这个数据结构我们并不陌生,可以有效管理元素的进出。
GetX路由
从Routers.getPages
的构成了解GetX的Route的使命 - 静态配置
从上述的讲解中我们可以知道Route其实就是一个页面的灵魂。那GetX是如何实现页面的灵魂的呢?
深入代码 寻找route的创建过程
class Routers {
static const root = '/';
static const first = '/first';
static const second = '/second';
static List<GetPage> getPages = [
GetPage(name: root, page: () => RootPage()),
GetPage(name: first, page: () => FirstPage()),
GetPage(name: second, page: () => SecondPage()),
];
}
我们的页面是GetPage,继承了Page,Page继承了RouteSetting。
继承自Page,Page中实现了创建路由方法(createRoute),而Page继承了RouteSetting,提供了创建路由所需的必要配置。
在GetPage中,我们实现父类方法createRoute
/// get_route.dart GetPage类
// ps: abstract class Page<T> extends RouteSettings
class GetPage<T> extends Page<T> {
@override
Route<T> createRoute(BuildContext context) {
// return GetPageRoute<T>(settings: this, page: page);
final page = PageRedirect(
route: this, // GetPage
settings: this, // RouteSetting
unknownRoute: unknownRoute,
).getPageToRoute<T>(this, unknownRoute);
return page;
}
}
/// navigator.dart Page类
abstract class Page<T> extends RouteSettings {
@factory
Route<T> createRoute(BuildContext context);
}
/// route_middleware.dart PageRedirect类
class PageRedirect {
GetPageRoute<T> getPageToRoute<T>(GetPage rou, GetPage? unk) {
while (needRecheck()) {}
final r = (isUnknown ? unk : rou)!;
// 通过PageRedirect提供的getPageToRoute方法
return GetPageRoute<T>(
// ... getx的route就定义好了
);
}
}
再来看一下GetPageRoute具体是什么
它继承了PageRoute,并with了GetPageRouteTransitionMixin<T>, PageRouteReportMixin
在GetPageRoute中我们完成了重要的buildContent方法,后续在路由入栈后,可以用该方法生成页面
/// default_route.dart GetPageRoute类
class GetPageRoute<T> extends PageRoute<T>
with GetPageRouteTransitionMixin<T>, PageRouteReportMixin {
GetPageRoute( // ..
) : super(settings: settings, fullscreenDialog: fullscreenDialog);
@override
final Duration transitionDuration;
final GetPageBuilder? page;
final String? routeName;
Widget? _child;
Widget _getChild() {
// ... 和page有关
final pageToBuild = middlewareRunner.runOnPageBuildStart(page)!;
_child = middlewareRunner.runOnPageBuilt(pageToBuild());
return _child!;
}
@override
Widget buildContent(BuildContext context) {
return _getChild();
}
// ...
}
最终getPages GetMaterialApp时初始化时会执行Get.addPages(getPages!);(后面会讲到)
其中PageRoute的顶级父类是Route。在Route中,我们通过它的字段navigator我们完成了对页面的路由。
一直强调的Navigator到底是什么? - 动态监控
在此之前,我们一直强调Navigator,但是我们不知道Navigator从何处被new出来的。
简而言之,一个能观察到所有页面的顶层组件,必然是在我们runApp之后不久就应该产生的。
那让我们一起探索一下这个神奇的组件。
debug代码 找到Navigator
从runApp之后的我们组件是GetMaterialApp,再往里查看代码,我们可以看到WidgetsApp,在它的build方法中出现了Navigator。
所以从一开始,我们的app就是一个Navigator组件下嵌套了各种各样的页面,并可以观察。
那可以着重观察一下监听路由的行为了解,监听器在何时被安排在了生成的页面中。
回到runApp,组件是GetMaterialApp,其中有一个参数名为navigatorObservers,我们一路观察下去。
最终我们在Navigator中找到参数observers。
接着探索,我们发现Navigator的observers在NavigatorState initState时被传入给_effectiveObservers。
在NavigatorState看到一个重要的方法flush,通知观察者刷新。
其中的_observedRouteAdditions&_observedRouteDeletions,字面意思一个路由添加、一个路由删除。 在navigator执行路由添加时,将**_NavigatorObservation**的子类添加到_observedRouteAdditions队列中,删除则添加到_observedRouteDeletions。
最后统一由通知给路由观察者发送通知,并执行路由通知者中的观察到刷新后观察者自己的任务
void _flushObserverNotifications() {
if (_effectiveObservers.isEmpty) {
_observedRouteDeletions.clear();
_observedRouteAdditions.clear();
return;
}
while (_observedRouteAdditions.isNotEmpty) {
final _NavigatorObservation observation = _observedRouteAdditions.removeLast();
_effectiveObservers.forEach(observation.notify);
}
while (_observedRouteDeletions.isNotEmpty) {
final _NavigatorObservation observation = _observedRouteDeletions.removeFirst();
_effectiveObservers.forEach(observation.notify);
}
}
那我们作为观察者观察一下 -> 不难发现,其实观察者可以有很多,这样我们可以实现一个切面思想,针对路由的行为,自定义我们观察到路由变化的事件。
我们看GetX实现的GetObserver,重写了很多方法,并一一实现了自己的观察到路由变化后执行的操作。
展示代码
当我们回看到自己的项目时,里面也有MyRouteObserver。
展示代码
我们同样重写方法,didPop/didPush等等,那他们什么时候调用的呢?
我们再回到_NavigatorObservation,他定义了一个notify方法.
当_NavigatorObservation的子类如_NavigatorPopObservation 我们发现它重写了notify方法,
abstract class _NavigatorObservation {
_NavigatorObservation(this.primaryRoute,
this.secondaryRoute,);
final Route<dynamic> primaryRoute;
final Route<dynamic>? secondaryRoute;
void notify(NavigatorObserver observer);
}
class _NavigatorPopObservation extends _NavigatorObservation {
_NavigatorPopObservation( //...
);
@override
void notify(NavigatorObserver observer) {
observer.didPop(primaryRoute, secondaryRoute);
}
}
这里调用观察者的方法,在这一刻路由行为的观察并进行行动终于形成了闭环。
并且,通过这一思想,我们就可以自定义实现我们项目中的路由链了。
从Get.toNamed()
了解GetX的路由跳转及路由生成 - 贯穿始终
路由的奇幻漂流 - 路由历史
Future<T?>? toNamed<T>(String page, {
dynamic arguments,
int? id,
bool preventDuplicates = true,
Map<String, String>? parameters,
}) {
// 当前路由和跳转路由重复时且阻止重复页面时,跳转无效
if (preventDuplicates && page == currentRoute) {
return null;
}
// 当跳转时携带参数回拼接给页面
// 我们项目中没有使用这种方法,而是使用了Get.arguments
if (parameters != null) {
final uri = Uri(path: page, queryParameters: parameters);
page = uri.toString();
}
// 通过 NavigatorState 的方法实现往路由栈添加页面
return global(id).currentState?.pushNamed<T>(
page,
arguments: arguments,
);
}
// global方法通过GlobalKey返回NavigatorState
GlobalKey<NavigatorState> global(int? k) {
GlobalKey<NavigatorState> newKey;
// ...
return newKey;
}
@optionalTypeArgs
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
/// GetX默认创建的全局键
factory GlobalKey({ String? debugLabel }) => LabeledGlobalKey<T>(debugLabel);
/// 创建一个不带标签的全局键
const GlobalKey.constructor() : super.empty();
Element? get _currentElement => WidgetsBinding.instance.buildOwner!._globalKeyRegistry[this];
/// 具有此键的组件在其中构建的构建上下文。
/// 如果树中没有与此全局键匹配的组件,则当前上下文为空。
BuildContext? get currentContext => _currentElement;
/// 树中当前具有此全局键的组件。
/// 如果树中没有与此全局键匹配的小部件,则当前组件为空。
Widget? get currentWidget => _currentElement?.widget;
/// 树中当前具有此全局键的小部件的State。
/// 1. 要stateful
/// 2. 和这个State要相同
/// 否则返回空
T? get currentState {
final Element? element = _currentElement;
if (element is StatefulElement) {
final StatefulElement statefulElement = element;
final State state = statefulElement.state;
if (state is T) {
return state;
}
}
return null;
}
}
阅读源码
当你使用GetX没有设置默认Key时,Get.toNamed的时候默认拿到的是GlobalKey是默认创建的,并且这个globalKey可以拿到NavigatorState
var _key = GlobalKey<NavigatorState>(debugLabel: 'Key Created by default');
这个key的NavigatorState为包裹该GetMaterialApp的父组件,你可以使用它来完成路由的跳转.
阅读源码
// 走到了NavigatorState中的pushNamed方法
Future<T?> pushNamed<T extends Object?>(String routeName, {
Object? arguments,
}) {
return push<T?>(_routeNamed<T>(routeName, arguments: arguments)!);
}
// _routeNamed 我们根据setting通过onGenerateRoute完成了路由的生成
// 那么我们这个生成器从何?在生成路由的同时,完成了什么工作?这就不得不一步步往上去寻找我们的配置,完成页面生成闭环
/// 但我们暂时按下不表,先把路由插入 讲完
Route<T?>? _routeNamed<T>(String name, { required Object? arguments, bool allowNull = false }) {
// ...
final RouteSettings settings = RouteSettings(
name: name,
arguments: arguments,
);
Route<T?>? route = widget.onGenerateRoute!(settings) as Route<T?>?;
// ...
return route;
}
// push方法
@optionalTypeArgs
Future<T?> push<T extends Object?>(Route<T> route) {
_pushEntry(_RouteEntry(route, pageBased: false, initialState: _RouteLifecycle.push));
// Future<T?> get popped => _popCompleter.future;
// final Completer<T?> _popCompleter = Completer<T?>();
// 当上述行为完成后,一个承诺完成的状态就可以完成了,并返回你想要的泛型
return route.popped;
}
// 当该route的行为被记录到_history列表中中
// _History类代表的是导航历史的_RouteEntries的集合
void _pushEntry(_RouteEntry entry) {
// ...
_history.add(entry);
// 刷新路由行为
_flushHistoryUpdates();
// ...
_afterNavigation(entry.route);
}
这样一个完整的路由行为就被记录下来,同时也通知了观察者记得刷新哦~
路由的生成
那话说回来,路由是何时生成的呢? 书接上回
onGenerateRoute是Navigator的参数,上层是WidgetsApp的_onGenerateRoute
// **onGenerateRoute**是Navigator的参数, 上层是WidgetsApp的_onGenerateRoute
class Navigator extends StatefulWidget {
const Navigator({
// ...
this.onGenerateRoute,
// ...
});
}
// 生成路由有两种方式onGenerateRoute/pageRouteBuilder 我们都看一下
class WidgetsApp extends StatefulWidget {
WidgetsApp({
// ...
this.onGenerateRoute,
// ...
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
this.initialRoute,
this.pageRouteBuilder,
//...
});
}
class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
final RouteFactory? onGenerateRoute;
@override
Widget build(BuildContext context) {
Widget? routing;
routing = FocusScope(
// ...
child: Navigator(
// ...
onGenerateRoute: _onGenerateRoute,
// ...
),
);
return xxx;
}
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];
// debug发现pageContentBuilder为空,因为name和widget.home都为空
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;
}
}
继续深入代码
在往上走到了MaterialApp:
pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
return MaterialPageRoute<T>(settings: settings, builder: builder);
},
onGenerateRoute: widget.onGenerateRoute,
再往上走到了GetMaterialApp:
onGenerateRoute: (getPages != null ? generator : onGenerateRoute),
// 当我们去到下一个页面时,该方法就会执行
Route<dynamic> generator(RouteSettings settings) {
return PageRedirect(settings: settings, unknownRoute: unknownRoute).page();
}
上述文章已经讲了构建页面的方法。
PageRedirect会通过之前的getPages根据setting匹配返回我们定义route
那最后,页面就回在createRoute中完成页面的build,至此页面的生成流程也介绍完毕。
从捣蛋弹窗
了解参数传递 - Get.arguments
捣蛋弹窗详情请看项目仓库代码。
通过debug发现,路由先记录历史记录,记录的同时,GetObserver会通过Get.routing内置的update方法更新自己上一个路由。
@override
void didPush(Route route, Route? previousRoute) {
// ...
// 更新自己
_routeSend?.update((value) {
// Only PageRoute is allowed to change current value
if (route is PageRoute) {
value.current = newRoute.name ?? '';
}
final previousRouteName = _extractRouteName(previousRoute);
if (previousRouteName != null) {
value.previous = previousRouteName;
}
value.args = route.settings.arguments;
value.route = route;
value.isBack = false;
value.removed = '';
value.isBottomSheet =
newRoute.isBottomSheet ? true : value.isBottomSheet ?? false;
value.isDialog = newRoute.isDialog ? true : value.isDialog ?? false;
});
// ...
}
当页面初始化时,此时上一个路由已经为弹窗,用get.arguments 传递给页面的参数获取不到。
解决办法1,通过页面参数透传 -> 演示
解决办法2,自己维护router列表 -> 演示