route_pilot 0.2.0
route_pilot: ^0.2.0 copied to clipboard
A Flutter package that simplifies navigation and routing with custom transitions, easy argument passing, and a clean API for managing routes in your Flutter applications.
RoutePilot #
RoutePilot is a lightweight yet powerful routing and navigation package for Flutter. It provides a clean, singleton-based API for page navigation, custom transitions, async middleware guards, deep linking with web URL sync, typed routes, route groups, overlay management, and URL launching — all without code generation.
Table of Contents #
- Features
- Installation
- Quick Start
- Navigation
- Custom Transitions
- Passing Arguments
- Path & Query Parameters
- Typed Routes
- Async Middleware
- Route Groups
- Unknown Route Handling (404)
- Overlays & UI Helpers
- Route Observer
- URL Launching
- System Intents
- Platform Configuration
- Full API Reference
- Example
- License
Features #
| Category | Capabilities |
|---|---|
| Navigation | Push, pop, replace, clear stack, pop-until by name or predicate |
| Transitions | 9 built-in transitions (fade, slide, scale, rotate, size, iOS Cupertino) with custom curves |
| Deep Linking | Navigator 2.0 Router API with automatic browser URL sync |
| Middleware | Async middleware guards with FutureOr<String?> redirect — supports auth checks, loading states |
| Typed Routes | PilotRoute<TArgs, TReturn> for compile-safe navigation with path & query params |
| Route Groups | Shared prefix, middleware, and transition via PilotRouteGroup |
| Arguments | Pass Map, custom objects, or any type between routes |
| Path Params | Dynamic segments (:id) and query parameters parsed automatically |
| 404 Fallback | Built-in unknown route handling with notFoundPage |
| Overlays | Dialogs, bottom sheets, snackbars, and loading overlays — all without BuildContext |
| URL Launcher | Open browser, in-app browser, WebView, phone calls, SMS, and email |
| Observer | Built-in PilotObserver tracks the full route stack with lifecycle callbacks |
Installation #
Add route_pilot to your pubspec.yaml:
dependencies:
route_pilot: ^0.1.0
Then run:
flutter pub get
Quick Start #
Import the package:
import 'package:route_pilot/route_pilot.dart';
RoutePilot provides a global singleton instance routePilot — no setup boilerplate needed.
Option A: Navigator 2.0 (Router API) — Recommended #
Best for web apps and apps that need deep linking and browser URL synchronization.
abstract class PilotRoutes {
static const String Home = '/';
static const String Profile = '/profile';
}
class PilotPages {
static final List<dynamic> pages = [
PilotPage(
name: PilotRoutes.Home,
page: (context) => const HomePage(),
transition: Transition.ios,
),
PilotPage(
name: PilotRoutes.Profile,
page: (context) => const ProfilePage(),
transition: Transition.fadeIn,
),
];
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: routePilot.getRouterConfig(
notFoundPage: PilotPage(
name: '/404',
page: (context) => const NotFoundPage(),
),
pages: PilotPages.pages,
initialRoute: PilotRoutes.Home, // Optionally specify initial route
),
);
}
}
Option B: Navigator 1.0 (Classic) #
Simpler setup for mobile-only apps.
class PilotPages {
static PilotPage onGenerateRoute(RouteSettings settings) {
switch (settings.name) {
case PilotRoutes.Home:
return PilotPage(
name: PilotRoutes.Home,
page: (context) => const HomePage(),
transition: Transition.ios,
);
case PilotRoutes.Profile:
return PilotPage(
name: PilotRoutes.Profile,
page: (context) => const ProfilePage(),
transition: Transition.fadeIn,
);
default:
return PilotPage(
name: 'error',
page: (context) => const NotFoundPage(),
);
}
}
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorKey: routePilot.navigatorKey,
navigatorObservers: [routePilot.observer],
onGenerateRoute: (settings) => routePilot.onGenerateRoute(
settings,
pages: [PilotPages.onGenerateRoute(settings)],
),
initialRoute: PilotRoutes.Home,
);
}
}
Navigation #
Push & Pop #
// Push a named route
routePilot.toNamed('/profile');
// Go back
routePilot.back();
// Go back with a result
routePilot.back('result_value');
Replace & Clear Stack #
// Replace the current route
routePilot.off('/new-page');
// Clear the entire stack and navigate (e.g., after login)
routePilot.offAll('/home');
Pop Until #
// Pop until a specific route
routePilot.backUntil('/home');
// Pop until a custom predicate
routePilot.backUntilPredicate((route) => route.isFirst);
Direct Widget Navigation #
Navigate to a widget directly without a named route:
routePilot.to(
const ProfilePage(),
transition: Transition.scale,
transitionDuration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
Custom Transitions #
RoutePilot supports 9 built-in transitions on every PilotPage:
| Transition | Description |
|---|---|
Transition.fadeIn |
Fade in |
Transition.rightToLeft |
Slide from right to left |
Transition.leftToRight |
Slide from left to right |
Transition.topToBottom |
Slide from top to bottom |
Transition.bottomToTop |
Slide from bottom to top |
Transition.scale |
Scale up from center |
Transition.rotate |
Rotation transition |
Transition.size |
Size expansion transition |
Transition.ios |
Native iOS Cupertino swipe-back transition |
PilotPage(
name: '/details',
page: (context) => const DetailsPage(),
transition: Transition.bottomToTop,
transitionDuration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
fullscreenDialog: true,
)
Passing Arguments #
Map Arguments #
// Send
routePilot.toNamed(PilotRoutes.Second, arguments: {
'name': 'John Doe',
'age': 25,
});
// Receive in your widget using RoutePilot
final name = routePilot.arg<String>('name'); // 'John Doe'
final age = routePilot.arg<int>('age'); // 25
Typed Object Arguments #
Pass any Dart object — no serialization required:
class PersonData {
final int id;
final String title;
PersonData({required this.id, required this.title});
}
// Send a typed object directly
routePilot.toNamed(PilotRoutes.Second, arguments: PersonData(id: 1, title: 'Dev'));
// Receive the object
final person = routePilot.getArguments<PersonData>();
You can also pass objects inside a map:
// Send object inside map
routePilot.toNamed(PilotRoutes.Second, arguments: {
'name': 'John',
'personData': PersonData(id: 1, title: 'Dev'),
});
// Receive specific object from map
final person = routePilot.arg<PersonData>('personData');
Access the raw arguments directly:
final rawArgs = routePilot.args; // dynamic
Displaying Extracted Arguments in UI #
Here's an example of how you can extract and display the arguments cleanly inside a StatelessWidget:
class SecondPage extends StatelessWidget {
// Option 1: Pass arguments via constructor from PilotPages generator
final String name;
final int age;
final PersonData personData;
const SecondPage({
super.key,
required this.name,
required this.age,
required this.personData,
});
@override
Widget build(BuildContext context) {
// Option 2: Alternatively, retrieve them directly inside build()
// final directName = routePilot.arg<String>('name');
return Scaffold(
appBar: AppBar(title: const Text('Second Page')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Name Passed: $name', style: const TextStyle(fontSize: 22)),
Text('Age Passed: $age', style: const TextStyle(fontSize: 22)),
const Divider(),
Text('PersonData ID: ${personData.id}'),
Text('PersonData Title: ${personData.title}'),
],
),
),
);
}
}
Path & Query Parameters #
Define dynamic path segments using :param syntax:
PilotPage(
name: '/user/:id',
page: (context) => const UserPage(),
)
Navigate and read parameters:
// Navigate to /user/42?tab=settings
routePilot.toNamed('/user/42?tab=settings');
// Read parameters
final id = routePilot.param('id'); // '42'
final tab = routePilot.param('tab'); // 'settings'
Typed Routes #
Use PilotRoute<TArgs, TReturn> for type-safe navigation with automatic path/query param substitution:
// Define
final userRoute = PilotRoute<PersonData, bool>('/user/:id');
// Navigate
final result = await userRoute.push(
arguments: PersonData(id: 42, title: 'Eldho'),
pathParams: {'id': '42'},
queryParams: {'tab': 'settings'},
);
// Navigates to: /user/42?tab=settings
Async Middleware #
Create guards that run asynchronously before a route is displayed. Return a route string to redirect, or null to allow navigation.
class AuthGuard extends PilotMiddleware {
@override
FutureOr<String?> redirect(String? route) async {
final isLoggedIn = await checkAuthToken();
if (!isLoggedIn) return '/login'; // Redirect
return null; // Allow
}
}
Attach middleware to individual pages:
PilotPage(
name: '/dashboard',
page: (context) => const DashboardPage(),
middlewares: [AuthGuard()],
)
Display a custom loading widget while async middlewares resolve:
routePilot.middlewareLoadingWidget = const Center(
child: CircularProgressIndicator(),
);
Route Groups #
Group routes that share a common prefix, middleware, or transition:
PilotRouteGroup(
prefix: '/dashboard',
middlewares: [AuthGuard()],
transition: Transition.fadeIn,
transitionDuration: const Duration(milliseconds: 300),
children: [
PilotPage(name: '/home', page: (context) => const DashboardHome()),
PilotPage(name: '/settings', page: (context) => const SettingsPage()),
// Resolves to: /dashboard/home, /dashboard/settings
],
)
Route groups can be nested — prefixes, middlewares, and transitions cascade automatically.
Unknown Route Handling (404) #
Provide a notFoundPage to handle any unmatched routes:
routePilot.getRouterConfig(
notFoundPage: PilotPage(
name: '/404',
page: (context) => const NotFoundPage(),
),
pages: [ /* ... */ ],
)
Overlays & UI Helpers #
All overlay APIs work without requiring a BuildContext — just call them from anywhere.
Dialogs #
final result = await routePilot.dialog(
AlertDialog(
title: const Text('Confirm'),
content: const Text('Are you sure?'),
actions: [
TextButton(onPressed: () => routePilot.back(false), child: const Text('No')),
TextButton(onPressed: () => routePilot.back(true), child: const Text('Yes')),
],
),
barrierDismissible: false,
);
Bottom Sheets #
routePilot.bottomSheet(
const MyBottomSheetWidget(),
isScrollControlled: true,
isDismissible: true,
);
SnackBars #
Queue-safe — automatically clears previous snackbars before showing a new one:
routePilot.snackBar(
'Item saved successfully!',
duration: const Duration(seconds: 3),
backgroundColor: Colors.green,
);
Loading Overlay #
A non-dismissible, full-screen loading overlay:
// Show
routePilot.showLoading();
// Or with a custom indicator
routePilot.showLoading(indicator: const MyCustomSpinner());
// Hide
routePilot.hideLoading();
Route Observer #
RoutePilot includes a built-in PilotObserver that tracks the full navigation stack:
// Get the current route name
final current = routePilot.currentRoute; // e.g., '/profile'
// Get the previous route name
final previous = routePilot.previousRoute; // e.g., '/'
// Access the full route stack
final stack = routePilot.observer.routeStack;
Wire it into your app (automatically done with Router API):
MaterialApp(
navigatorObservers: [routePilot.observer],
// ...
);
URL Launching #
// Open in default browser
await routePilot.launchInBrowser(Uri.parse('https://flutter.dev'));
// Open in in-app browser
await routePilot.launchInAppBrowser(Uri.parse('https://dart.dev'));
// Open in embedded WebView
await routePilot.launchInAppWebView(Uri.parse('https://pub.dev'));
// WebView with custom headers
await routePilot.launchInAppWithCustomHeaders(
Uri.parse('https://api.example.com'),
{'Authorization': 'Bearer token123'},
);
// WebView without JavaScript
await routePilot.launchInAppWithoutJavaScript(Uri.parse('https://example.com'));
// WebView without DOM Storage
await routePilot.launchInAppWithoutDomStorage(Uri.parse('https://example.com'));
// iOS Universal Link (falls back to in-app browser)
await routePilot.launchUniversalLinkIOS(Uri.parse('https://example.com'));
System Intents #
// Make a phone call
await routePilot.makePhoneCall('+1-234-567-8900');
// Send an SMS
await routePilot.sendSms('+1-234-567-8900', body: 'Hello from Flutter!');
// Send an email
await routePilot.sendEmail(
'hello@example.com',
subject: 'Greetings',
body: 'Sent via RoutePilot!',
);
Platform Configuration #
Android #
1. Predictive Back Gesture (Android 13+)
Enable predictive back navigation in android/app/src/main/AndroidManifest.xml:
<application
...
android:enableOnBackInvokedCallback="true">
2. URL Launcher Queries
Add schemes your app queries to AndroidManifest.xml:
<queries>
<!-- SMS -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="sms" />
</intent>
<!-- Phone -->
<intent>
<action android:name="android.intent.action.VIEW" />
<data android:scheme="tel" />
</intent>
<!-- In-App Browser (Chrome Custom Tabs) -->
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
iOS #
Add queried URL schemes to ios/Runner/Info.plist:
<key>LSApplicationQueriesSchemes</key>
<array>
<string>sms</string>
<string>tel</string>
</array>
Full API Reference #
RoutePilot (singleton via routePilot) #
| Method / Property | Description |
|---|---|
navigatorKey |
Global GlobalKey<NavigatorState> |
observer |
Built-in PilotObserver for route tracking |
currentRoute |
Current route name from the stack |
previousRoute |
Previous route name from the stack |
middlewareLoadingWidget |
Widget shown while async middlewares resolve |
getRouterConfig({pages, notFoundPage, initialRoute}) |
Creates RouterConfig for MaterialApp.router |
onGenerateRoute(settings, {pages, notFoundPage}) |
Route generator for classic MaterialApp |
to(Widget, {arguments, transition, ...}) |
Push a widget directly |
toNamed(String, {arguments}) |
Push a named route |
back([result]) |
Pop the current route |
backUntil(String) |
Pop until a named route |
backUntilPredicate(RoutePredicate) |
Pop until predicate returns true |
off(String, {arguments}) |
Replace current route |
offAll(String, {arguments}) |
Clear stack and push |
dialog(Widget, {barrierDismissible}) |
Show a dialog |
bottomSheet(Widget, {isScrollControlled, ...}) |
Show a modal bottom sheet |
snackBar(String, {duration, backgroundColor}) |
Show a queue-safe snackbar |
showLoading({indicator}) |
Show non-dismissible loading overlay |
hideLoading() |
Dismiss loading overlay |
args |
Raw dynamic arguments |
arg<T>(String key) |
Get a typed value from map arguments |
getArguments<T>() |
Get the full arguments cast to type T |
param(String key) |
Get a path or query parameter |
launchInBrowser(Uri) |
Open URL in external browser |
launchInAppBrowser(Uri) |
Open URL in in-app browser |
launchInAppWebView(Uri) |
Open URL in embedded WebView |
launchInAppWithCustomHeaders(Uri, Map) |
WebView with custom headers |
launchInAppWithoutJavaScript(Uri) |
WebView without JavaScript |
launchInAppWithoutDomStorage(Uri) |
WebView without DOM storage |
launchUniversalLinkIOS(Uri) |
iOS Universal Link with fallback |
makePhoneCall(String) |
Initiate a phone call |
sendSms(String, {body}) |
Send an SMS |
sendEmail(String, {subject, body}) |
Compose an email |
PilotPage<T> #
| Property | Type | Description |
|---|---|---|
name |
String |
Route name / path |
page |
PilotPageBuilder |
Widget builder (BuildContext) → Widget |
transition |
Transition? |
Transition animation type |
transitionDuration |
Duration? |
Animation duration (default: 300ms) |
curve |
Curve |
Animation curve (default: Curves.linear) |
fullscreenDialog |
bool |
Present as full-screen dialog |
maintainState |
bool |
Keep state when inactive |
opaque |
bool |
Whether the route is opaque |
parameters |
Map<String, String>? |
Optional route parameters |
arguments |
Object? |
Optional route arguments |
middlewares |
List<PilotMiddleware>? |
Guards to run before building |
PilotMiddleware #
| Method | Description |
|---|---|
FutureOr<String?> redirect(String? route) |
Return a route string to redirect, or null to proceed |
PilotRoute<TArgs, TReturn> #
| Method | Description |
|---|---|
Future<TReturn?> push({arguments, pathParams, queryParams}) |
Navigate with type-safe params & arguments |
PilotRouteGroup #
| Property | Type | Description |
|---|---|---|
prefix |
String? |
Prefix applied to all children |
middlewares |
List<PilotMiddleware>? |
Shared middlewares for all children |
transition |
Transition? |
Shared transition for all children |
transitionDuration |
Duration? |
Shared transition duration |
children |
List<dynamic> |
Child pages or nested groups |
Example #
Check the example project for a full working demo showcasing:
- Navigator 2.0 Router API setup with deep linking
- Map arguments and typed object argument passing
- Path parameters and query parameters
- Typed routes with
PilotRoute - Async middleware auth guard with redirect
- Route groups with shared prefix & middleware
- 404 unknown route fallback
- Dialogs, bottom sheets, snackbars, and loading overlays
Run the example:
cd example
flutter run
Contributing #
Contributions are welcome! Please see the CONTRIBUTING.md for guidelines.
License #
This project is licensed under the MIT License. See the LICENSE file for details.
Author #
Maintained by Eldho Paulose.
- GitHub: eldhopaulose
- Website: eldhopaulose.github.io
- Resizo: resizo.in
Feel free to reach out for questions, suggestions, or contributions! ⭐ If you find qRoutePilot useful, please star the repo on GitHub.