Skip to content

Repository files navigation

Browser Router - Advanced Navigation for Flutter

pub version License: MIT

An advanced, strongly-typed navigation and overlay management system for Flutter, built with zero external UI dependencies on pure package:flutter/widgets.dart.


Features

  • Centralized Route Management: Define all application routes in a single declarative registry.
  • Strongly-Typed Route Arguments: Pass type-safe arguments using RouteParams with built-in validation and polymorphic type resolution.
  • Custom & Adaptive Transitions: Seamlessly apply transitions (slide, fade, scale, etc.) per route or globally based on platform/path.
  • Versatile Presentation Styles: Present any screen as a full page, a modal dialog, or a swipeable bottom sheet via TraceRoute.
  • Semantic Navigation API: Create a decoupled, domain-driven navigation layer using Trace objects.
  • Reactive Navigation Lifecycle: Listen to visibility changes (onAppear, onDisappear) via Browser.watch.
  • Atomic Argument Consumption: Eliminate duplicate event triggers on widget rebuilds with context.getArgumentAndClean<T>().
  • Universal Navigation & Deep Linking: Automatic URL query parameter extraction to DeepLinkParam and versatile URL routing with context.launchAction().
  • Advanced Overlays & Sequential Banners: Managed banner queues, modal overlays, and bottom sheets decoupled from the navigator stack.
  • Code Splitting & Deferred Loading: Out-of-the-box support for lazy loading routes with DeferredBrowserRoute.
  • Zero UI Framework Dependencies: 100% decoupled from Material and Cupertino widgets.

Installation

Add browser_router to your pubspec.yaml:

dependencies:
  browser_router: ^0.3.0

Then run:

flutter pub get

Getting Started

1. Define Routes

Create a list of BrowserRoute instances:

import 'package:browser_router/browser.dart';
import 'package:flutter/widgets.dart';

final routes = [
  BrowserRoute(
    path: '/',
    page: const HomeScreen(),
    routeTransition: RouteTransition.none,
  ),
  BrowserRoute(
    path: '/profile',
    page: const ProfileScreen(),
    routeTransition: RouteTransition.slide_right,
  ),
];

2. Wrap Your App with Browser

Place Browser at the root of your application widget hierarchy:

import 'package:browser_router/browser.dart';
import 'package:flutter/widgets.dart';
import 'routes.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return Browser(
      routes: routes,
      defaultRoute: routes.first,
      builder: (context, routeObserver, generate) {
        return WidgetsApp(
          color: const Color(0xFFFFFFFF),
          navigatorObservers: [routeObserver],
          onGenerateRoute: generate,
          onGenerateInitialRoutes: (routePath) => [
            generate(
              RouteSettings(name: routePath, arguments: const <dynamic, dynamic>{}),
            ),
          ],
        );
      },
    );
  }
}

3. Basic Navigation

Navigate using the BuildContext extension methods:

// Push a new route
context.pushNamed('/profile');

// Pop the current route
context.pop();

Typed Route Arguments (RouteParams)

Pass strongly-typed data between screens safely without casting dynamic maps.

1. Define an Arguments Class

Subclass RouteParams (using final class or base class):

import 'package:browser_router/browser.dart';

final class ProfileArgs extends RouteParams {
  const ProfileArgs({required this.userId});

  final String userId;

  @override
  bool validate() => userId.isNotEmpty;
}

2. Add Route Validation (Optional)

Enforce arguments validation before navigation occurs. If validation fails, Browser automatically falls back to defaultRoute:

BrowserRoute(
  path: '/profile',
  page: const ProfileScreen(),
  validateArguments: (check, get) => check<ProfileArgs>(),
)

3. Push with Arguments

context.pushNamed(
  '/profile',
  args: [ProfileArgs(userId: 'usr_12345')],
);

4. Read Arguments in the Target Screen

Use context.getArgument<T>() in your build method. This read is idempotent and safe across multiple widget rebuilds:

class ProfileScreen extends StatelessWidget {
  const ProfileScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final args = context.getArgument<ProfileArgs>();

    return Center(
      child: Text('User ID: ${args?.userId}'),
    );
  }
}

Returning Data & Reactive Lifecycle (Browser.watch)

browser_router solves the problem of lost return data and unhandled gestures (swipe-to-dismiss) by updating the route settings of the underlying screen directly.

1. Returning Arguments on Pop

// Return data directly when popping
context.pop(args: OrderResultArgs(status: 'COMPLETED'));

// Or pop multiple screens to the root and pass arguments
context.popToFirst(args: [OrderResultArgs(status: 'COMPLETED')]);

2. Staging Arguments for Gesture Dismissals

If a screen can be dismissed via swipe gestures or system back buttons, stage return arguments in initState or upon user actions using setPopArgument:

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    context.setPopArgument(DraftSavedArgs(savedAt: DateTime.now()));
  });
}

