Flutter's built-in navigation works fine for small apps. But the moment you need auth guards, deep links, typed arguments, and the ability to show a dialog without a BuildContext — you're either drowning in boilerplate or relying on a package that generates 500 lines of code every time you add a route.
I wanted something in between: a clean, composable API that covers 95% of real-world routing needs, ships as a single package, and stays out of the way.
That's RoutePilot — one singleton (routePilot) that handles push/pop, middleware, overlays, URL launching, and more.
✨ What's inside
| Category | Capabilities |
|---|---|
| Navigation | Push, pop, replace, clear stack, pop-until by name or predicate |
| Transitions | 9 built-in transitions with custom curves and durations |
| Deep Linking | Navigator 2.0 Router API with automatic browser URL sync |
| Middleware | Async guards with FutureOr<String?> redirect — auth, loading states |
| Typed Routes |
PilotRoute<TArgs, TReturn> for compile-safe navigation |
| Route Groups | Shared prefix, middleware, and transition across related routes |
| Overlays | Dialogs, bottom sheets, snackbars, loading overlays — no BuildContext
|
| URL Launcher | Browser, in-app WebView, phone calls, SMS, email |
| Observer | Built-in route stack tracking with currentRoute and previousRoute
|
🚀 Get started in 3 minutes
1. Install
# pubspec.yaml
dependencies:
route_pilot: ^0.2.0
flutter pub get
2. Define your routes
abstract class PilotRoutes {
static const String home = '/';
static const String profile = '/profile';
static const String user = '/user/:id';
}
final pages = [
PilotPage(
name: PilotRoutes.home,
page: (ctx) => const HomePage(),
transition: Transition.ios,
),
PilotPage(
name: PilotRoutes.profile,
page: (ctx) => const ProfilePage(),
transition: Transition.fadeIn,
middlewares: [AuthGuard()], // 👈 guard this route
),
];
3. Wire up your app
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: routePilot.getRouterConfig(
pages: pages,
initialRoute: PilotRoutes.home,
notFoundPage: PilotPage(
name: '/404',
page: (ctx) => const NotFoundPage(),
),
),
);
}
}
That's it — deep linking, browser URL sync, and the full navigation API are ready.
🗺️ Navigation API
// Push / pop
routePilot.toNamed('/profile');
routePilot.back();
// Pass arguments
routePilot.toNamed('/profile', arguments: {'userId': 42});
final id = routePilot.arg<int>('userId');
// Replace or clear the stack
routePilot.off('/home'); // replace current route
routePilot.offAll('/home'); // clear stack + push
// Path + query params → /user/42?tab=settings
routePilot.toNamed('/user/42?tab=settings');
final uid = routePilot.param('id'); // '42'
final tab = routePilot.param('tab'); // 'settings'
// Navigate directly to a widget (no route name needed)
routePilot.to(
const DetailsPage(),
transition: Transition.bottomToTop,
transitionDuration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
🎞️ All 9 transitions, zero config
| Transition | Effect |
|---|---|
Transition.ios |
Native Cupertino swipe-back |
Transition.fadeIn |
Cross-fade |
Transition.rightToLeft |
Standard push slide |
Transition.leftToRight |
Slide from left |
Transition.bottomToTop |
Sheet-style rise |
Transition.topToBottom |
Drop-down |
Transition.scale |
Scale up from center |
Transition.rotate |
Rotation |
Transition.size |
Size expansion |
Every transition accepts a custom curve and transitionDuration.
🔒 Async middleware — the right way to handle auth
Middleware runs before a route renders. Return a redirect path to block navigation, or null to allow it.
class AuthGuard extends PilotMiddleware {
@override
FutureOr<String?> redirect(String? route) async {
final isLoggedIn = await checkAuthToken();
if (!isLoggedIn) return '/login'; // redirect
return null; // allow
}
}
// Show a spinner while the guard resolves
routePilot.middlewareLoadingWidget =
const Center(child: CircularProgressIndicator());
Middleware can be attached to individual PilotPages or shared across a PilotRouteGroup. Nested groups cascade automatically.
📦 Route groups — keep things organized
PilotRouteGroup(
prefix: '/dashboard',
middlewares: [AuthGuard()],
transition: Transition.fadeIn,
children: [
PilotPage(name: '/home', page: (ctx) => DashboardHome()),
PilotPage(name: '/settings', page: (ctx) => SettingsPage()),
// resolves to: /dashboard/home, /dashboard/settings
],
)
💬 Overlays without BuildContext
Show any overlay from a ViewModel, a service class, or anywhere else in your app — no context threading required.
// Dialog with a typed return value
final confirmed = await routePilot.dialog(AlertDialog(
title: const Text('Delete item?'),
actions: [
TextButton(onPressed: () => routePilot.back(false), child: const Text('Cancel')),
TextButton(onPressed: () => routePilot.back(true), child: const Text('Delete')),
],
));
// SnackBar — queue-safe, clears any previous snackbar automatically
routePilot.snackBar('Saved!', backgroundColor: Colors.green);
// Non-dismissible full-screen loading overlay
routePilot.showLoading();
await uploadFile();
routePilot.hideLoading();
// Bottom sheet
routePilot.bottomSheet(
const MyBottomSheetWidget(),
isScrollControlled: true,
);
🧩 Typed routes — compile-time safety
// Define once
final userRoute = PilotRoute<PersonData, bool>('/user/:id');
// Push with type-safe args + auto path/query substitution
final result = await userRoute.push(
arguments: PersonData(id: 42, title: 'Eldho'),
pathParams: {'id': '42'},
queryParams: {'tab': 'settings'},
);
// Navigates to: /user/42?tab=settings
// result is bool? — fully typed return value
🔗 URL Launcher & System Intents
// Open in browser / in-app browser / WebView
await routePilot.launchInBrowser(Uri.parse('https://flutter.dev'));
await routePilot.launchInAppBrowser(Uri.parse('https://dart.dev'));
await routePilot.launchInAppWebView(Uri.parse('https://pub.dev'));
// Phone, SMS, Email
await routePilot.makePhoneCall('+1-234-567-8900');
await routePilot.sendSms('+1-234-567-8900', body: 'Hello!');
await routePilot.sendEmail('hello@example.com', subject: 'Hi', body: 'From RoutePilot!');
🧭 Route observer
// Access current and previous route names from anywhere
final current = routePilot.currentRoute; // e.g. '/profile'
final previous = routePilot.previousRoute; // e.g. '/'
// Full route stack
final stack = routePilot.observer.routeStack;
Why not GetX or GoRouter?
GetX bundles state management, dependency injection, and routing together. If you only want routing, you're carrying a lot of extra weight.
GoRouter is the official recommendation, but middleware (redirects) requires working around the redirect callback in ways that don't handle async loading states well out of the box.
AutoRoute requires code generation — every time you add a route, you run the build runner.
RoutePilot is focused on routing only. Bring your own state manager. Zero code generation. Middleware is first-class, not a workaround.
By the numbers
- 9 built-in transitions
- 0 code generation steps
-
2 dependencies:
flutter+url_launcher - MIT licensed
Try it
dependencies:
route_pilot: ^0.2.0
The package ships with a full example app demonstrating:
- Navigator 2.0 with deep linking
- Typed routes and typed arguments
- Auth guard middleware with redirect
- Route groups with shared prefix
- Every overlay type
Links
- 📦 pub.dev: https://pub.dev/packages/route_pilot
- 🐙 GitHub: https://github.com/eldhopaulose/route_pilot
- 🌐 Author: https://eldhopaulose.github.io
- 🏢 Resizo: https://resizo.in
If you find it useful, a ⭐ on GitHub goes a long way. Issues and PRs are very welcome!
Built with ❤️ by Eldho Paulose
Top comments (0)