createAppRouter function
- required String initialLocation,
- required String landingLocation,
- required ProviderContainer container,
Creates a configured GoRouter starting at initialLocation.
Watches sessionProvider via
GoRouter.new's refreshListenable. Redirects to
AppRoutes.update when a forced update is pending (highest
priority), and to AppRoutes.login when the session is inactive.
Optional updates are surfaced as a dismissible banner instead.
Implementation
GoRouter createAppRouter({
required String initialLocation,
required String landingLocation,
required ProviderContainer container,
}) => GoRouter(
navigatorKey: rootNavigatorKey,
initialLocation: initialLocation,
refreshListenable: _SessionRefreshListenable(container),
redirect: (context, state) {
// Force-update gate: block all navigation when a forced update is pending.
final updateConfig = container.read(updateConfigProvider);
final isForcedUpdate = updateConfig?.isForcedUpdate == true;
if (isForcedUpdate) {
if (state.matchedLocation == AppRoutes.update) return null;
return AppRoutes.update;
}
// Escape: forced update cleared remotely while user is on the gate screen.
// Also kicks the user out if they manually navigated to the update screen
// but no update is actually available.
if (state.matchedLocation == AppRoutes.update && updateConfig == null) {
final hasSession = container.read(sessionProvider);
if (!hasSession) return AppRoutes.intro;
return container.read(pendingAppRouteProvider) ?? landingLocation;
}
// Auth gate: redirect unauthenticated users to login. An authenticated
// internal destination is consumed only once its route is reached.
final hasSession = container.read(sessionProvider);
if (hasSession) {
final pendingRoute = container.read(pendingAppRouteProvider);
if (pendingRoute == null) return null;
if (state.matchedLocation == pendingRoute) {
// Redirect runs while the widget tree builds, where modifying a
// provider throws, so release the destination once this frame
// settles. Repeat redirects before then are harmless: they resolve
// to this same already-reached route.
Future(() => container.read(pendingAppRouteProvider.notifier).take());
return null;
}
return pendingRoute;
}
if (_publicRoutes.contains(state.matchedLocation)) return null;
return AppRoutes.login;
},
observers: [
if (firebaseService.analyticsObserver != null)
firebaseService.analyticsObserver!,
],
routes: [
GoRoute(
path: AppRoutes.whatsNew,
builder: (context, state) => _framed(const WhatsNewScreen()),
),
GoRoute(
path: AppRoutes.update,
builder: (context, state) => const UpdateScreen(),
),
GoRoute(
path: AppRoutes.intro,
builder: (context, state) => _framed(const IntroScreen()),
),
GoRoute(
path: AppRoutes.studentUnion,
builder: (context, state) => _framed(const StudentUnionScreen()),
),
GoRoute(
path: AppRoutes.login,
builder: (context, state) => _framed(const LoginScreen()),
),
GoRoute(
path: AppRoutes.changePassword,
builder: (context, state) {
final extra = state.extra as Map<String, dynamic>?;
final isExpired = extra?['isExpired'] as bool? ?? false;
final username = extra?['username'] as String?;
return _framed(
ChangePasswordScreen(
isExpired: isExpired,
username: username,
),
);
},
),
GoRoute(
path: AppRoutes.about,
builder: (context, state) => _framed(const AboutScreen()),
),
GoRoute(
path: AppRoutes.scanner,
builder: (context, state) => const ScannerScreen(),
),
GoRoute(
path: AppRoutes.regedit,
builder: (context, state) => _framed(const RegeditScreen()),
),
GoRoute(
path: AppRoutes.preferences,
builder: (context, state) => _framed(const PreferencesScreen()),
),
GoRoute(
path: AppRoutes.language,
builder: (context, state) => _framed(
const PreferenceDetailScreen(id: .language),
),
),
GoRoute(
path: AppRoutes.themeMode,
builder: (context, state) => _framed(
const PreferenceDetailScreen(id: .themeMode),
),
),
GoRoute(
path: AppRoutes.portal,
builder: (context, state) => _framed(const PortalScreen()),
),
GoRoute(
path: AppRoutes.calendar,
builder: (context, state) => _framed(const CalendarScreen()),
),
GoRoute(
path: AppRoutes.ntutWifi,
builder: (context, state) => const NtutWifiScreen(),
),
GoRoute(
path: AppRoutes.campusMap,
builder: (context, state) => CampusMapScreen(
initialQuery: state.uri.queryParameters['room'],
),
),
StatefulShellRoute(
builder: (context, state, navigationShell) =>
HomeScreen(navigationShell: navigationShell),
navigatorContainerBuilder: (context, navigationShell, children) {
return AnimatedShellContainer(
currentIndex: navigationShell.currentIndex,
children: children,
);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: AppRoutes.home,
pageBuilder: (context, state) =>
NoTransitionPage(child: _framed(const MainHomeScreen())),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: AppRoutes.courseTable,
pageBuilder: (context, state) =>
NoTransitionPage(child: _framed(const CourseTableScreen())),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: AppRoutes.score,
pageBuilder: (context, state) =>
NoTransitionPage(child: _framed(const ScoreScreen())),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: AppRoutes.profile,
pageBuilder: (context, state) =>
NoTransitionPage(child: _framed(const ProfileScreen())),
),
],
),
],
),
],
);