3. Consuming Results with Browser.watch and getArgumentAndClean

Wrap the receiving widget with Browser.watch. In onAppear, use context.getArgumentAndClean<T>() to read and atomically remove the argument:

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Browser.watch(
      onAppear: (context, deepLink) {
        // Atomic consumption: eliminates repeat execution on rebuilds
        final result = context.getArgumentAndClean<OrderResultArgs>();
        if (result != null) {
          showToast('Order: ${result.status}');
        }
      },
      child: const HomeContent(),
    );
  }
}

getArgument vs. getArgumentAndClean

Method Behavior Primary Use Case
context.getArgument<T>() Reads the argument without modifying the route map. Screen construction data (e.g. IDs, configurations).
context.getArgumentAndClean<T>() Reads and removes the argument from the route map. One-time events (e.g. pop results, snackbar triggers).

Semantic Navigation API with Trace

Encapsulate routes, arguments, and presentations into reusable domain objects:

enum AppPath {
  home('/'),
  profile('/profile'),
  productDetail('/product/detail');

  const AppPath(this.path);
  final String path;
}

class AppTrace extends Trace {
  const AppTrace._({
    required super.path,
    super.args,
    super.traceRoute,
  });

  factory AppTrace.toProfile(String userId) {
    return AppTrace._(
      path: AppPath.profile.path,
      args: ProfileArgs(userId: userId),
      traceRoute: const PageTraceRoute(
        routeTransition: RouteTransition.slide_right,
      ),
    );
  }

  factory AppTrace.toProductModal(String productId) {
    return AppTrace._(
      path: AppPath.productDetail.path,
      args: ProductArgs(id: productId),
      traceRoute: const PopupTraceRoute(
        routeTransition: RouteTransition.fade,
      ),
    );
  }
}

Semantic Actions

// Standard push
AppTrace.toProfile('123').push(context);

// Push and replace current route
AppTrace.toProfile('123').pushAndReplacement(context);

// Pop to first screen and push
AppTrace.toProfile('123').popToFirstAndPush(context);

// Pop to root and replace
AppTrace.toProfile('123').cleanAndPush(context);

// Pop to existing instance in stack or push if not present
AppTrace.toProfile('123').findMeOrPush(context);

Presentation Styles (TraceRoute) & Transitions

Change how a route is presented without altering its widget implementation:

  • PageTraceRoute: Full-screen page navigation.
  • PopupTraceRoute: Displays the route as a modal dialog.
  • SwipeTraceRoute: Displays the route as an interactive bottom sheet with swipe-to-dismiss gestures.
  • OverlayTraceRoute: Displays the route in the overlay layer.

Available Transitions (RouteTransition)

browser_router includes modern Material 3, iOS Cupertino, Web, and legacy presets:

Preset Platform / Style Motion Behavior
RouteTransition.fade_through Material 3 & Web Outgoing fades out & scales (1.0 -> 0.96), incoming fades in & scales (0.92 -> 1.0). Ideal for bottom nav bars and top-level destinations.
RouteTransition.fade_scale Modern Web & M3 Snappy zoom-fade (0.95 -> 1.0) with fast easing. Perfect for web SPAs, search overlays, and dialogs.
RouteTransition.shared_axis_x Material 3 Horizontal directional slide with subtle fade. Ideal for wizards and linear multi-step flows.
RouteTransition.shared_axis_y Material 3 Vertical directional slide with subtle fade. Ideal for form expansions and vertical progressions.
RouteTransition.shared_axis_z Material 3 Depth zoom (0.8 -> 1.0 / 1.0 -> 1.1) with fade. Ideal for drill-down hierarchies.
RouteTransition.slide_cupertino Authentic iOS iOS native push with left-edge gradient drop shadow and 1/3 parallax on the exiting route.
RouteTransition.scale Popup / Dialog Clean scale and fade animation for alerts and confirmation modals.
RouteTransition.slide_right Legacy Slide Standard horizontal right slide.
RouteTransition.slide_left Legacy Slide Standard horizontal left slide.
RouteTransition.slide_up Legacy Slide Standard vertical upward slide.
RouteTransition.slide_down Legacy Slide Standard vertical downward slide.
RouteTransition.fade Legacy Fade Simple fade in / fade out.
RouteTransition.none Instant Zero-duration transition without animation.

Custom Transitions (CustomBuildTransition)

You can define custom transition builders at the route level or per navigation request:

// At BrowserRoute level
BrowserRoute(
  path: '/custom',
  page: const CustomScreen(),
  customTransition: CustomBuildTransition(
    ({required animation, required secondaryAnimation, required child}) {
      return RotationTransition(
        turns: animation,
        child: child,
      );
    },
  ),
);

