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, [...]
import 'package:flutter/material.dart';
import 'package:vex_router/vex_router.dart';
// ─── Mock auth service ────────────────────────────────────────────────────────
class AuthService {
static bool _loggedIn = false;
static bool get isLoggedIn => _loggedIn;
static void login() => _loggedIn = true;
static void logout() => _loggedIn = false;
}
// ─── GetX-style controllers (no hard GetX dep — pure Dart) ───────────────────
class HomeController {
int counter = 0;
void increment() => counter++;
void dispose() => debugPrint('HomeController disposed');
}
class ProfileController {
final String userId;
ProfileController(this.userId);
void dispose() => debugPrint('ProfileController($userId) disposed');
}
// ─── Bindings ─────────────────────────────────────────────────────────────────
class HomeBinding extends VexBinding {
static HomeController? controller;
@override
void onInit() {
controller = HomeController();
debugPrint('HomeController initialised');
}
@override
void onDispose() {
controller?.dispose();
controller = null;
}
}
class ProfileBinding extends VexBinding {
final String userId;
static ProfileController? controller;
const ProfileBinding(this.userId);
@override
void onInit() {
controller = ProfileController(userId);
debugPrint('ProfileController initialised for user $userId');
}
@override
void onDispose() {
controller?.dispose();
controller = null;
}
}
// ─── Guards ───────────────────────────────────────────────────────────────────
final authGuard = VexAuthGuard(
isAuthenticated: () => AuthService.isLoggedIn,
loginRoute: 'login',
excludedRoutes: ['login', 'register', 'splash'],
);
// ─── Analytics observer ───────────────────────────────────────────────────────
class AnalyticsObserver extends VexObserver {
@override
void onNavigate(VexRouteInfo? from, VexRouteInfo to) =>
debugPrint('📊 Navigate: ${from?.name ?? 'start'} → ${to.name}');
@override
void onPop(VexRouteInfo popped, VexRouteInfo? revealed) =>
debugPrint('📊 Pop: ${popped.name}');
@override
void onDeepLink(Uri uri) =>
debugPrint('📊 DeepLink: $uri');
}
// ─────────────────────────────────────────────────────────────────────────────
// Router definition
// ─────────────────────────────────────────────────────────────────────────────
late final VexRouter _router;
void main() {
_router = VexRouter(
initialRoute: 'splash',
guards: [authGuard],
observers: [AnalyticsObserver()],
defaultTransition: VexTransitionType.slideRight, // ← global default
defaultTransitionDuration: const Duration(milliseconds: 280),
notFoundBuilder: (path) => NotFoundScreen(path: path),
errorBuilder: (error, info) => ErrorScreen(error: error, info: info),
routes: [
// ── Splash ──────────────────────────────────────────────────────────
VexRoute(
name: 'splash',
path: '/splash',
builder: (_) => const SplashScreen(),
transition: VexTransitionType.fade,
),
// ── Auth ─────────────────────────────────────────────────────────────
VexRoute(
name: 'login',
path: '/login',
builder: (_) => const LoginScreen(),
transition: VexTransitionType.slideUp,
),
VexRoute(
name: 'register',
path: '/register',
builder: (_) => const RegisterScreen(),
),
// ── Home ─────────────────────────────────────────────────────────────
VexRoute(
name: 'home',
path: '/home',
builder: (_) => const HomeScreen(),
binding: HomeBinding(),
transition: VexTransitionType.fade,
guards: [authGuard],
),
// ── User profile — path param :id ─────────────────────────────────
VexRoute(
name: 'profile',
path: '/profile/:id',
builder: (info) => ProfileScreen(userId: info.requireParam('id')),
binding: VexInlineBinding(
init: () => debugPrint('profile init'),
dispose: () => debugPrint('profile dispose'),
),
guards: [authGuard],
transition: VexTransitionType.slideRight,
),
// ── Settings — children demonstrate nested routes ─────────────────
VexRoute(
name: 'settings',
path: '/settings',
builder: (_) => const SettingsScreen(),
guards: [authGuard],
children: [
VexRoute(
name: 'settings.account',
path: 'account',
builder: (_) => const AccountSettingsScreen(),
),
VexRoute(
name: 'settings.privacy',
path: 'privacy',
builder: (_) => const PrivacySettingsScreen(),
),
],
),
// ── Posts — query params demo (/posts?category=flutter&page=1) ────
VexRoute(
name: 'posts',
path: '/posts',
builder: (info) => PostsScreen(
category: info.query('category') ?? 'all',
page: int.tryParse(info.query('page') ?? '1') ?? 1,
),
guards: [authGuard],
),
// ── Post detail — path + query + extra ───────────────────────────
VexRoute(
name: 'post',
path: '/posts/:postId',
builder: (info) => PostDetailScreen(
postId: info.requireParam('postId'),
highlight: info.query('highlight'),
),
transition: VexTransitionType.cupertino,
guards: [authGuard],
),
// ── Custom transition demo ────────────────────────────────────────
VexRoute(
name: 'modal',
path: '/modal',
builder: (_) => const ModalScreen(),
fullscreenDialog: true,
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,
);
},
),
],
);
runApp(const VexDemoApp());
}
// ─────────────────────────────────────────────────────────────────────────────
// App
// ─────────────────────────────────────────────────────────────────────────────
class VexDemoApp extends StatelessWidget {
const VexDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'VexRouter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.deepPurple,
useMaterial3: true,
),
// ← That's it. One line. No GetMaterialApp. No context shenanigans.
routerConfig: _router.config,
);
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Screens
// ─────────────────────────────────────────────────────────────────────────────
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
Future.delayed(const Duration(seconds: 2), () {
if (!mounted) {
return;
}
// Global navigation — no context needed!
VexNavigator.root.pushAndClearStack('login');
});
}
@override
Widget build(BuildContext context) => const Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
FlutterLogo(size: 80),
SizedBox(height: 24),
Text('VexRouter', style: TextStyle(fontSize: 28, fontWeight: FontWeight.w700)),
SizedBox(height: 8),
Text('The ultimate Flutter router'),
],
),
),
);
}
class LoginScreen extends StatelessWidget {
const LoginScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ElevatedButton(
onPressed: () {
AuthService.login();
// Extension method syntax
context.vexPushAndClear('home');
},
child: const Text('Login'),
),
const SizedBox(height: 12),
TextButton(
onPressed: () => context.vexPush('register'),
child: const Text('Register'),
),
],
),
),
);
}
class RegisterScreen extends StatelessWidget {
const RegisterScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Register')),
body: const Center(child: Text('Register screen')),
);
}
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () => context.vexPush('settings'),
),
IconButton(
icon: const Icon(Icons.logout),
onPressed: () {
AuthService.logout();
// Global — no context
VexNavigator.root.pushAndClearStack('login');
},
),
],
),
body: ListView(
padding: const EdgeInsets.all(20),
children: [
_Card('Push profile/:id', Icons.person_outline, () {
context.vexPush('profile', params: {'id': '42'});
}),
_Card('Push with query params', Icons.search_outlined, () {
context.vexPushPath('/posts?category=flutter&page=2');
}),
_Card('Push post detail', Icons.article_outlined, () {
context.vexPush('post',
params: {'postId': '101'},
query: {'highlight': 'dart'});
}),
_Card('Modal (custom transition)', Icons.open_in_new_outlined, () {
context.vexPush('modal');
}),
_Card('Show as bottom sheet', Icons.layers_outlined, () {
VexNavigator.of(context).showBottomSheet<void>('modal');
}),
_Card('Show as dialog', Icons.dialpad_outlined, () {
VexNavigator.of(context).showDialog<void>('modal');
}),
_Card('String extension nav', Icons.code_outlined, () {
'profile'.vexPush(context, params: {'id': '99'});
}),
_Card('Typed VexResult pop demo', Icons.reply_outlined, () async {
final result =
await context.vexPush<String>('modal');
result.when(
onOk: (v) => debugPrint('Got: $v'),
onErr: (e) => debugPrint('Err: $e'),
onCancelled: () => debugPrint('Cancelled'),
);
}),
],
),
);
}
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key, required this.userId});
final String userId;
@override
Widget build(BuildContext context) {
final info = context.currentRoute;
return Scaffold(
appBar: AppBar(title: Text('Profile $userId')),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('User ID: $userId',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('Route: ${info?.name ?? '-'}'),
Text('Params: ${info?.pathParams ?? {}}'),
],
),
),
);
}
}
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: Column(
children: [
ListTile(
title: const Text('Account'),
onTap: () => context.vexPush('settings.account'),
),
ListTile(
title: const Text('Privacy'),
onTap: () => context.vexPush('settings.privacy'),
),
],
),
);
}
class AccountSettingsScreen extends StatelessWidget {
const AccountSettingsScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Account Settings')),
body: const Center(child: Text('Account settings (nested route)')),
);
}
class PrivacySettingsScreen extends StatelessWidget {
const PrivacySettingsScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Privacy Settings')),
body: const Center(child: Text('Privacy settings (nested route)')),
);
}
class PostsScreen extends StatelessWidget {
const PostsScreen({super.key, required this.category, required this.page});
final String category;
final int page;
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Posts')),
body: Center(
child: Text('Category: $category | Page: $page'),
),
);
}
class PostDetailScreen extends StatelessWidget {
const PostDetailScreen(
{super.key, required this.postId, this.highlight});
final String postId;
final String? highlight;
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: Text('Post $postId')),
body: Center(
child: Text('Post: $postId\nHighlight: ${highlight ?? "none"}'),
),
);
}
class ModalScreen extends StatelessWidget {
const ModalScreen({super.key});
@override
Widget build(BuildContext context) => Scaffold(
appBar: AppBar(title: const Text('Modal')),
body: Center(
child: ElevatedButton(
onPressed: () => context.vexPop('result_from_modal'),
child: const Text('Return a value and pop'),
),
),
);
}
class NotFoundScreen extends StatelessWidget {
const NotFoundScreen({super.key, required this.path});
final String path;
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('404',
style: TextStyle(fontSize: 80, fontWeight: FontWeight.w900)),
Text('No route matched "$path"'),
TextButton(
onPressed: () => context.vexPushAndClear('home'),
child: const Text('Go Home'),
),
],
),
),
);
}
class ErrorScreen extends StatelessWidget {
const ErrorScreen({super.key, required this.error, required this.info});
final Object error;
final VexRouteInfo info;
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: Text('Error on ${info.name}:\n$error'),
),
);
}
// ── Helper widget ─────────────────────────────────────────────────────────────
class _Card extends StatelessWidget {
const _Card(this.label, this.icon, this.onTap);
final String label;
final IconData icon;
final VoidCallback onTap;
@override
Widget build(BuildContext context) => Card(
margin: const EdgeInsets.symmetric(vertical: 6),
child: ListTile(
leading: Icon(icon, color: Theme.of(context).colorScheme.primary),
title: Text(label),
trailing: const Icon(Icons.arrow_forward_ios, size: 14),
onTap: onTap,
),
);
}