vex_router 1.0.0 copy "vex_router: ^1.0.0" to clipboard
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, [...]

example/lib/main.dart

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,
        ),
      );
}
2
likes
150
points
80
downloads

Documentation

Documentation
API reference

Publisher

verified publishermysteriouscoder.com

Weekly Downloads

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, and a simple API. Zero boilerplate. Zero compromises.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on vex_router