- π Authentication-aware routing - Automatically redirect users based on auth state
- π State-driven navigation - Route changes respond to Riverpod state changes
- π Simplified navigation flow - Handle onboarding, authentication flow, and protected routes
- π Declarative route definition - Define protected and public routes clearly
- πͺ Easy integration - Works with your existing Go Router and Riverpod setup
- π Initial data loading - Wait for required data before navigation starts
dependencies:
pod_router : ^0.0.1
Run:
flutter pub get
import 'package:pod_router/pod_router.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
// 1. Create your auth notifier
class MyAuthNotifier extends BaseAuthNotifier {
MyAuthNotifier(Ref ref) : super(ref);
@override
void initialize() {
// Your auth initialization logic here
}
Future<void> login() async {
setLoading();
// Login implementation
setAuthenticated();
}
Future<void> logout() async {
setLoading();
// Logout implementation
setUnauthenticated();
}
}
// 2. Create the auth provider
final authProvider = createAuthNotifierProvider<MyAuthNotifier>(
(ref) => MyAuthNotifier(ref)
);
// 3. Define your routes manager
class AppRoutesManager extends RoutesManager {
AppRoutesManager(Ref ref) : super(ref);
@override
List<String> get protectedRoutes => ['/profile', '/settings'];
@override
List<String> get publicRoutes => ['/login', '/register'];
@override
String get splashRoute => '/splash';
@override
String get defaultAuthenticatedRoute => '/home';
@override
String get defaultUnauthenticatedRoute => '/login';
// 4. Define initial data providers (NEW!)
@override
List<ProviderListenable> get initialDataProviders => [
userDataProvider,
settingsProvider,
];
@override
void initializeListeners() {
super.initializeListeners(); // Don't forget this for initial data loading!
// Connect auth state to router
ref.listen(
authProvider.select((value) => value.status),
(prev, next) {
authState.value = next;
},
fireImmediately: true,
);
// Connect loading state
ref.listen(authProvider.select((value) => value.isLoading),
(prev, next) {
isLoading.value = next;
},
fireImmediately: true,
);
}
}
// 5. Create the router provider
final routesProvider = routesManagerProvider<AppRoutesManager>(
(ref) => AppRoutesManager(ref)
);
// 6. Set up the Go Router
final routerProvider = Provider<GoRouter>((ref) {
final routesManager = ref.watch(routesProvider);
return GoRouter(
redirect: routesManager.onRedirect,
refreshListenable: Listenable.merge(routesManager.refreshables),
routes: [
// Your routes here
],
);
});
The package provides a BaseAuthNotifier
class that handles authentication state. Extend this class to implement your specific authentication logic:
class FirebaseAuthNotifier extends BaseAuthNotifier {
FirebaseAuthNotifier(Ref ref) : super(ref);
@override
void initialize() {
// Listen to Firebase auth changes
FirebaseAuth.instance.authStateChanges().listen((user) {
if (user != null) {
setAuthenticated();
} else {
setUnauthenticated();
}
});
}
Future<void> signIn(String email, String password) async {
setLoading();
try {
await FirebaseAuth.instance.signInWithEmailAndPassword(
email: email,
password: password,
);
} catch (e) {
setUnauthenticated();
rethrow;
}
}
Future<void> signOut() async {
setLoading();
await FirebaseAuth.instance.signOut();
}
}
The RoutesManager
class handles route redirects based on authentication state:
class MyRoutesManager extends RoutesManager {
MyRoutesManager(Ref ref) : super(ref);
@override
List<String> get protectedRoutes => [
'/profile',
'/settings',
'/dashboard'
];
@override
List<String> get publicRoutes => [
'/login',
'/register',
'/forgot-password'
];
@override
String get splashRoute => '/splash';
@override
String get defaultAuthenticatedRoute => '/home';
@override
String get defaultUnauthenticatedRoute => '/login';
@override
String? get initialAppFlowRoute => '/onboarding';
@override
void initializeListeners() {
super.initializeListeners(); // Important for initial data loading!
// Listen to auth state
ref.listen(
authProvider.select((value) => value.status),
(prev, next) {
authState.value = next;
},
fireImmediately: true,
);
// Optional: Listen to onboarding completion
ref.listen(onboardingCompletedProvider, (prev, next) {
appFlowNotifier.value = !next;
});
// Other state listeners...
}
}
The package now supports waiting for initial data to load before navigation:
class MyRoutesManager extends RoutesManager {
// ... other overrides
@override
List<ProviderListenable> get initialDataProviders => [
// Add providers that need to be loaded before navigation begins
userDataProvider,
themeProvider,
localizationProvider,
// Any async provider that should complete before navigation
];
}
When these providers are specified, the router will:
- Show the splash screen until all data is loaded
- Register the routes manager with the system
- Track loading state automatically
- Provide error handling for data loading failures
Integrate with Go Router with Route Manager
final goRouterProvider = Provider<GoRouter>((ref) {
final routesManager = ref.watch(routesManagerProvider);
return GoRouter(
debugLogDiagnostics: true,
redirect: routesManager.onRedirect,
refreshListenable: Listenable.merge(routesManager.refreshables),
initialLocation: '/splash',
routes: [
// Your routes...
],
);
});
See the example folder for a complete implementation.
Enable debug logs to help troubleshoot routing issues:
void main() {
PackageLogger.enableDebugLogs = true;
runApp(ProviderScope(child: MyApp()));
}
You can override the onRedirect
method in your RoutesManager
subclass to customize the redirect logic:
@override
FutureOr<String?> onRedirect(BuildContext context, GoRouterState state) async {
// Custom logic before calling super
if (specialCondition) {
return '/special-route';
}
// Use default logic
return await super.onRedirect(context, state);
}
You can add additional ValueNotifier
objects to trigger route refreshes:
final themeChanged = ValueNotifier<bool>(false);
@override
List<ChangeNotifier> get refreshables => [
...super.refreshables,
themeChanged,
];
The package provides a utility to wait for authentication state to be determined:
Future<void> loadInitialData() async {
// Wait for auth state before proceeding
final authStatus = await waitForAuthState(ref, authProvider);
// Now you can perform actions based on auth status
if (authStatus == AuthStatus.authenticated) {
await ref.read(userDataProvider.notifier).loadUserData();
}
}
- Add more Examples
- reducing boilerplate in defining public and protected routes
- enhancing the auth notifier workflow
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE file for details.