Lewati ke konten utama

Doc 09 — Flutter Route Specification

Purpose: go_router config siap-pakai yang mirroring 315 prototype reactions dari Figma. Developer dapat copy-paste ke lib/core/router/app_router.dart dan langsung fungsional (stub pages bisa pakai Scaffold(body: Text('TODO')) untuk iteratively replace).

Package: go_router: ^14.0.0 Flutter: >=3.22


Architecture Overview

AppRouter
├── /splash (SplashPage, auto-redirect after 1.5s)
├── /onboarding/1..3 (linear onboarding)
├── /login, /login/staff (auth entry points)
├── /forgot-password/* (reset flows)
├── /404 (error builder)
└── authenticated shell (guard: token present)
├── ShellRoute(ParentShell) → /parent/* (6 tabs)
├── ShellRoute(GuruShell) → /guru/* (5 tabs)
├── ShellRoute(WaliKelasShell) → /wali-kelas/* (5 tabs)
├── ShellRoute(BendaharaShell) → /bendahara/* (5 tabs)
├── ShellRoute(KepsekShell) → /kepsek/* (5 tabs)
└── ShellRoute(AdminShell) → /admin/* (5 tabs)

Setiap ShellRoute memegang Scaffold dengan bottomNavigationBar persistent dan swap body saat tab tap. Sub-routes tetap push sebagai full-screen di atas shell.


1. File skeleton

lib/core/router/app_router.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

import '../../features/auth/splash_page.dart';
import '../../features/auth/onboarding_welcome_page.dart';
// ... other imports

import 'auth_guard.dart';
import 'shells/parent_shell.dart';
import 'shells/guru_shell.dart';
// ... other shells

final GlobalKey<NavigatorState> _rootNavKey = GlobalKey<NavigatorState>();

GoRouter buildRouter({required AuthState authState}) {
return GoRouter(
navigatorKey: _rootNavKey,
initialLocation: '/splash',
debugLogDiagnostics: true, // disable in production
redirect: (context, state) => AuthGuard.evaluate(state, authState),
errorBuilder: (_, __) => const NotFoundPage(),
routes: [
// ─── PUBLIC (no auth) ─────────────────────────────────────────────
GoRoute(
path: '/splash',
name: 'splash',
builder: (_, __) => const SplashPage(),
),
GoRoute(
path: '/onboarding/1',
name: 'onboarding-welcome',
builder: (_, __) => const OnboardingWelcomePage(),
),
GoRoute(
path: '/onboarding/2',
name: 'onboarding-fitur',
pageBuilder: _slideInRight((_) => const OnboardingFiturPage()),
),
GoRoute(
path: '/onboarding/3',
name: 'onboarding-mulai',
pageBuilder: _slideInRight((_) => const OnboardingMulaiPage()),
),
GoRoute(
path: '/login',
name: 'login-parent',
builder: (_, __) => const LoginPage(),
),
GoRoute(
path: '/login/staff',
name: 'login-staff',
builder: (_, __) => const LoginStaffPage(),
),
GoRoute(
path: '/forgot-password',
name: 'forgot-password',
builder: (_, __) => const ForgotPasswordPage(),
),
GoRoute(
path: '/forgot-password/staff',
name: 'forgot-password-staff',
builder: (_, __) => const ForgotPasswordStaffPage(),
),

// ─── PARENT SHELL (6-tab BottomNav) ───────────────────────────────
ShellRoute(
builder: (context, state, child) => ParentShell(
currentLocation: state.matchedLocation,
child: child,
),
routes: _parentRoutes,
),

// ─── GURU SHELL (5-tab) ───────────────────────────────────────────
ShellRoute(
builder: (context, state, child) => GuruShell(
currentLocation: state.matchedLocation,
child: child,
),
routes: _guruRoutes,
),

// ─── WALI KELAS SHELL (5-tab) ─────────────────────────────────────
ShellRoute(
builder: (context, state, child) => WaliKelasShell(
currentLocation: state.matchedLocation,
child: child,
),
routes: _waliKelasRoutes,
),

// ─── BENDAHARA SHELL (5-tab) ──────────────────────────────────────
ShellRoute(
builder: (context, state, child) => BendaharaShell(
currentLocation: state.matchedLocation,
child: child,
),
routes: _bendaharaRoutes,
),

// ─── KEPSEK SHELL (5-tab) ─────────────────────────────────────────
ShellRoute(
builder: (context, state, child) => KepsekShell(
currentLocation: state.matchedLocation,
child: child,
),
routes: _kepsekRoutes,
),

// ─── ADMIN SHELL (5-tab) ──────────────────────────────────────────
ShellRoute(
builder: (context, state, child) => AdminShell(
currentLocation: state.matchedLocation,
child: child,
),
routes: _adminRoutes,
),

// ─── ROOT-LEVEL AUTHENTICATED (shared across personas) ────────────
GoRoute(
path: '/profile/edit',
parentNavigatorKey: _rootNavKey, // render above shell
builder: (_, __) => const EditProfilePage(),
),
GoRoute(
path: '/profile/change-password',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const ChangePasswordPage(),
),

// ─── ERROR ────────────────────────────────────────────────────────
GoRoute(
path: '/404',
builder: (_, __) => const NotFoundPage(),
),
],
);
}

// ─── Transition helpers ─────────────────────────────────────────────────
Page<dynamic> Function(GoRouterState) _slideInRight(Widget Function(GoRouterState) builder) {
return (state) => CustomTransitionPage(
key: state.pageKey,
child: builder(state),
transitionsBuilder: (_, animation, __, child) => SlideTransition(
position: Tween<Offset>(begin: const Offset(1, 0), end: Offset.zero)
.animate(CurvedAnimation(parent: animation, curve: Curves.easeOut)),
child: child,
),
transitionDuration: const Duration(milliseconds: 300),
);
}

Page<dynamic> Function(GoRouterState) _fadeIn(Widget Function(GoRouterState) builder) {
return (state) => CustomTransitionPage(
key: state.pageKey,
child: builder(state),
transitionsBuilder: (_, animation, __, child) => FadeTransition(
opacity: animation,
child: child,
),
transitionDuration: const Duration(milliseconds: 300),
);
}

2. Parent routes (30 screens)

final List<RouteBase> _parentRoutes = [
// Tab routes (6) — all with fade transition, no stack push (NavigationBar swaps body)
GoRoute(
path: '/parent/dashboard',
name: 'parent-dashboard',
pageBuilder: _fadeIn((_) => const ParentDashboardPage()),
),
GoRoute(
path: '/parent/billing',
name: 'parent-billing',
pageBuilder: _fadeIn((_) => const ParentBillingListPage()),
routes: [
GoRoute(
path: ':invoiceId',
name: 'parent-billing-detail',
parentNavigatorKey: _rootNavKey, // push above shell
pageBuilder: _slideInRight((state) => BillingDetailPage(
invoiceId: state.pathParameters['invoiceId']!,
)),
routes: [
GoRoute(
path: 'pay/transfer',
parentNavigatorKey: _rootNavKey,
builder: (_, state) => PaymentTransferPage(
invoiceId: state.pathParameters['invoiceId']!,
),
),
GoRoute(
path: 'pay/va',
parentNavigatorKey: _rootNavKey,
builder: (_, state) => PaymentVirtualAccountPage(
invoiceId: state.pathParameters['invoiceId']!,
),
),
GoRoute(
path: 'pay/qris',
parentNavigatorKey: _rootNavKey,
builder: (_, state) => PaymentQrisPage(
invoiceId: state.pathParameters['invoiceId']!,
),
),
GoRoute(
path: 'pay/success',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const PaymentSuccessPage(),
),
],
),
],
),
GoRoute(
path: '/parent/savings',
name: 'parent-savings',
pageBuilder: _fadeIn((_) => const ParentSavingsPage()),
routes: [
GoRoute(
path: 'deposit',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const SavingsDepositPage(),
),
GoRoute(
path: 'allowance',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const SavingsAllowancePage(),
),
],
),
GoRoute(
path: '/parent/grades',
name: 'parent-grades',
pageBuilder: _fadeIn((_) => const ParentGradesPage()),
routes: [
GoRoute(
path: ':subjectId',
parentNavigatorKey: _rootNavKey,
builder: (_, state) => SubjectDetailPage(
subjectId: state.pathParameters['subjectId']!,
),
),
],
),
GoRoute(
path: '/parent/attendance',
name: 'parent-attendance',
pageBuilder: _fadeIn((_) => const ParentAttendancePage()),
),
GoRoute(
path: '/parent/profile',
name: 'parent-profile',
pageBuilder: _fadeIn((_) => const ParentProfilePage()),
routes: [
GoRoute(
path: 'edit',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const ParentEditProfilePage(),
),
GoRoute(
path: 'change-password',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const ChangePasswordPage(),
),
GoRoute(
path: 'notifications',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const NotificationSettingsPage(),
),
],
),

// Drill-down routes (accessed dari Dashboard Quick Actions / FAB / widget taps)
GoRoute(
path: '/parent/hafalan',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const HafalanProgressPage(),
routes: [
GoRoute(
path: ':setoranId',
builder: (_, state) => SetoranDetailPage(
setoranId: state.pathParameters['setoranId']!,
),
),
],
),
GoRoute(
path: '/parent/announcements',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const AnnouncementListPage(),
routes: [
GoRoute(
path: ':id',
builder: (_, state) => AnnouncementDetailPage(
announcementId: state.pathParameters['id']!,
),
),
],
),
GoRoute(
path: '/parent/permissions',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const PermissionListPage(),
routes: [
GoRoute(
path: ':id',
builder: (_, state) => PermissionDetailPage(
permissionId: state.pathParameters['id']!,
),
),
],
),
GoRoute(
path: '/parent/report-card',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const ReportCardPage(),
),
GoRoute(
path: '/parent/schedule',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const ParentSchedulePage(),
routes: [
GoRoute(
path: ':sessionId',
builder: (_, state) => ScheduleSessionDetailPage(
sessionId: state.pathParameters['sessionId']!,
),
),
],
),
GoRoute(
path: '/parent/chat',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const ChatWaliKelasPage(),
),
GoRoute(
path: '/parent/wali-kelas/:id',
parentNavigatorKey: _rootNavKey,
builder: (_, state) => WaliKelasProfilePage(
id: state.pathParameters['id']!,
),
),
GoRoute(
path: '/parent/notifications',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const NotificationsPage(),
),
];

3. Guru routes (8 screens)

final List<RouteBase> _guruRoutes = [
GoRoute(path: '/guru/dashboard',
pageBuilder: _fadeIn((_) => const GuruDashboardPage())),
GoRoute(path: '/guru/attendance',
pageBuilder: _fadeIn((_) => const GuruAttendanceScannerPage()),
routes: [
GoRoute(path: 'manual/:classId',
parentNavigatorKey: _rootNavKey,
builder: (_, state) => AttendanceManualPage(
classId: state.pathParameters['classId']!,
)),
]),
GoRoute(path: '/guru/grades',
pageBuilder: _fadeIn((_) => const GuruGradeInputClassListPage()),
routes: [
GoRoute(path: ':classId/bulk',
parentNavigatorKey: _rootNavKey,
builder: (_, state) => BulkGradeInputPage(
classId: state.pathParameters['classId']!,
)),
]),
GoRoute(path: '/guru/schedule',
pageBuilder: _fadeIn((_) => const GuruSchedulePage())),
GoRoute(path: '/guru/profile',
pageBuilder: _fadeIn((_) => const GuruProfilePage())),

// Drill-down
GoRoute(path: '/guru/students/:studentId',
parentNavigatorKey: _rootNavKey,
builder: (_, state) => StudentDetailPage(
studentId: state.pathParameters['studentId']!,
)),
GoRoute(path: '/guru/announcements/new',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const SendAnnouncementPage()),
GoRoute(path: '/guru/hafalan/input',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const HafalanInputPage()),
GoRoute(path: '/guru/classes',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const GuruClassListPage(),
routes: [
GoRoute(path: ':classId/students',
builder: (_, state) => ClassStudentListPage(
classId: state.pathParameters['classId']!,
)),
]),
GoRoute(path: '/guru/history',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const GuruTeachingHistoryPage()),

// Staff self-attendance (new screens 387:2, 388:2, 389:2)
GoRoute(path: '/guru/presensi',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const GuruPresensiHariIniPage(),
routes: [
GoRoute(path: 'history',
builder: (_, __) => const GuruRiwayatPresensiPage()),
GoRoute(path: 'izin',
builder: (_, __) => const GuruPengajuanIzinPage()),
]),
];

4. Wali Kelas routes (13 screens) — abbreviated

final List<RouteBase> _waliKelasRoutes = [
// Primary tabs
GoRoute(path: '/wali-kelas/dashboard', pageBuilder: _fadeIn((_) => const WaliKelasDashboardPage())),
GoRoute(path: '/wali-kelas/students',
pageBuilder: _fadeIn((_) => const ClassStudentListPage()),
routes: [
GoRoute(path: ':studentId',
parentNavigatorKey: _rootNavKey,
builder: (_, state) => WaliKelasStudentDetailPage(
studentId: state.pathParameters['studentId']!,
)),
]),
GoRoute(path: '/wali-kelas/grades',
pageBuilder: _fadeIn((_) => const WaliKelasGradesOverviewPage()),
routes: [
GoRoute(path: 'recap',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const GradesRecapPage()),
]),
GoRoute(path: '/wali-kelas/attendance',
pageBuilder: _fadeIn((_) => const WaliKelasAttendanceOverviewPage()),
routes: [
GoRoute(path: 'recap',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const AttendanceRecapPage()),
]),
GoRoute(path: '/wali-kelas/profile', pageBuilder: _fadeIn((_) => const WaliKelasProfilePage())),

// Drill-down
GoRoute(path: '/wali-kelas/permissions/:id/review',
parentNavigatorKey: _rootNavKey,
builder: (_, state) => PermissionApprovalPage(
permissionId: state.pathParameters['id']!,
)),
GoRoute(path: '/wali-kelas/announcements/new',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const AnnouncementComposerPage()),
GoRoute(path: '/wali-kelas/report-cards',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const ReportCardManagementPage(),
routes: [
GoRoute(path: 'generate',
builder: (_, __) => const ReportCardGeneratorPage()),
GoRoute(path: ':id/preview',
builder: (_, state) => ReportCardPreviewPage(
reportCardId: state.pathParameters['id']!,
)),
]),

// Staff self-attendance (new screens 391:2, 391:53, 391:104)
GoRoute(path: '/wali-kelas/presensi',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const WaliKelasPresensiHariIniPage(),
routes: [
GoRoute(path: 'history',
builder: (_, __) => const WaliKelasRiwayatPresensiPage()),
GoRoute(path: 'izin',
builder: (_, __) => const WaliKelasPengajuanIzinPage()),
]),
];

5. Bendahara routes (17 screens) — abbreviated

final List<RouteBase> _bendaharaRoutes = [
GoRoute(path: '/bendahara/dashboard', pageBuilder: _fadeIn((_) => const BendaharaDashboardPage())),
GoRoute(path: '/bendahara/payments',
pageBuilder: _fadeIn((_) => const BendaharaPaymentsPage()),
routes: [
GoRoute(path: 'receive', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const ReceivePaymentPage()),
GoRoute(path: 'verify', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const PaymentVerificationPage()),
GoRoute(path: 'pending', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const PendingPaymentsPage()),
GoRoute(path: 'history', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const PaymentHistoryPage()),
]),
GoRoute(path: '/bendahara/journal',
pageBuilder: _fadeIn((_) => const BendaharaJournalPage()),
routes: [
GoRoute(path: 'new', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const JournalEntryPage()),
]),
GoRoute(path: '/bendahara/reports',
pageBuilder: _fadeIn((_) => const BendaharaReportsPage()),
routes: [
GoRoute(path: 'spp', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const SppReportPage()),
GoRoute(path: 'aging', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const AgingReportPage()),
]),
GoRoute(path: '/bendahara/profile', pageBuilder: _fadeIn((_) => const BendaharaProfilePage())),

// Drill-down
GoRoute(path: '/bendahara/cash',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const CashLedgerPage(),
routes: [
GoRoute(path: 'deposit-bank', builder: (_, __) => const BankDepositPage()),
]),
GoRoute(path: '/bendahara/invoices',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const InvoiceListPage(),
routes: [
GoRoute(path: 'bulk-create', builder: (_, __) => const BulkInvoiceCreatePage()),
]),
GoRoute(path: '/bendahara/discounts',
parentNavigatorKey: _rootNavKey,
builder: (_, __) => const FeeDiscountPage()),
];

6. Kepsek routes (8 screens)

final List<RouteBase> _kepsekRoutes = [
GoRoute(path: '/kepsek/dashboard', pageBuilder: _fadeIn((_) => const KepsekDashboardPage())),
GoRoute(path: '/kepsek/reports', pageBuilder: _fadeIn((_) => const KepsekReportsPage())),
GoRoute(path: '/kepsek/finance', pageBuilder: _fadeIn((_) => const KepsekFinancePage())),
GoRoute(path: '/kepsek/teachers',
pageBuilder: _fadeIn((_) => const TeacherManagementPage())),
GoRoute(path: '/kepsek/profile', pageBuilder: _fadeIn((_) => const KepsekProfilePage())),

// Drill-down
GoRoute(path: '/kepsek/yayasan', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const YayasanDashboardPage(),
routes: [
GoRoute(path: 'units', builder: (_, __) => const EducationUnitListPage()),
]),
GoRoute(path: '/kepsek/audit', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const AuditTrailPage()),
];

7. Admin routes (12 screens)

final List<RouteBase> _adminRoutes = [
GoRoute(path: '/admin/dashboard', pageBuilder: _fadeIn((_) => const AdminDashboardPage())),
GoRoute(path: '/admin/academic',
pageBuilder: _fadeIn((_) => const AdminAcademicPage()),
routes: [
GoRoute(path: 'subjects/:id',
parentNavigatorKey: _rootNavKey,
builder: (_, state) => SubjectDetailPage(
subjectId: state.pathParameters['id']!,
)),
]),
GoRoute(path: '/admin/finance', pageBuilder: _fadeIn((_) => const AdminFinancePage())),
GoRoute(path: '/admin/hr',
pageBuilder: _fadeIn((_) => const AdminHRPage()),
routes: [
GoRoute(path: 'students', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const StudentManagementPage()),
GoRoute(path: 'classes', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const ClassManagementPage()),
]),
GoRoute(path: '/admin/menu', pageBuilder: _fadeIn((_) => const AdminMenuPage())),

// Drill-down
GoRoute(path: '/admin/settings', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const AdminSettingsPage()),
GoRoute(path: '/admin/teaching-schedule', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const TeachingScheduleManagementPage()),
GoRoute(path: '/admin/report-templates', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const ReportTemplateListPage()),
GoRoute(path: '/admin/academic-calendar', parentNavigatorKey: _rootNavKey,
builder: (_, __) => const AcademicCalendarPage()),
];

8. Auth Guard

lib/core/router/auth_guard.dart

import 'package:go_router/go_router.dart';

class AuthState {
final bool isAuthenticated;
final String? role; // 'parent', 'guru', 'wali-kelas', 'bendahara', 'kepsek', 'admin'
final bool hasCompletedOnboarding;

const AuthState({
required this.isAuthenticated,
required this.role,
required this.hasCompletedOnboarding,
});
}

class AuthGuard {
static String? evaluate(GoRouterState state, AuthState auth) {
final loc = state.matchedLocation;

// Always allow splash, onboarding, and error routes
if (loc == '/splash' || loc.startsWith('/onboarding/') || loc == '/404') {
return null;
}

// If not authenticated, only auth pages allowed
if (!auth.isAuthenticated) {
final isAuthRoute = loc.startsWith('/login') || loc.startsWith('/forgot-password');
if (!isAuthRoute) return '/login';
return null;
}

// If authenticated, redirect away from auth pages to persona dashboard
if (loc.startsWith('/login') || loc.startsWith('/forgot-password')) {
return _dashboardFor(auth.role);
}

// Role-based access: can this user access this path?
final allowedPrefix = _allowedPrefix(auth.role);
if (allowedPrefix == null) return '/login';

final isAllowedPersonaPath = loc.startsWith(allowedPrefix) ||
loc.startsWith('/profile/'); // shared authenticated routes

if (!isAllowedPersonaPath) {
return _dashboardFor(auth.role);
}

return null;
}

static String? _dashboardFor(String? role) {
switch (role) {
case 'parent': return '/parent/dashboard';
case 'guru': return '/guru/dashboard';
case 'wali-kelas': return '/wali-kelas/dashboard';
case 'bendahara': return '/bendahara/dashboard';
case 'kepsek': return '/kepsek/dashboard';
case 'admin': return '/admin/dashboard';
default: return '/login';
}
}

static String? _allowedPrefix(String? role) {
switch (role) {
case 'parent': return '/parent/';
case 'guru': return '/guru/';
case 'wali-kelas': return '/wali-kelas/';
case 'bendahara': return '/bendahara/';
case 'kepsek': return '/kepsek/';
case 'admin': return '/admin/';
default: return null;
}
}
}

9. Example Shell (ParentShell)

lib/core/router/shells/parent_shell.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class ParentShell extends StatelessWidget {
final String currentLocation;
final Widget child;

const ParentShell({
super.key,
required this.currentLocation,
required this.child,
});

static const _tabs = [
(path: '/parent/dashboard', label: 'Beranda', icon: Icons.home_rounded),
(path: '/parent/billing', label: 'Tagihan', icon: Icons.receipt_long_rounded),
(path: '/parent/savings', label: 'Tabungan', icon: Icons.savings_rounded),
(path: '/parent/grades', label: 'Nilai', icon: Icons.grade_rounded),
(path: '/parent/attendance',label: 'Absensi', icon: Icons.fact_check_rounded),
(path: '/parent/profile', label: 'Profil', icon: Icons.person_rounded),
];

int get _currentIndex {
final idx = _tabs.indexWhere((t) => currentLocation.startsWith(t.path));
return idx == -1 ? 0 : idx;
}


Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (idx) => context.go(_tabs[idx].path),
destinations: _tabs
.map((t) => NavigationDestination(
icon: Icon(t.icon),
label: t.label,
))
.toList(),
),
);
}
}

Ulang pattern yang sama untuk GuruShell, WaliKelasShell, BendaharaShell, KepsekShell, AdminShell — hanya ganti _tabs sesuai mapping di Doc 08.


10. Transition reference (mirroring Figma prototype)

Transition typeWhen to usego_router implementation
No transition (instant)Bottom nav tab switchDefault NavigationBar onDestinationSelected + context.go() (swaps body without animation)
Fade (300ms)Push drill-down, modal closeCustomTransitionPage dengan FadeTransition
Slide in from right (300ms)Forward push (e.g. onboarding, list → detail)CustomTransitionPage dengan SlideTransition Offset(1,0)→(0,0)
Slide out to left (300ms)Back swipe / manual backAuto-inverted by default pop animation

11. Wiring notes

  1. Create lib/core/router/ folder with files: app_router.dart, auth_guard.dart, shells/*.dart.
  2. In main.dart:
    void main() => runApp(const ProviderScope(child: AmalSantriApp()));

    class AmalSantriApp extends ConsumerWidget {
    const AmalSantriApp({super.key});

    Widget build(BuildContext context, WidgetRef ref) {
    final auth = ref.watch(authStateProvider);
    return MaterialApp.router(
    title: 'Amal E-Santri',
    theme: AppTheme.light(),
    darkTheme: AppTheme.dark(),
    themeMode: ThemeMode.system,
    routerConfig: buildRouter(authState: auth),
    );
    }
    }
  3. Stub every page class with Scaffold(appBar: AppBar(title: Text('X')), body: Center(child: Text('TODO'))) so router compiles on day 1. Replace iteratively.

See also

  • Doc 08 (08-flutter-handoff-map.md) — full Figma-to-widget mapping
  • Doc 10 (10-widget-component-spec.md) — shared widgets used by routes