A progressive, router-agnostic navigation layer for Flutter that allows you to wrap existing GoRouter apps and migrate to a clean, testable, decoupled architecture — without rewriting your routes.
Flutter apps are usually tightly coupled to a routing library:
// ❌ Your business logic depends on GoRouter
context.go('/users/42');
context.push('/settings');This creates several problems:
- Vendor lock-in: Business logic depends on GoRouter/AutoRoute
- Untestable: Navigation requires Flutter widgets
- Expensive migrations: Changing routers means rewriting navigation
- Tight coupling: UI and routing are inseparable
Nav Bridge solves this with a thin abstraction layer:
// ✅ Your app talks to AppRouter, not GoRouter
appRouter.goToUserProfile('42');| Feature | Description |
|---|---|
| Wrap Mode | Use your existing GoRouter without any changes |
| Progressive Migration | Migrate one feature at a time, old & new coexist |
| Full DI Support | Riverpod Ref available in all guards |
| Guard Bridges | Keep existing guards working immediately |
| InMemoryAdapter | Unit test navigation without Flutter |
| Shell Navigation | Full StatefulShellRoute support |
dependencies:
nav_bridge: ^2.0.0
go_router: ^15.0.0
# Optional - for Riverpod guards
flutter_riverpod: ^2.4.0flutter pub getZero changes to your existing code:
import 'package:nav_bridge/nav_bridge.dart';
// Your existing GoRouter (unchanged)
final goRouter = GoRouter(
routes: [...],
redirect: myRedirectLogic,
);
// Wrap it with Nav Bridge
final adapter = GoRouterAdapter.wrap(goRouter);
// Everything still works!
context.go('/profile/42'); // ✅ Still worksfinal adapter = GoRouterAdapter.wrap(
goRouter,
additionalGuards: [AuthGuard(), RoleGuard()],
);
// Inject dependencies (Riverpod Ref, etc.)
adapter.contextBuilder = (state) => {
'ref': ref,
'goRouterState': state,
'context': navigatorKey.currentContext,
};abstract class AppRouter {
Future<void> goToHome();
Future<void> goToUserProfile(String userId);
}
class MyAppRouter implements AppRouter {
final GoRouterAdapter _adapter;
MyAppRouter(this._adapter);
@override
Future<void> goToUserProfile(String userId) =>
_adapter.go('/profile/$userId');
}class AuthGuard extends RiverpodRouteGuard {
@override
int get priority => 100; // Higher = runs first
@override
List<String>? get excludes => ['/login', '/register'];
@override
Future<GuardResult> canActivateWithRef(
GuardContext context,
Ref ref,
) async {
final isAuthenticated = ref.read(authProvider).isAuthenticated;
if (!isAuthenticated) {
return GuardResult.redirect('/login');
}
return GuardResult.allow();
}
}Already have guards? Bridge them without any changes:
// Your existing guard function
FutureOr<GuardResult> myExistingGuard(
BuildContext context,
GoRouterState state,
Ref ref,
) async {
// Your existing logic...
}
// Bridge it - zero changes needed!
final bridgedGuard = GoRouterGuardBridge(myExistingGuard);sealed class GuardResult {
// Allow navigation
static GuardAllow allow();
// Redirect to another path
static GuardRedirect redirect(String path, {Map<String, dynamic>? extra});
// Block navigation
static GuardReject reject({String? reason});
}Test navigation without Flutter widgets:
void main() {
group('Navigation', () {
test('authenticated user can access profile', () async {
final router = InMemoryAdapter(
guards: [MockAuthGuard(isAuthenticated: true)],
);
await router.go('/profile/42');
expect(router.currentLocation, '/profile/42');
});
test('unauthenticated user is redirected to login', () async {
final router = InMemoryAdapter(
guards: [MockAuthGuard(isAuthenticated: false)],
);
await router.go('/profile/42');
expect(router.currentLocation, '/login');
});
test('tracks navigation history', () async {
final router = InMemoryAdapter();
await router.go('/');
await router.push('/profile/42');
await router.push('/settings');
expect(router.navigationHistory, ['/', '/profile/42', '/settings']);
router.pop();
expect(router.currentLocation, '/profile/42');
});
});
}// Just wrap, nothing else changes
final adapter = GoRouterAdapter.wrap(existingRouter);// Bridge existing guards
final bridged = GoRouterGuardBridge(existingGuard);// Create type-safe navigation
abstract class AppRouter {
Future<void> goToProfile(String id);
}// Old and new coexist
context.go('/old'); // Old code ✅
appRouter.goToProfile('42'); // New code ✅┌─────────────────────────────────────────────────────────┐
│ Feature Code │
│ (Uses AppRouter interface) │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ AppRouter │
│ (Your type-safe abstraction) │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Nav Bridge │
│ (Guards, DI, Navigation abstraction) │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ GoRouterAdapter.wrap() │
│ (Wraps your existing router) │
└─────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Your Existing GoRouter │
│ (Routes, Shell, everything intact) │
└─────────────────────────────────────────────────────────┘
| Adapter | Use Case |
|---|---|
GoRouterAdapter.wrap() |
Existing GoRouter apps (recommended) |
GoRouterAdapter.create() |
New applications |
GoRouterAdapter.withGuards() |
New with integrated guard system |
InMemoryAdapter |
Unit testing |
| Class | Description |
|---|---|
RouteGuard |
Base class for all guards |
RiverpodRouteGuard |
Guard with Riverpod Ref access |
GoRouterGuardBridge |
Bridge for existing guards with Ref |
SimpleGoRouterGuardBridge |
Bridge for guards without Ref |
GoRouterRedirectBridge |
Bridge for redirect functions |
CompositeGuard |
Combine guards with AND logic |
AnyGuard |
Combine guards with OR logic |
| Class | Description |
|---|---|
GuardContext |
Context passed to guards with DI support |
GuardResult |
Sealed class: Allow, Redirect, Reject |
RouteDefinition |
Router-agnostic route definition |
ShellRouteDefinition |
Shell/tab navigation support |
- GoRouter wrap mode
- Riverpod guard support
- Guard bridge adapters
- InMemoryAdapter for testing
- Shell navigation support
- AutoRoute adapter
- Beamer adapter
- Typed route code generation
- Analytics observers
- Transition abstraction
| Scenario | Recommendation |
|---|---|
| Existing GoRouter app | Perfect fit |
| Large team / enterprise | Highly recommended |
| Need navigation unit tests | Essential |
| Planning router migration | Future-proof |
| Small personal app | Optional |
Contributions are welcome! Please read our Contributing Guide first.
# Clone the repo
git clone https://github.com/chekarhamza88-stack/nav_bridge.git
# Install dependencies
flutter pub get
# Run tests
flutter test
# Check analysis
flutter analyzeMIT License - see LICENSE for details.
Nav Bridge doesn't replace GoRouter.
It makes GoRouter testable, replaceable, decoupled, and enterprise-ready.
Made with love by chekarhamza88-stack
