DEV Community

Cover image for RoutePilot — Flutter routing without the boilerplate
Eldho Paulose
Eldho Paulose

Posted on

RoutePilot — Flutter routing without the boilerplate

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
Enter fullscreen mode Exit fullscreen mode
flutter pub get
Enter fullscreen mode Exit fullscreen mode

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
  ),
];
Enter fullscreen mode Exit fullscreen mode

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(),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
);
Enter fullscreen mode Exit fullscreen mode

🎞️ 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());
Enter fullscreen mode Exit fullscreen mode

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
  ],
)
Enter fullscreen mode Exit fullscreen mode

💬 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,
);
Enter fullscreen mode Exit fullscreen mode

🧩 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
Enter fullscreen mode Exit fullscreen mode

🔗 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!');
Enter fullscreen mode Exit fullscreen mode

🧭 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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

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)