admin管理员组

文章数量:1316399

I need some of my app's pages to be notified when they are activated or deactivated as a result of change of the current Navigator. When both my target page (PageA) and the overlapping page (PageB) belong to the same Navigator, I can attach a RouteObserver to the Navigator and implement the RouteAware interface in the PageAState class to trigger the didPushNext and didPopNext callbacks.

Unfortunately, this approach does not work when PageA belongs to a local navigator while PageB is part of the root navigator. If PageB appears over PageA, neither of the navigators triggers the didPushNext or didPopNext callbacks for PageAState.

Just to make things clear, here is the simplified version of my router's configuration:

final routerConfig = GoRouter(
  navigatorKey: _rootNavigatorKey,
  initialLocation: '/',
  observers: [rootRouteObserver],
  routes: [
    GoRoute(
      path: '/',
      redirect: (context, state) => AppRoutes.splash(),
    ),
    StatefulShellRoute.indexedStack(
      builder:
          (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) {
        return ScaffoldWithNavigationBar(navigationShell: navigationShell);
      },
      branches: <StatefulShellBranch>[        
        StatefulShellBranch(
          navigatorKey: _miscNavigatorKey,
          observers: [miscRouteObserver],
          routes: <RouteBase>[
            GoRoute(
              path: AppRoutes.misc(),
              builder: (context, state) => const MiscPage(),              
              routes: <RouteBase>[
                GoRoute(
                  parentNavigatorKey: _rootNavigatorKey,
                  path: AppRoutes._ordersRouteName,
                  builder: (context, state) => const OrdersPage(),
                  routes: <RouteBase>[
                    GoRoute(
                      parentNavigatorKey: _rootNavigatorKey,
                      path: '${AppRoutes._orderRouteName}/:orderId',
                      builder: (context, state) =>
                          SingleOrderPage(orderId: state.pathParameters['orderId']),
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ],
    ),
  ],
);

As you can see, MiscPage and OrdersPage belong to different navigators (identified by _miscNavigatorKey and _rootNavigatorKey respectively). MiscPage is displayed inside a shell route with tabbed navigation and represents a root page of one the tabs. OrdersPage appears above it and is designed to be shown fullscreen, overlapping both MiscPage and the tab bar.

I have the same the same behaviour implemented for both MiscPage and OrdersPage:

class AnyPageState extends State<AnyPage> with RouteAware {
  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback(
      (_) {
        rootRouteObserver.subscribe(this, ModalRoute.of(context)!);
        miscRouteObserver.subscribe(this, ModalRoute.of(context)!);
      }
    );
    super.initState();
  }

  @override
  void dispose() {
    rootRouteObserver.unsubscribe(this);
    miscRouteObserver.unsubscribe(this);
    super.dispose();
  }

  @override
  void didPopNext() {
    doSomething();
  }
}

So, here I'm subscribed to both route observers and expecting for didPopNext to be called upon OrdersPage activates again after closing SingleOrderPage and MiscPage - after closing OrdersPage. IRL didPopNext is called for OrdersPage only - IMO, because both OrdersPage and SingleOrderPage have the same (root) navigator. MiscPage and OrdersPage have different navigators, so didPopNext is never called for the first page.

Back to my question, is there any other way in GoRouter, Navigator, Flutter, etc. to get the job done in this case?

I need some of my app's pages to be notified when they are activated or deactivated as a result of change of the current Navigator. When both my target page (PageA) and the overlapping page (PageB) belong to the same Navigator, I can attach a RouteObserver to the Navigator and implement the RouteAware interface in the PageAState class to trigger the didPushNext and didPopNext callbacks.

Unfortunately, this approach does not work when PageA belongs to a local navigator while PageB is part of the root navigator. If PageB appears over PageA, neither of the navigators triggers the didPushNext or didPopNext callbacks for PageAState.

Just to make things clear, here is the simplified version of my router's configuration:

final routerConfig = GoRouter(
  navigatorKey: _rootNavigatorKey,
  initialLocation: '/',
  observers: [rootRouteObserver],
  routes: [
    GoRoute(
      path: '/',
      redirect: (context, state) => AppRoutes.splash(),
    ),
    StatefulShellRoute.indexedStack(
      builder:
          (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) {
        return ScaffoldWithNavigationBar(navigationShell: navigationShell);
      },
      branches: <StatefulShellBranch>[        
        StatefulShellBranch(
          navigatorKey: _miscNavigatorKey,
          observers: [miscRouteObserver],
          routes: <RouteBase>[
            GoRoute(
              path: AppRoutes.misc(),
              builder: (context, state) => const MiscPage(),              
              routes: <RouteBase>[
                GoRoute(
                  parentNavigatorKey: _rootNavigatorKey,
                  path: AppRoutes._ordersRouteName,
                  builder: (context, state) => const OrdersPage(),
                  routes: <RouteBase>[
                    GoRoute(
                      parentNavigatorKey: _rootNavigatorKey,
                      path: '${AppRoutes._orderRouteName}/:orderId',
                      builder: (context, state) =>
                          SingleOrderPage(orderId: state.pathParameters['orderId']),
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ],
    ),
  ],
);

As you can see, MiscPage and OrdersPage belong to different navigators (identified by _miscNavigatorKey and _rootNavigatorKey respectively). MiscPage is displayed inside a shell route with tabbed navigation and represents a root page of one the tabs. OrdersPage appears above it and is designed to be shown fullscreen, overlapping both MiscPage and the tab bar.

I have the same the same behaviour implemented for both MiscPage and OrdersPage:

class AnyPageState extends State<AnyPage> with RouteAware {
  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback(
      (_) {
        rootRouteObserver.subscribe(this, ModalRoute.of(context)!);
        miscRouteObserver.subscribe(this, ModalRoute.of(context)!);
      }
    );
    super.initState();
  }

  @override
  void dispose() {
    rootRouteObserver.unsubscribe(this);
    miscRouteObserver.unsubscribe(this);
    super.dispose();
  }

  @override
  void didPopNext() {
    doSomething();
  }
}

So, here I'm subscribed to both route observers and expecting for didPopNext to be called upon OrdersPage activates again after closing SingleOrderPage and MiscPage - after closing OrdersPage. IRL didPopNext is called for OrdersPage only - IMO, because both OrdersPage and SingleOrderPage have the same (root) navigator. MiscPage and OrdersPage have different navigators, so didPopNext is never called for the first page.

Back to my question, is there any other way in GoRouter, Navigator, Flutter, etc. to get the job done in this case?

Share Improve this question edited Feb 2 at 16:25 Ken White 126k15 gold badges236 silver badges464 bronze badges asked Jan 29 at 19:33 Vyacheslav OrlovskyVyacheslav Orlovsky 4361 gold badge7 silver badges18 bronze badges
Add a comment  | 

1 Answer 1

Reset to default 0

I could not find any built-in solution, so have to propose my own one:

  1. Create a class that holds references to all RouteObserver instances used in router config and mediates subscriptions. It also holds information to match GoRoute and Navigator's Route of the subscribed pages
class _DummyRoute extends Route {}

class _MultiRouteObserver {
  static final _dummyRoute = _DummyRoute();
  final _observers = <RouteObserver>{};
  final _routesMap = <RouteBase, Route>{};

  RouteObserver add([RouteObserver? observer]) {
    observer ??= RouteObserver();
    _observers.add(observer);
    return observer;
  }

  void subscribe(RouteAware routeAware, {required Route route, RouteBase? goRoute}) {
    if (goRoute != null) {
      _routesMap[goRoute] = route;
      final navigator = goRoute.parentNavigatorKey!.currentWidget as Navigator;
      RouteObserver? observer = navigator.observers.firstWhereOrNull(
        (element) => _observers.contains(element),
      ) as RouteObserver?;
      observer?.subscribe(routeAware, route);
    } else {
      for (RouteObserver observer in _observers) {
        observer.subscribe(routeAware, route);
      }
    }
  }

  void unsubscribe(RouteAware routeAware, {RouteBase? goRoute}) {
    if (goRoute != null) {
      _routesMap.remove(goRoute);
    }
    for (RouteObserver observer in _observers) {
      observer.unsubscribe(routeAware);
    }
  }

  void didPopNextFor(RouteBase goRoute) {
    for (RouteObserver observer in _observers) {
      observer.didPop(_dummyRoute, _routesMap[goRoute]);
    }
  }

  void didPushNextFor(RouteBase goRoute) {
    for (RouteObserver observer in _observers) {
      observer.didPush(_dummyRoute, _routesMap[goRoute]);
    }
  }
}

final appRouteObserver = _MultiRouteObserver();
  1. Update router config, pass a RouteObserver instance created with appRouteObserver.add() and explicitly specify an actual parentNavigatorKey for each GoRoute
final routerConfig = GoRouter(
  navigatorKey: _rootNavigatorKey,
  initialLocation: '/',
  observers: [appRouteObserver.add()],
  routes: [    
    StatefulShellRoute.indexedStack(
      parentNavigatorKey: _rootNavigatorKey,
      builder:
          (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) {
        return ScaffoldWithNavigationBar(navigationShell: navigationShell);
      },
      branches: <StatefulShellBranch>[
        StatefulShellBranch(
          navigatorKey: _catalogueNavigatorKey,
          observers: [appRouteObserver.add()],
          routes: <RouteBase>[
            GoRoute(
              parentNavigatorKey: _catalogueNavigatorKey,
              path: AppRoutes.catalogue(),
              builder: (context, state) => const CataloguePage(),
              routes: <RouteBase>[
                GoRoute(
                  parentNavigatorKey: _catalogueNavigatorKey,
                  path: '${AppRoutes._categoryRouteName}/:categoryId',
                  builder: (context, state) => CategoryPage(
                      categoryId: int.parse(state.pathParameters['categoryId'] ?? '0')),
                  routes: <RouteBase>[
                    GoRoute(
                      parentNavigatorKey: _rootNavigatorKey,
                      path: '${AppRoutes._productRouteName}/:productId',
                      builder: (context, state) => ProductPage(
                          productId: int.parse(state.pathParameters['productId'] ?? '0')),
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
        StatefulShellBranch(
          navigatorKey: _miscNavigatorKey,
          observers: [appRouteObserver.add()],
          routes: <RouteBase>[
            GoRoute(
              parentNavigatorKey: _miscNavigatorKey,
              path: AppRoutes.misc(),
              redirect: _loggedInOnly,
              builder: (context, state) => const MiscPage(),
              routes: <RouteBase>[
                GoRoute(
                  parentNavigatorKey: _rootNavigatorKey,
                  path: AppRoutes._userProfileRouteName,
                  builder: (context, state) => const UserProfilePage(),
                ),
                GoRoute(
                  parentNavigatorKey: _rootNavigatorKey,
                  path: AppRoutes._ordersRouteName,
                  builder: (context, state) => const OrdersPage(),
                  routes: <RouteBase>[
                    GoRoute(
                      parentNavigatorKey: _rootNavigatorKey,
                      path: '${AppRoutes._orderRouteName}/:orderId',
                      builder: (context, state) =>
                          OrderPage(orderId: state.pathParameters['orderId']),
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),
      ],
    ),
  ],
);
  1. Create a mixin that implements subscription to any RouteObserver instance through common RouteAware interface. From this point didPop, didPopNext, etc. will be called for any page containing this mixin.
mixin AppRouteObserverSubscription<T extends StatefulWidget> on State<T>, RouteAware {
  GoRoute? goRoute;

  @override
  void initState() {
    goRoute = GoRouter.of(context).state?.topRoute;
    WidgetsBinding.instance.addPostFrameCallback(
      (_) => appRouteObserver.subscribe(
        this,
        route: ModalRoute.of(context)!,
        goRoute: goRoute,
      ),
    );
    super.initState();
  }

  @override
  void dispose() {
    appRouteObserver.unsubscribe(this, goRoute: goRoute);
    super.dispose();
  }
}
  1. Add the following actions to the ScaffoldWithNavigationBarState (the AppRouteObserverSubscription mixin has not been applied as goRoute's value is obtained there in a unique way). This code actually makes didPopNext and didPushNext to be called for the topmost page inside the router's StatefulShellRoute in case of any other page that belongs to root Navigator appears over it
@override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback(
      (_) => appRouteObserver.subscribe(
        this,
        route: ModalRoute.of(context)!,
        goRoute: widget.navigationShell.route,
      ),
    );
    super.initState();
  }

  @override
  void dispose() {
    appRouteObserver.unsubscribe(this, goRoute: widget.navigationShell.route);
    super.dispose();
  }

  @override
  void didPopNext() {
    final topRoute = GoRouter.of(context).state?.topRoute;
    if (topRoute != null) {
      appRouteObserver.didPopNextFor(topRoute);
    }
  }

  @override
  void didPushNext() {
    final topRoute = GoRouter.of(context).state?.topRoute;
    if (topRoute != null) {
      appRouteObserver.didPushNextFor(topRoute);
    }
  }

本文标签: