dartway_router 1.0.2
dartway_router: ^1.0.2 copied to clipboard
Opinionated wrapper around go_router that makes routing explicit, predictable, and scalable for real-world Flutter apps.
DartWay Router #
A powerful, type-safe navigation package for Flutter that wraps Go Router with an intuitive enum-based API. Build navigation systems with compile-time safety, route guards, and flexible page transitions.
Two full example projects are available in the package repository in the examples/ folder: change_notifier_example (state via ChangeNotifier) and riverpod_example (state via Riverpod).
Features #
- 🎯 Type-Safe Navigation: Enum-based routes with compile-time checking
- 🔄 State Management Agnostic: Works with any
Listenable(ChangeNotifier, ValueNotifier, Riverpod, etc.) - 🛡️ Route Guards: Protect routes with authentication/authorization guards
- 📊 Type-Safe Parameters: Extract navigation parameters with full type safety
- 🎭 Flexible Transitions: Built-in page transitions (material, fade, slide, scale)
- 🏗️ Navigation Zones: Group routes into logical zones (authenticated, public, admin, etc.)
- 🐚 Shell Routes: Easy shell route configuration for common UI patterns
- ✅ Comprehensive Validation: Automatic validation of route configuration
- 📱 Zero Dependencies: Only depends on Flutter and Go Router
Installation #
flutter pub add dartway_router
Quick Start #
1. Define Your Router State #
Create a state class that extends Listenable (or use ChangeNotifier, ValueNotifier, etc.):
import 'package:flutter/material.dart';
class AppSession extends ChangeNotifier {
bool _isAuthenticated = false;
bool get isAuthenticated => _isAuthenticated;
void login() {
_isAuthenticated = true;
notifyListeners();
}
void logout() {
_isAuthenticated = false;
notifyListeners();
}
}
2. Define Navigation Parameters #
Create an enum for your navigation parameters:
enum AppParams<T> with DwNavigationParamsMixin<T> {
userId<int>(),
userName<String>(),
itemId<int>(),
}
3. Define Your Routes (Two Zones) #
We use two zones: a public auth zone (login) and a protected app zone (home, profile, etc.). The app zone uses a guard that checks AppSession and redirects unauthenticated users to login.
Auth zone (e.g. login screen):
enum AuthRoutes implements DwNavigationRoute<AppSession> {
login(
DwNavigationRouteDescriptor.zoneRoot(pageWidget: LoginPage()),
);
const AuthRoutes(this.descriptor);
@override
final DwNavigationRouteDescriptor<AppSession> descriptor;
@override
String get zoneRoot => 'auth';
@override
DwShellRoutePageBuilder? get shellRouteBuilder => null;
@override
List<DwNavigationGuard<AppSession>> get zoneGuards => [];
}
App zone (protected; guard uses AppSession):
enum AppRoutes implements DwNavigationRoute<AppSession> {
home(
DwNavigationRouteDescriptor.zoneRoot(pageWidget: HomePage()),
),
profile(
DwNavigationRouteDescriptor.simple(pageWidget: ProfilePage()),
),
userDetail(
DwNavigationRouteDescriptor.parameterized(
pageWidget: UserDetailPage(),
parameter: AppParams.userId,
parent: home,
extraPathSegment: 'users',
),
);
const AppRoutes(this.descriptor);
@override
final DwNavigationRouteDescriptor<AppSession> descriptor;
@override
String get zoneRoot => '';
@override
DwShellRoutePageBuilder? get shellRouteBuilder => null;
@override
List<DwNavigationGuard<AppSession>> get zoneGuards => [
(session) {
if (!session.isAuthenticated) {
return AuthRoutes.login.fullPath;
}
return null; // Allow navigation
},
];
}
4. Create the Router #
Pass both zones and routerState (required for guards). Start at login; the guard will redirect to login when the user is not authenticated.
final appSession = AppSession();
final router = DwRouter<AppSession>(
routerState: appSession,
navigationZones: [
AuthRoutes.values, // Auth zone (login)
AppRoutes.values, // App zone (protected by guard)
],
pageBuilder: DwPageBuilder.material,
options: DwGoRouterOptions(
initialLocation: AuthRoutes.login.fullPath,
debugLogDiagnostics: true,
),
);
5. Use in Your App #
import 'package:flutter/material.dart';
import 'router/app_router.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'DartWay Router Example',
theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
routerConfig: router.router,
);
}
}
6. Navigate and Extract Parameters #
Use AppSession on login/logout and navigate between zones:
// On login screen: after successful login
void onLoginPressed() {
appSession.login();
context.goNamed(AppRoutes.home.name);
}
// On profile/settings: logout
void onLogoutPressed() {
appSession.logout();
context.goNamed(AuthRoutes.login.name);
}
// Navigate within app zone
context.goNamed(AppRoutes.profile.name);
context.goNamed(
AppRoutes.userDetail.name,
pathParameters: AppParams.userId.set(123),
);
// Extract parameters in a widget
class UserDetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final userId = AppParams.userId.fromPath(context);
return Text('User ID: $userId');
}
}
Core Concepts #
Navigation Zones #
Navigation zones allow you to group related routes together. Each zone can have:
- A shared shell route builder (e.g., bottom navigation bar)
- Common route guards (e.g., authentication checks)
- A zone root route
Example with multiple zones (e.g. auth zone + protected app zone, as in Quick Start):
final router = DwRouter<AppSession>(
routerState: appSession,
navigationZones: [
AuthRoutes.values, // Public/auth zone (login)
AppRoutes.values, // Authenticated zone (guard uses AppSession)
AdminRoutes.values, // Optional: admin zone
],
pageBuilder: DwPageBuilder.material,
);
Route Types #
Zone Root Route
The entry point of a navigation zone. Contributes an empty path segment.
home(
DwNavigationRouteDescriptor.zoneRoot(pageWidget: HomePage()),
)
Accessible at / (assuming zoneRoot is empty).
Simple Route
A route without parameters. Contributes its enum name as the path.
profile(
DwNavigationRouteDescriptor.simple(pageWidget: ProfilePage()),
)
Accessible at /profile.
Parameterized Route
A route with path parameters. Must have a parent route.
userDetail(
DwNavigationRouteDescriptor.parameterized(
pageWidget: UserDetailPage(),
parameter: AppParams.userId,
parent: home,
),
)
Accessible at /:userId (relative to parent).
With extraPathSegment:
userDetail(
DwNavigationRouteDescriptor.parameterized(
pageWidget: UserDetailPage(),
parameter: AppParams.userId,
parent: home,
extraPathSegment: 'users',
),
)
Accessible at /users/:userId.
Route Guards #
Guards allow you to protect routes with authentication or authorization checks. Guards are executed in order, and if any guard returns a redirect path, navigation is redirected.
enum AppRoutes implements DwNavigationRoute<AppSession> {
// ... routes ...
@override
List<DwNavigationGuard<AppSession>> get zoneGuards => [
(session) {
if (!session.isAuthenticated) {
return AuthRoutes.login.fullPath;
}
return null; // Allow navigation
},
];
}
Important: When using guards, you must provide routerState to DwRouter:
final router = DwRouter<AppSession>(
routerState: appSession, // Required when using guards
navigationZones: [AppRoutes.values],
pageBuilder: DwPageBuilder.material,
);
Shell Routes #
Shell routes allow you to wrap routes in a common UI shell, such as a scaffold with a bottom navigation bar.
enum AppRoutes implements DwNavigationRoute<AppSession> {
// ... routes ...
@override
DwShellRoutePageBuilder? get shellRouteBuilder =>
(context, state, child) {
final currentRoot = router.rootRouteFromState(state);
final currentIndex = currentRoot == AppRoutes.profile ? 1 : 0;
return MaterialPage(
child: Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentIndex,
onTap: (index) {
switch (index) {
case 0:
context.goNamed(AppRoutes.home.name);
break;
case 1:
context.goNamed(AppRoutes.profile.name);
break;
}
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
),
);
};
}
Type-Safe Parameters #
Navigation parameters are defined using enums with DwNavigationParamsMixin:
enum AppParams<T> with DwNavigationParamsMixin<T> {
userId<int>(),
userName<String>(),
price<double>(),
isActive<bool>(),
}
Extracting Parameters
// From path parameters (required)
final userId = AppParams.userId.fromPath(context);
// From path parameters (nullable)
final userId = AppParams.userId.fromPathOrNull(context);
// From query parameters (nullable)
final searchQuery = AppParams.userName.fromQueryOrNull(context);
// From query parameters (required)
final searchQuery = AppParams.userName.fromQuery(context);
// From extra data (nullable)
final data = AppParams.userId.fromExtra(context);
Setting Parameters for Navigation
// Navigate with path parameter
context.goNamed(
AppRoutes.userDetail.name,
pathParameters: AppParams.userId.set(123),
);
// Navigate with query parameter
context.go(
'/search?${AppParams.userName.set('flutter').entries.first.key}=${AppParams.userName.set('flutter').entries.first.value}',
);
Page Transitions #
Choose from built-in transitions or create custom ones:
// Material (no transition)
pageBuilder: DwPageBuilder.material
// Fade transition
pageBuilder: DwPageBuilder.fade
// Slide transition (from right by default)
pageBuilder: DwPageBuilder.slide
// Slide from bottom
pageBuilder: (context, key, child) =>
DwPageBuilder.slide(context, key, child, from: AxisDirection.bottom)
// Scale transition
pageBuilder: DwPageBuilder.scale
// Custom transition
pageBuilder: (context, key, child) {
return CustomTransitionPage(
key: key,
child: child,
transitionDuration: Duration(milliseconds: 500),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return RotationTransition(
turns: animation,
child: child,
);
},
);
}
Advanced Usage #
Multiple Navigation Zones #
Organize your app into multiple navigation zones:
enum AppRoutes implements DwNavigationRoute<AppSession> {
home(DwNavigationRouteDescriptor.zoneRoot(pageWidget: HomePage())),
profile(DwNavigationRouteDescriptor.simple(pageWidget: ProfilePage()));
// ... implementation ...
}
enum AuthRoutes implements DwNavigationRoute<AppSession> {
login(DwNavigationRouteDescriptor.simple(pageWidget: LoginPage())),
signup(DwNavigationRouteDescriptor.simple(pageWidget: SignupPage()));
// ... implementation ...
}
final router = DwRouter<AppSession>(
routerState: appSession,
navigationZones: [
AppRoutes.values, // Authenticated zone
AuthRoutes.values, // Public zone
],
pageBuilder: DwPageBuilder.material,
);
Nested Routes #
Create nested route hierarchies:
enum AppRoutes implements DwNavigationRoute<AppSession> {
home(DwNavigationRouteDescriptor.zoneRoot(pageWidget: HomePage())),
// Child route
settings(
DwNavigationRouteDescriptor.simple(
pageWidget: SettingsPage(),
parent: home,
),
),
// Nested child route
profileSettings(
DwNavigationRouteDescriptor.simple(
pageWidget: ProfileSettingsPage(),
parent: settings,
),
),
}
Route Paths #
Understanding route paths:
-
routePath: The path segment for this route- Root routes: Full path from zone root (e.g.,
/profile) - Child routes: Relative path segment only (e.g.,
:userId)
- Root routes: Full path from zone root (e.g.,
-
fullPath: The complete path from root- Always includes the full hierarchy (e.g.,
/users/:userId)
- Always includes the full hierarchy (e.g.,
// For a child route with parent
print(AppRoutes.userDetail.routePath); // ':userId'
print(AppRoutes.userDetail.fullPath); // '/:userId' (includes parent)
Route Resolution #
Get the current route from navigation state:
// Get the top route (the route at the top of the stack)
final topRoute = router.topRouteFromState(GoRouterState.of(context));
// Get the root route (the zone root)
final rootRoute = router.rootRouteFromState(GoRouterState.of(context));
// Check if a route is active
if (AppRoutes.profile.isActive(context)) {
// Route is currently active
}
Custom Router Options #
Configure GoRouter behavior:
final router = DwRouter<AppSession>(
routerState: appSession,
navigationZones: [AppRoutes.values],
pageBuilder: DwPageBuilder.material,
options: DwGoRouterOptions(
initialLocation: '/home',
debugLogDiagnostics: true,
redirectLimit: 10,
errorBuilder: (context, state) => ErrorPage(),
redirect: (context, state) {
// Custom redirect logic
return null;
},
),
);
API Reference #
Core Classes #
DwRouter<RouterState>
Main router class that wraps GoRouter.
Properties:
routerState- Optional router state for refresh notificationsnavigationZones- List of navigation zone route listspageBuilder- Function to build pages with transitionsoptions- GoRouter configuration optionsrouter- The underlying GoRouter instance
Methods:
topRouteFromState(state)- Get top route from navigation staterootRouteFromState(state)- Get root route from navigation state
DwNavigationRoute<RouterState>
Abstract interface for navigation routes. Routes are defined as enums implementing this interface.
Required Properties:
descriptor- Route descriptor defining path and pagezoneRoot- Root path segment for the navigation zoneshellRouteBuilder- Optional shell route builderzoneGuards- List of navigation guards
DwNavigationRouteDescriptor<RouterState>
Describes how a route contributes to the URL path.
Factory Constructors:
zoneRoot({required pageWidget})- Zone root routesimple({required pageWidget, parent, extraPathSegment})- Simple routeparameterized({required pageWidget, required parameter, required parent, extraPathSegment})- Parameterized route
DwNavigationParamsMixin<T>
Mixin for type-safe navigation parameters.
Methods:
set(value)- Create parameter map for navigationfromPath(context)- Extract from path parameters (required)fromPathOrNull(context)- Extract from path parameters (nullable)fromQuery(context)- Extract from query parameters (required)fromQueryOrNull(context)- Extract from query parameters (nullable)fromExtra(context)- Extract from extra data (nullable)
DwPageBuilder
Collection of predefined page builders.
Static Methods:
material(context, key, child)- Material page (no transition)fade(context, key, child, {curve, duration})- Fade transitionslide(context, key, child, {from, curve, duration})- Slide transitionscale(context, key, child, {curve, duration})- Scale transition
DwGoRouterOptions
Configuration options for GoRouter.
Properties:
navigatorKey- Navigator keyinitialLocation- Initial route patherrorBuilder- Custom error page builderredirect- Custom redirect functiondebugLogDiagnostics- Enable debug logging- And more...
Extensions #
DwNavigationRouteExtension
Extension on DwNavigationRoute providing:
routePath- Route path (relative for child routes)fullPath- Full path from rootisActive(context)- Check if route is currently active
Examples #
Complete working examples are available in the /examples directory:
- change_notifier_example - Example using ChangeNotifier for state management
- riverpod_example - Example using Riverpod for state management
Project Structure #
Recommended project structure:
lib/
├── main.dart
├── router/
│ ├── app_router.dart # Router configuration
│ └── zones/
│ ├── app_routes.dart # App routes enum
│ └── auth_routes.dart # Auth routes enum
├── pages/
│ ├── home_page.dart
│ ├── profile_page.dart
│ └── user_detail_page.dart
└── core/
└── app_session.dart # Router state (ChangeNotifier, etc.)
Best Practices #
- Use enums for routes: Provides compile-time safety and autocomplete
- Group related routes: Use navigation zones to organize routes logically
- Use guards for protection: Protect routes with guards rather than checking in widgets
- Type-safe parameters: Always use
DwNavigationParamsMixinfor parameters - Consistent naming: Use consistent naming conventions for routes and parameters
- Shell routes for common UI: Use shell routes for bottom nav, sidebars, etc.
Troubleshooting #
Routes not found #
- Ensure all routes are included in
navigationZones - Check that route names are unique
- Verify that paths don't conflict
Guards not working #
- Ensure
routerStateis provided toDwRouter - Check that guards return
nullto allow navigation - Verify guard logic is correct
Parameters not extracted #
- Ensure parameter name matches the route path parameter
- Check parameter type matches the mixin type
- Use
fromPathOrNullif parameter might be missing
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
License #
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.