vex_router 1.0.0
vex_router: ^1.0.0 copied to clipboard
The ultimate Flutter router. Navigation 2.0 done right — no code generation, no GetMaterialApp lock-in, full GetX controller binding support, type-safe routes, deep linking, guards, nested navigation, [...]
🚀 vex_router #
The ultimate Flutter Navigation 2.0 router.
No code generation. No GetMaterialApp. No compromise.
📊 Full Package Comparison #
Every popular Flutter routing package in one table.
✅ = supported ❌ = not supported ⚠️ = partial / workaround needed
| Feature | vex_router | go_router | auto_route | GetX nav | beamer | routemaster |
|---|---|---|---|---|---|---|
| Navigation 2.0 (Router API) | ✅ | ✅ | ✅ | ⚠️ partial | ✅ | ✅ |
| No code generation needed | ✅ | ✅ | ❌ build_runner | ✅ | ✅ | ✅ |
| No GetMaterialApp / custom App | ✅ | ✅ | ✅ | ❌ required | ✅ | ✅ |
| Type-safe routes | ✅ | ⚠️ opt-in builder | ✅ generated | ❌ string-based | ❌ | ⚠️ |
| Named route push | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ |
| Path parameters | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ |
| Query parameters | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ |
| Typed extra data | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ |
| Typed pop result (VexResult) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Nested / child routes | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ |
| Shell / tab navigation | ✅ | ✅ StatefulShell | ✅ | ⚠️ | ✅ | ✅ |
| Tab state preserved (IndexedStack) | ✅ | ✅ | ✅ | ⚠️ | ⚠️ | ⚠️ |
| Route guards (imperative) | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| Declarative redirects | ✅ | ✅ | ⚠️ | ❌ | ❌ | ⚠️ |
| Multiple guards per route | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| Global guards | ✅ | ⚠️ single redirect | ❌ | ❌ | ❌ | ❌ |
| GetX controller binding | ✅ native | ❌ | ❌ | ✅ | ❌ | ❌ |
| Any controller binding (DI-agnostic) | ✅ | ❌ | ✅ AutoRouteWrapper | ❌ GetX only | ❌ | ❌ |
| Inline binding (no class) | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Multi-binding per route | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Auto controller dispose on pop | ✅ | ❌ manual | ✅ | ✅ | ❌ | ❌ |
| Navigation observer / analytics | ✅ | ✅ | ✅ | ⚠️ | ✅ | ⚠️ |
| 9+ built-in transitions | ✅ | ⚠️ custom only | ⚠️ | ✅ | ❌ | ❌ |
| Per-route transition | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Custom transition builder | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| Global default transition | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| Deep linking | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ |
| Web URL sync | ✅ | ✅ | ✅ | ⚠️ limited | ✅ | ✅ |
| Platform back button support | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ |
| Error screen builder | ✅ | ✅ | ✅ | ❌ | ⚠️ | ⚠️ |
| 404 not-found screen | ✅ | ✅ | ✅ | ❌ | ✅ | ⚠️ |
| Context-free navigation | ✅ VexNavigator.root | ❌ | ❌ | ✅ Get.to | ❌ | ❌ |
| BuildContext extensions | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| String route extensions | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Navigate by raw URL path | ✅ | ✅ | ✅ | ⚠️ | ✅ | ✅ |
| Navigate by route name | ✅ | ✅ | ✅ | ✅ | ⚠️ | ✅ |
| pushAndClearStack | ✅ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ |
| pushAndRemoveUntil | ✅ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ |
| popUntil(name) | ✅ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ |
| popToRoot | ✅ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ |
| maybePop() | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| showDialog as route | ✅ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ |
| showBottomSheet as route | ✅ | ✅ | ✅ | ✅ | ⚠️ | ⚠️ |
| Full-screen dialog flag | ✅ | ✅ | ✅ | ⚠️ | ❌ | ⚠️ |
| State restoration | ✅ | ✅ | ⚠️ | ❌ | ❌ | ⚠️ |
| Opaque route control | ✅ | ✅ | ✅ | ⚠️ | ❌ | ❌ |
| maintainState control | ✅ | ✅ | ✅ | ⚠️ | ❌ | ❌ |
| Page title (web) | ✅ | ✅ | ✅ | ❌ | ✅ | ⚠️ |
| Actively maintained (2025) | ✅ | ⚠️ feature-complete | ✅ | ⚠️ slow | ⚠️ | ❌ |
| No GetX dependency | ✅ | ✅ | ✅ | ❌ required | ✅ | ✅ |
| Unit testable (no static context) | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ |
| Works with any state manager | ✅ | ✅ | ✅ | ❌ GetX only | ✅ | ✅ |
| Zero runtime dependencies | ✅ | ❌ collection | ❌ many | ❌ many | ❌ | ❌ |
⚡ Why vex_router wins #
Problems with go_router ❌ #
- Feature-frozen ("feature-complete") since early 2025 — no new APIs
- Tabs /
StatefulShellRouteboilerplate is extremely verbose - No route guards — only a single global
redirectcallback - No controller binding — you manage DI manually
- No typed pop results
- No context-free navigation without workarounds
- Bloc scoping across nested shells requires painful nested ShellRoutes
Problems with auto_route ❌ #
- Requires
build_runnerand code generation — every route change →flutter pub run build_runner build - Generated code is hard to debug and adds CI overhead
- Steep onboarding curve for new team members
- No inline bindings — must extend
AutoRouteWrapper - No global guards (only per-route
AutoRouteGuard) - No typed pop results
Problems with GetX navigation ❌ #
- Forces
GetMaterialApp— replaces Flutter's own MaterialApp router - Bypasses Flutter's
BuildContextentirely (anti-pattern) - Navigation is not unit-testable (static
Get.to(),Get.offNamed()) - GetX navigation conflicts with Navigation 2.0 deep-link handling on web
- "GetX does too much" — one package trying to be state manager + DI + router + HTTP
- Maintained by a single developer; slow issue resolution
- Only 35% of API documented
Problems with beamer ❌ #
- Largely unmaintained — last major release 2022
- Complex
BeamLocationsetup - No route guards
- No transition control
Problems with routemaster ❌ #
- Unmaintained — archived/inactive since 2022
- Limited ecosystem support
🚀 Quick Start (2 minutes) #
1. Install #
dependencies:
vex_router: ^1.0.0
2. Define routes #
final router = VexRouter(
initialRoute: 'home',
routes: [
VexRoute(name: 'home', path: '/home', builder: (_) => HomeScreen()),
VexRoute(name: 'profile', path: '/profile/:id', builder: (i) => ProfileScreen(id: i.requireParam('id'))),
VexRoute(name: 'login', path: '/login', builder: (_) => LoginScreen()),
],
);
3. Plug into MaterialApp #
MaterialApp.router(
routerConfig: router.config, // ← that's all
)
No GetMaterialApp. No AutoRouter(). No build_runner. Done. 🎉
🎛️ All APIs #
Navigation — from context #
// Push
context.vexPush('profile', params: {'id': '42'});
context.vexPush('posts', query: {'tab': 'recent'});
context.vexPush('checkout', extra: cartItems);
// Replace
context.vexReplace('home');
// Push + clear stack
context.vexPushAndClear('login');
// Navigate by raw URL
context.vexPushPath('/posts/101?highlight=dart');
// Pop
context.vexPop();
context.vexPop('returned_value');
// Current route info
final info = context.currentRoute;
Navigation — context-free (from controllers / services) #
// Works from anywhere — GetX controllers, Riverpod providers, Bloc listeners…
VexNavigator.root.push('home');
VexNavigator.root.pushAndClearStack('login');
VexNavigator.root.pop();
Typed pop results #
final result = await context.vexPush<UserProfile>('editProfile', extra: profile);
result.when(
onOk: (profile) => save(profile),
onErr: (error) => showError(error),
onCancelled: () => debugPrint('user cancelled'),
);
// Or simple guards:
if (result.isOk) doSomethingWith(result.value);
Type-safe parameter access #
VexRoute(
name: 'order',
path: '/orders/:orderId',
builder: (info) {
final id = info.requireParam('orderId'); // String, throws if missing
final intId = info.requireIntParam('orderId'); // int, throws if non-numeric
final tab = info.query('tab') ?? 'summary'; // String?, null if absent
final flag = info.queryBool('active'); // bool
final order = info.requireExtra<Order>(); // typed extra
return OrderScreen(id: id, tab: tab);
},
),
String / extension method navigation #
// On String — reads like English
'profile'.vexPush(context, params: {'id': '42'});
'login'.vexPushAndClear(context);
'editProfile'.vexReplace(context, extra: myProfile);
🔒 Guards #
Auth guard (built-in) #
final authGuard = VexAuthGuard(
isAuthenticated: () => AuthService.isLoggedIn,
loginRoute: 'login',
excludedRoutes: ['splash', 'register', 'onboarding'],
);
VexRouter(
guards: [authGuard], // ← applied globally
routes: [...],
)
Custom guard #
class PremiumGuard extends VexGuard {
@override
Future<VexGuardResult> canNavigate(VexRouteInfo to) async {
if (User.isPremium) return const VexGuardAllow();
return const VexGuardRedirect('upgrade');
}
}
Guard results:
const VexGuardAllow() // proceed
const VexGuardRedirect('routeName') // redirect by name
const VexGuardRedirectPath('/some/path') // redirect by path
const VexGuardBlock(reason: 'no access') // cancel silently
Multiple guards per route #
VexRoute(
name: 'admin',
path: '/admin',
builder: (_) => AdminScreen(),
guards: [authGuard, AdminGuard(), AuditGuard()], // evaluated in order
),
🎮 GetX Controller Binding #
vex_router is the only router that supports GetX controller bindings without using GetMaterialApp.
// Define a binding
class HomeBinding extends VexBinding {
@override
void onInit() => Get.put(HomeController()); // GetX put
@override
void onDispose() => Get.delete<HomeController>();
}
// Attach to a route
VexRoute(
name: 'home',
path: '/home',
builder: (_) => HomeScreen(),
binding: HomeBinding(),
)
Inline binding — no class needed:
VexRoute(
name: 'profile',
path: '/profile/:id',
builder: (i) => ProfileScreen(id: i.requireParam('id')),
binding: VexInlineBinding(
init: () => Get.put(ProfileController()),
dispose: () => Get.delete<ProfileController>(),
),
)
Multi-binding — initialise several controllers:
VexRoute(
name: 'dashboard',
path: '/dashboard',
builder: (_) => DashboardScreen(),
binding: VexMultiBinding([
UserBinding(),
FeedBinding(),
NotificationBinding(),
]),
)
Works equally well with Riverpod, Bloc, Provider, MobX, setState — any DI pattern.
🎞️ Transitions (9 built-in) #
VexRoute(
name: 'profile',
path: '/profile',
builder: (_) => ProfileScreen(),
transition: VexTransitionType.slideRight,
)
| Name | Description |
|---|---|
material |
Platform-adaptive (Material / Cupertino) |
cupertino |
iOS cubic slide from right |
fade |
Fade in/out |
scale |
Scale from center with ease-out-back |
slideUp |
Slide from bottom (modal style) |
slideLeft |
Slide from left |
slideRight |
Slide from right |
rotation |
Rotate + fade |
size |
Size + fade |
none |
Instant, no animation |
custom |
Your own transitionBuilder |
Custom builder:
VexRoute(
name: 'modal',
path: '/modal',
transition: VexTransitionType.custom,
customTransitionBuilder: (ctx, anim, secAnim, child) {
return SlideTransition(
position: Tween(begin: const Offset(0, 1), end: Offset.zero)
.animate(CurvedAnimation(parent: anim, curve: Curves.elasticOut)),
child: child,
);
},
builder: (_) => ModalScreen(),
)
🐚 Shell (Tab) Navigation #
class MainShell extends StatelessWidget {
final int tabIndex;
final Widget body;
final void Function(int) onTabChanged;
@override
Widget build(BuildContext context) => Scaffold(
body: body,
bottomNavigationBar: BottomNavigationBar(
currentIndex: tabIndex,
onTap: onTabChanged,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Search'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
],
),
);
}
// Configure the shell
VexShell(
tabs: [
VexShellTab(rootRouteName: 'feed'),
VexShellTab(rootRouteName: 'search'),
VexShellTab(rootRouteName: 'profile'),
],
navigators: [...], // provided by VexRouter
keepAlive: true, // tab state preserved when switching
builder: (ctx, tabIndex, body, onTabChanged) =>
MainShell(tabIndex: tabIndex, body: body, onTabChanged: onTabChanged),
)
🔭 Observers (Analytics / Logging) #
class AnalyticsObserver extends VexObserver {
@override
void onNavigate(VexRouteInfo? from, VexRouteInfo to) =>
Analytics.screen(to.name);
@override
void onPop(VexRouteInfo popped, VexRouteInfo? revealed) =>
Analytics.event('back', {'from': popped.name});
@override
void onDeepLink(Uri uri) => Analytics.deepLink(uri.toString());
@override
void onGuardBlocked(VexRouteInfo attempted, String reason) =>
Analytics.event('guard_blocked', {'route': attempted.name});
}
VexRouter(
observers: [AnalyticsObserver(), CrashReportingObserver()],
routes: [...],
)
🗺️ Nested & Child Routes #
VexRoute(
name: 'settings',
path: '/settings',
builder: (_) => SettingsScreen(),
children: [
VexRoute(
name: 'settings.account',
path: 'account', // resolves to /settings/account
builder: (_) => AccountSettingsScreen(),
),
VexRoute(
name: 'settings.privacy',
path: 'privacy', // resolves to /settings/privacy
builder: (_) => PrivacySettingsScreen(),
),
],
)
🪟 Dialogs & Bottom Sheets as Routes #
// Bottom sheet
final result = await VexNavigator.of(context).showBottomSheet<String>(
'editName',
extra: currentName,
);
// Dialog
final confirmed = await VexNavigator.of(context).showDialog<bool>(
'confirmDelete',
barrierDismissible: false,
);
🧩 Works with Every State Manager #
| State manager | How to use |
|---|---|
| GetX | binding: VexInlineBinding(init: () => Get.put(Ctrl())) |
| Riverpod | binding: VexInlineBinding(init: () => container.read(provider)) |
| Bloc | binding: VexInlineBinding(init: () => BlocProvider.of<B>(ctx)..add(Init())) |
| Provider | No binding needed — Provider is widget-tree scoped |
| MobX | binding: VexInlineBinding(init: () => store.init()) |
| setState | No binding needed |
🗺️ Roadmap #
- ❌ Tab-level deep linking (restore exact tab + child stack)
- ❌ Route history persistence (SharedPreferences)
- ❌ Animated tab switching
- ❌
vex_router_builderoptional code-gen layer for compile-time route names - ❌ First-class Flutter Web hash / history mode toggle
- ❌ Middleware pipeline (pre/post navigation hooks)
📄 License #
MIT © 2025 vex_router contributors