browser_router 0.3.2
browser_router: ^0.3.2 copied to clipboard
An advanced navigation system for Flutter that enables typed routes, custom transitions, and robust overlay management.
Browser Router - Advanced Navigation for Flutter #
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
RouteParamswith 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
Traceobjects. - Reactive Navigation Lifecycle: Listen to visibility changes (
onAppear,onDisappear) viaBrowser.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
DeepLinkParamand versatile URL routing withcontext.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/macOS: slide_cupertino, Android/Fuchsia: shared_axis_x, Web/Desktop: only_hero
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á |
![]() Cayetano Bañón Rubio |
![]() Jesus Bernabeu |