// Or per navigation request via TraceRoute
context.pushNamed(
  '/custom',
  traceRoute: PageTraceRoute(
    customTransition: CustomBuildTransition(
      ({required animation, required secondaryAnimation, required child}) {
        return FadeTransition(opacity: animation, child: child);
      },
    ),
  ),
);

Accessibility & Reduced Motion (WCAG Compliance)

All BrowserPageRoute and BrowserPopupRoute transitions automatically detect system accessibility settings:

  • MediaQuery.disableAnimationsOf(context)
  • MediaQuery.accessibleNavigationOf(context)

When motion reduction is requested by the user, transitions bypass animations instantly with zero motion discomfort, requiring zero extra configuration.


Adaptive Transitions and Traces

Configure global platform-adaptive transition rules using built-in Browser.defaultAdaptiveTransition:

Browser(
  routes: routes,
  defaultRoute: routes.first,
  adaptiveTransition: Browser.defaultAdaptiveTransition, // iOS: slide_cupertino, Android: shared_axis_x, Web/Desktop: fade_scale
  adaptiveTrace: (name) {
    // All routes under /modal/ open as popups automatically
    if (name?.startsWith('/modal/') ?? false) {
      return const PopupTraceRoute();
    }
    return null;
  },
  builder: (context, routeObserver, generate) => ...,
)

Universal Navigation & Deep Linking (launchAction)

browser_router automatically captures query parameters into DeepLinkParam:

// Navigating to: /profile?id=456&theme=dark
final deepLink = context.getArgument<DeepLinkParam>();
final id = deepLink?.params['id']; // "456"

Use context.launchAction() for unified routing of internal routes and external URLs:

// Pop current view
context.launchAction('/?navigateType=pop');

// Pop to first view and push
context.launchAction('/profile?navigateType=popFirstAndPush');

// Push replacement
context.launchAction('/dashboard?navigateType=pushReplacement');

// External link (triggers openUrl callback)
context.launchAction('https://flutter.dev');

Overlays, Sequential Banners & Sheets

Manage UI components that sit above the navigation stack using built-in overlay utilities:

1. Sequential Banners Queue

Display notification banners one after another in FIFO order:

Browser.enqueueBanner(
  context,
  (dismiss) => Container(
    padding: const EdgeInsets.all(16),
    color: const Color(0xFF008080),
    child: Row(
      children: [
        const Text('Update available!'),
        GestureDetector(
          onTap: dismiss,
          child: const Text(' Dismiss'),
        ),
      ],
    ),
  ),
);

2. Custom Overlay Modals

Show a modal overlay independent of the Navigator route stack:

Browser.showOverlay(
  context,
  backgroundColor: const Color(0x80000000),
  isDismissible: true,
  builder: (dismiss) => Container(
    width: 300,
    height: 200,
    color: const Color(0xFFFFFFFF),
    child: Center(
      child: GestureDetector(
        onTap: dismiss,
        child: const Text('Close Modal'),
      ),
    ),
  ),
);

// Dismiss programmatically
Browser.dismissOverlay(context);

3. Modal Bottom Sheet

Browser.showModalBottomSheet(
  context: context,
  backgroundColor: const Color(0xFFFFFFFF),
  heightFactor: 0.6,
  builder: (context) => const SheetContentView(),
);

Code Splitting & Deferred Loading (DeferredBrowserRoute)

Optimize initial download bundle sizes on Flutter Web and apps by loading route modules on-demand:

import 'package:browser_router/deferred_browser_route.dart';
import 'package:my_app/screens/heavy_feature.dart' deferred as heavy_feature;

final routes = [
  DeferredBrowserRoute(
    path: '/heavy_feature',
    loadLibrary: heavy_feature.loadLibrary,
    pageBuilder: () => heavy_feature.HeavyFeatureScreen(),
    loadingWidget: const Center(child: Text('Loading...')),
  ),
];

Flutter Web Routing Strategies

browser_router supports both Hash-based and Path-based URL routing strategies.

1. Default Strategy (Hash-based)

URLs contain #: https://yourapp.com/#/profile?id=123. Works out-of-the-box without web server configuration.

2. Path-based Strategy

Clean URLs: https://yourapp.com/profile?id=123.

To enable:

import 'package:flutter_web_plugins/url_strategy.dart';

void main() {
  usePathUrlStrategy();
  runApp(const MyApp());
}

Important

When using path-based URLs, configure your web server (Nginx, Firebase Hosting, Apache) to rewrite all requests to index.html to prevent 404 errors on direct URL access.


License

This project is licensed under the MIT License - see the LICENSE file for details.


Maintainers & Contributors ✨

Big thanks to the contributors:

Eduardo Martínez Catalá
Eduardo Martínez Catalá
Cayetano Bañón Rubio
Cayetano Bañón Rubio
Jesus Bernabeu
Jesus Bernabeu

About

An advanced navigation system for Flutter that enables typed routes, custom transitions, and robust overlay management.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages