unrouter 0.5.1 copy "unrouter: ^0.5.1" to clipboard
unrouter: ^0.5.1 copied to clipboard

A Flutter router that gives you routing flexibility: define routes centrally, scope them to widgets, or mix both - with browser-style history navigation.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:unrouter/unrouter.dart';

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

// Router configuration - demonstrates hybrid routing (declarative + widget-scoped)
// Declarative routes can use Routes widget internally for progressive routing
final router = Unrouter(
  strategy: .browser,
  enableNavigator1: true,
  routes: const [
    Inlet(factory: Home.new),
    Inlet(path: 'about', factory: About.new),
    Inlet(path: 'route-animation', factory: RouteAnimationDemo.new),

    // Layout route - wraps children without adding path segment
    Inlet(
      factory: AuthLayout.new,
      children: [
        Inlet(path: 'login', factory: Login.new),
        Inlet(path: 'register', factory: Register.new),
      ],
    ),

    // Nested route - has path segment + children
    Inlet(
      path: 'concerts',
      factory: ConcertsLayout.new,
      children: [
        Inlet(factory: ConcertsHome.new),
        Inlet(path: ':city', factory: CityPage.new),
        Inlet(path: 'trending', factory: TrendingPage.new),
      ],
    ),

    // Nested animation demo
    Inlet(
      path: 'nested-animation',
      factory: NestedAnimationLayout.new,
      children: [
        Inlet(factory: NestedAnimationIntro.new),
        Inlet(path: 'details', factory: NestedAnimationDetails.new),
        Inlet(path: 'reviews', factory: NestedAnimationReviews.new),
      ],
    ),

    // Hybrid routing: declarative route that uses Routes widget internally
    // This demonstrates partial matching - /products matches this declarative route,
    // then ProductsPage uses Routes widget (widget-scoped) to match remaining segments like /123
    Inlet(path: 'products', factory: ProductsPage.new),
  ],
);

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Unrouter Example',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      routerConfig: router,
    );
  }
}

// Home Page (Index Route)
class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Home'),
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
          child: Center(
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 520),
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  const Icon(Icons.home, size: 80, color: Colors.deepPurple),
                  const SizedBox(height: 24),
                  const Text(
                    'Welcome to Unrouter Example',
                    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                  ),
                  const SizedBox(height: 32),
                  const Text(
                    'Imperative Navigation (using buttons)',
                    style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
                  ),
                  const SizedBox(height: 16),
                  _buildNavButton(
                    context,
                    'About',
                    Icons.info,
                    Colors.blue,
                    () => context.navigate(.parse('/about')),
                  ),
                  _buildNavButton(
                    context,
                    'Login',
                    Icons.login,
                    Colors.green,
                    () => router.navigate(.parse('/login')),
                  ),
                  const SizedBox(height: 32),
                  const Divider(),
                  const SizedBox(height: 16),
                  const Text(
                    'Declarative Navigation (using Link widget)',
                    style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
                  ),
                  const SizedBox(height: 16),
                  Link(
                    to: Uri.parse('/concerts'),
                    child: Container(
                      padding: const EdgeInsets.symmetric(
                        horizontal: 24,
                        vertical: 12,
                      ),
                      decoration: BoxDecoration(
                        color: Colors.orange,
                        borderRadius: BorderRadius.circular(8),
                      ),
                      child: const Row(
                        mainAxisSize: MainAxisSize.min,
                        children: [
                          Icon(Icons.music_note, color: Colors.white),
                          SizedBox(width: 8),
                          Text(
                            'Concerts',
                            style: TextStyle(color: Colors.white, fontSize: 16),
                          ),
                        ],
                      ),
                    ),
                  ),
                  const SizedBox(height: 12),
                  Link(
                    to: Uri.parse('/products'),
                    builder: (context, location, navigate) {
                      return Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 24,
                          vertical: 12,
                        ),
                        decoration: BoxDecoration(
                          color: Colors.purple,
                          borderRadius: BorderRadius.circular(8),
                        ),
                        child: InkWell(
                          onTap: () => navigate(),
                          child: const Row(
                            mainAxisSize: MainAxisSize.min,
                            children: [
                              Icon(Icons.shopping_bag, color: Colors.white),
                              SizedBox(width: 8),
                              Text(
                                'Products (Link builder)',
                                style: TextStyle(
                                  color: Colors.white,
                                  fontSize: 16,
                                ),
                              ),
                            ],
                          ),
                        ),
                      );
                    },
                  ),
                  const SizedBox(height: 32),
                  const Divider(),
                  const SizedBox(height: 16),
                  const Text(
                    'Route Animations',
                    style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
                  ),
                  const SizedBox(height: 16),
                  _buildNavButton(
                    context,
                    'Full Page Transition',
                    Icons.layers,
                    Colors.deepPurple,
                    () => context.navigate(.parse('/route-animation')),
                  ),
                  _buildNavButton(
                    context,
                    'Nested Transition',
                    Icons.view_agenda,
                    Colors.orange,
                    () => context.navigate(.parse('/nested-animation')),
                  ),
                  const SizedBox(height: 32),
                  const Divider(),
                  const SizedBox(height: 16),
                  const Text(
                    'Navigator 1.0 APIs (enabled by default)',
                    style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
                  ),
                  const SizedBox(height: 16),
                  _buildNavButton(
                    context,
                    'Show Dialog',
                    Icons.chat_bubble_outline,
                    Colors.indigo,
                    () => _showLegacyDialog(context),
                  ),
                  _buildNavButton(
                    context,
                    'Show Bottom Sheet',
                    Icons.keyboard_arrow_up,
                    Colors.teal,
                    () => _showLegacyBottomSheet(context),
                  ),
                  _buildNavButton(
                    context,
                    'Show Menu',
                    Icons.more_vert,
                    Colors.brown,
                    () => _showLegacyMenu(context),
                  ),
                  _buildNavButton(
                    context,
                    'Push Page',
                    Icons.open_in_new,
                    Colors.deepPurple,
                    () => _pushLegacyPage(context),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildNavButton(
    BuildContext context,
    String label,
    IconData icon,
    Color color,
    VoidCallback onPressed,
  ) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 8),
      child: ElevatedButton.icon(
        onPressed: onPressed,
        icon: Icon(icon),
        label: Text(label),
        style: ElevatedButton.styleFrom(
          backgroundColor: color,
          foregroundColor: Colors.white,
          minimumSize: const Size(200, 50),
        ),
      ),
    );
  }

  void _showLegacyDialog(BuildContext context) {
    showDialog<void>(
      context: context,
      builder: (context) {
        return AlertDialog(
          title: const Text('Dialog'),
          content: const Text(
            'This dialog uses Navigator 1.0 APIs inside Unrouter.',
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Close'),
            ),
          ],
        );
      },
    );
  }

  void _showLegacyBottomSheet(BuildContext context) {
    showModalBottomSheet<void>(
      context: context,
      builder: (context) {
        return SafeArea(
          child: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const Text(
                  'Bottom Sheet',
                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
                ),
                const SizedBox(height: 12),
                const Text('This bottom sheet is Navigator 1.0 based.'),
                const SizedBox(height: 16),
                ElevatedButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: const Text('Close'),
                ),
              ],
            ),
          ),
        );
      },
    );
  }

  void _showLegacyMenu(BuildContext context) {
    final overlay = Overlay.of(context).context.findRenderObject() as RenderBox;
    final position = RelativeRect.fromLTRB(
      24,
      24,
      overlay.size.width - 24,
      overlay.size.height - 24,
    );
    showMenu<String>(
      context: context,
      position: position,
      items: const [
        PopupMenuItem<String>(value: 'first', child: Text('Menu Item A')),
        PopupMenuItem<String>(value: 'second', child: Text('Menu Item B')),
      ],
    );
  }

  void _pushLegacyPage(BuildContext context) {
    Navigator.of(context).push(
      MaterialPageRoute<void>(builder: (context) => const LegacyDetailsPage()),
    );
  }
}

// About Page
class About extends StatelessWidget {
  const About({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('About'),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => context.navigate.back(),
        ),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Icon(Icons.info_outline, size: 80, color: Colors.blue),
            const SizedBox(height: 24),
            const Text(
              'About Unrouter',
              style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 16),
            const Padding(
              padding: EdgeInsets.symmetric(horizontal: 32),
              child: Text(
                'A declarative routing library for Flutter with static route configuration.',
                textAlign: TextAlign.center,
                style: TextStyle(fontSize: 16),
              ),
            ),
            const SizedBox(height: 32),
            ElevatedButton(
              onPressed: () => context.navigate(.parse('/')),
              child: const Text('Back to Home'),
            ),
          ],
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final controller = context.routeAnimation(
      duration: const Duration(milliseconds: 400),
    );
    final animation = CurvedAnimation(
      parent: controller,
      curve: Curves.easeOutCubic,
      reverseCurve: Curves.easeInCubic,
    );
    final slide = Tween<Offset>(
      begin: const Offset(0, 0.08),
      end: Offset.zero,
    ).animate(animation);

    return FadeTransition(
      opacity: animation,
      child: SlideTransition(
        position: slide,
        child: Scaffold(
          appBar: AppBar(
            title: const Text('Route Animation'),
            leading: IconButton(
              icon: const Icon(Icons.arrow_back),
              onPressed: () => context.navigate.back(),
            ),
          ),
          body: Padding(
            padding: const EdgeInsets.all(24),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                const Text(
                  'Full-page transition',
                  style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
                ),
                const SizedBox(height: 12),
                Text(
                  'This page uses context.routeAnimation(...) to animate '
                  'the entire route on push/pop.',
                  style: TextStyle(color: Colors.grey.shade700),
                ),
                const SizedBox(height: 24),
                Card(
                  elevation: 2,
                  child: ListTile(
                    leading: const Icon(Icons.auto_awesome, size: 32),
                    title: const Text('Animation Controller'),
                    subtitle: Text(
                      'Duration: ${controller.duration?.inMilliseconds}ms',
                    ),
                  ),
                ),
                const SizedBox(height: 24),
                ElevatedButton.icon(
                  onPressed: () => context.navigate(.parse('/nested-animation')),
                  icon: const Icon(Icons.view_agenda),
                  label: const Text('See Nested Animation'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Nested Animation'),
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => context.navigate.back(),
        ),
      ),
      body: Column(
        children: [
          Container(
            color: Colors.orange.shade100,
            child: Row(
              children: [
                _buildTab(context, 'Intro', '/nested-animation'),
                _buildTab(context, 'Details', '/nested-animation/details'),
                _buildTab(context, 'Reviews', '/nested-animation/reviews'),
              ],
            ),
          ),
          Expanded(
            child: Container(
              padding: const EdgeInsets.all(16),
              color: Colors.orange.shade50,
              child: const Outlet(),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildTab(BuildContext context, String label, String path) {
    final state = context.maybeRouteState;
    final isActive = state?.location.uri.path == path;

    return Expanded(
      child: InkWell(
        onTap: () => context.navigate(.parse(path)),
        child: Container(
          padding: const EdgeInsets.symmetric(vertical: 14),
          decoration: BoxDecoration(
            border: Border(
              bottom: BorderSide(
                color: isActive ? Colors.orange : Colors.transparent,
                width: 3,
              ),
            ),
          ),
          child: Text(
            label,
            textAlign: TextAlign.center,
            style: TextStyle(
              fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
              color: isActive ? Colors.orange.shade900 : Colors.black87,
            ),
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _AnimatedPanel(
      icon: Icons.animation,
      title: 'Nested transitions',
      subtitle:
          'Only this panel animates while the layout header stays in place.',
      color: Colors.deepOrange,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const [
          Text('• Switch tabs to see push/replace/pop transitions.'),
          SizedBox(height: 8),
          Text('• The Outlet region animates independently.'),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _AnimatedPanel(
      icon: Icons.settings,
      title: 'Details',
      subtitle: 'Route animation can be customized per nested page.',
      color: Colors.orange.shade700,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const [
          Text('• Fade + slide driven by routeAnimation.'),
          SizedBox(height: 8),
          Text('• Duration can be tuned per route.'),
          SizedBox(height: 8),
          Text('• Works for leaf routes inside layouts.'),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _AnimatedPanel(
      icon: Icons.star,
      title: 'Reviews',
      subtitle: 'Animated panel with stacked transitions.',
      color: Colors.brown,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: const [
          Text('“Feels like Navigator transitions.”'),
          SizedBox(height: 8),
          Text('“Works in nested layouts too.”'),
          SizedBox(height: 8),
          Text('“No Navigator 1.0 dependency.”'),
        ],
      ),
    );
  }
}

class _AnimatedPanel extends StatelessWidget {
  const _AnimatedPanel({
    required this.icon,
    required this.title,
    required this.subtitle,
    required this.color,
    required this.child,
  });

  final IconData icon;
  final String title;
  final String subtitle;
  final Color color;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final controller = context.routeAnimation(
      duration: const Duration(milliseconds: 260),
    );
    final animation = CurvedAnimation(
      parent: controller,
      curve: Curves.easeOut,
      reverseCurve: Curves.easeIn,
    );

    return FadeTransition(
      opacity: animation,
      child: SlideTransition(
        position: Tween<Offset>(
          begin: const Offset(0.08, 0),
          end: Offset.zero,
        ).animate(animation),
        child: Card(
          elevation: 1,
          child: Padding(
            padding: const EdgeInsets.all(20),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Row(
                  children: [
                    CircleAvatar(
                      backgroundColor: color,
                      child: Icon(icon, color: Colors.white),
                    ),
                    const SizedBox(width: 12),
                    Text(
                      title,
                      style: const TextStyle(
                        fontSize: 18,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 12),
                Text(
                  subtitle,
                  style: TextStyle(color: Colors.grey.shade700),
                ),
                const SizedBox(height: 16),
                child,
              ],
            ),
          ),
        ),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Navigator 1.0 Page')),
      body: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Icon(Icons.open_in_new, size: 80, color: Colors.deepPurple),
            const SizedBox(height: 24),
            const Text(
              'Pushed with Navigator.of(context).push',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 16),
            ),
            const SizedBox(height: 24),
            ElevatedButton(
              onPressed: () => Navigator.of(context).pop(),
              child: const Text('Pop'),
            ),
          ],
        ),
      ),
    );
  }
}

// Auth Layout (Layout Route - no path segment)
class AuthLayout extends StatelessWidget {
  const AuthLayout({super.key});

  @override
  Widget build(BuildContext context) {
    print('Auth');
    return Scaffold(
      body: Container(
        decoration: BoxDecoration(
          gradient: LinearGradient(
            begin: Alignment.topLeft,
            end: Alignment.bottomRight,
            colors: [Colors.green.shade300, Colors.green.shade700],
          ),
        ),
        child: SafeArea(
          child: Column(
            children: [
              // Header
              Padding(
                padding: const EdgeInsets.all(24),
                child: Row(
                  children: [
                    IconButton(
                      icon: const Icon(Icons.arrow_back, color: Colors.white),
                      onPressed: () => context.navigate.back(),
                    ),
                    const SizedBox(width: 8),
                    const Text(
                      'Authentication',
                      style: TextStyle(
                        color: Colors.white,
                        fontSize: 24,
                        fontWeight: FontWeight.bold,
                      ),
                    ),
                  ],
                ),
              ),
              // Child routes render here
              const Expanded(child: Outlet()),
            ],
          ),
        ),
      ),
    );
  }
}

// Login Page
class Login extends StatelessWidget {
  const Login({super.key});

  @override
  Widget build(BuildContext context) {
    print('Login');
    return Center(
      child: Card(
        margin: const EdgeInsets.all(32),
        child: Padding(
          padding: const EdgeInsets.all(32),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Icon(Icons.login, size: 64, color: Colors.green),
              const SizedBox(height: 24),
              const Text(
                'Login',
                style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 32),
              TextField(
                decoration: InputDecoration(
                  labelText: 'Email',
                  border: const OutlineInputBorder(),
                  prefixIcon: const Icon(Icons.email),
                  filled: true,
                  fillColor: Colors.grey.shade100,
                ),
              ),
              const SizedBox(height: 16),
              TextField(
                obscureText: true,
                decoration: InputDecoration(
                  labelText: 'Password',
                  border: const OutlineInputBorder(),
                  prefixIcon: const Icon(Icons.lock),
                  filled: true,
                  fillColor: Colors.grey.shade100,
                ),
              ),
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: () {},
                style: ElevatedButton.styleFrom(
                  minimumSize: const Size(double.infinity, 50),
                  backgroundColor: Colors.green,
                  foregroundColor: Colors.white,
                ),
                child: const Text('Sign In'),
              ),
              const SizedBox(height: 16),
              TextButton(
                onPressed: () => context.navigate(.parse('/register')),
                child: const Text('Don\'t have an account? Register'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// Register Page
class Register extends StatelessWidget {
  const Register({super.key});

  @override
  Widget build(BuildContext context) {
    print('Register');
    return Center(
      child: Card(
        margin: const EdgeInsets.all(32),
        child: Padding(
          padding: const EdgeInsets.all(32),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              const Icon(Icons.person_add, size: 64, color: Colors.green),
              const SizedBox(height: 24),
              const Text(
                'Register',
                style: TextStyle(fontSize: 32, fontWeight: FontWeight.bold),
              ),
              const SizedBox(height: 32),
              TextField(
                decoration: InputDecoration(
                  labelText: 'Name',
                  border: const OutlineInputBorder(),
                  prefixIcon: const Icon(Icons.person),
                  filled: true,
                  fillColor: Colors.grey.shade100,
                ),
              ),
              const SizedBox(height: 16),
              TextField(
                decoration: InputDecoration(
                  labelText: 'Email',
                  border: const OutlineInputBorder(),
                  prefixIcon: const Icon(Icons.email),
                  filled: true,
                  fillColor: Colors.grey.shade100,
                ),
              ),
              const SizedBox(height: 16),
              TextField(
                obscureText: true,
                decoration: InputDecoration(
                  labelText: 'Password',
                  border: const OutlineInputBorder(),
                  prefixIcon: const Icon(Icons.lock),
                  filled: true,
                  fillColor: Colors.grey.shade100,
                ),
              ),
              const SizedBox(height: 24),
              ElevatedButton(
                onPressed: () {},
                style: ElevatedButton.styleFrom(
                  minimumSize: const Size(double.infinity, 50),
                  backgroundColor: Colors.green,
                  foregroundColor: Colors.white,
                ),
                child: const Text('Create Account'),
              ),
              const SizedBox(height: 16),
              TextButton(
                onPressed: () => context.navigate(.parse('/login')),
                child: const Text('Already have an account? Login'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

// Concerts Layout (Nested Route)
class ConcertsLayout extends StatelessWidget {
  const ConcertsLayout({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Concerts'),
        backgroundColor: Colors.orange,
        foregroundColor: Colors.white,
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => context.navigate.back(),
        ),
      ),
      body: Column(
        children: [
          // Navigation tabs
          Container(
            color: Colors.orange.shade100,
            child: Row(
              children: [
                _buildTab(context, 'All', '/concerts'),
                _buildTab(context, 'Trending', '/concerts/trending'),
                _buildTab(context, 'Tokyo', '/concerts/tokyo'),
                _buildTab(context, 'NYC', '/concerts/new-york'),
              ],
            ),
          ),
          // Child routes render here
          const Expanded(child: Outlet()),
        ],
      ),
    );
  }

  Widget _buildTab(BuildContext context, String label, String path) {
    final state = context.maybeRouteState;
    final isActive = state?.location.uri.path == path;

    return Expanded(
      child: InkWell(
        onTap: () => router.navigate(.parse(path)),
        child: Container(
          padding: const EdgeInsets.symmetric(vertical: 16),
          decoration: BoxDecoration(
            border: Border(
              bottom: BorderSide(
                color: isActive ? Colors.orange : Colors.transparent,
                width: 3,
              ),
            ),
          ),
          child: Text(
            label,
            textAlign: TextAlign.center,
            style: TextStyle(
              fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
              color: isActive ? Colors.orange.shade900 : Colors.black87,
            ),
          ),
        ),
      ),
    );
  }
}

// Concerts Home (Index Route)
class ConcertsHome extends StatelessWidget {
  const ConcertsHome({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        _buildConcertCard(
          'Summer Music Festival',
          'Various Artists',
          'July 15-17, 2025',
          Icons.festival,
          Colors.purple,
        ),
        _buildConcertCard(
          'Rock Legends Live',
          'Classic Rock Band',
          'August 5, 2025',
          Icons.music_note,
          Colors.red,
        ),
        _buildConcertCard(
          'Jazz Night',
          'Jazz Ensemble',
          'September 12, 2025',
          Icons.piano,
          Colors.blue,
        ),
        _buildConcertCard(
          'Electronic Dreams',
          'DJ Mix',
          'October 20, 2025',
          Icons.headphones,
          Colors.cyan,
        ),
      ],
    );
  }

  Widget _buildConcertCard(
    String title,
    String artist,
    String date,
    IconData icon,
    Color color,
  ) {
    return Card(
      margin: const EdgeInsets.only(bottom: 16),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: color,
          child: Icon(icon, color: Colors.white),
        ),
        title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
        subtitle: Text('$artist\n$date'),
        isThreeLine: true,
        trailing: const Icon(Icons.arrow_forward_ios),
      ),
    );
  }
}

// City Page (Dynamic Route)
class CityPage extends StatelessWidget {
  const CityPage({super.key});

  @override
  Widget build(BuildContext context) {
    final state = context.routeState;
    final city = state.params['city'] ?? 'Unknown';
    final displayCity = city
        .replaceAll('-', ' ')
        .split(' ')
        .map((word) => word[0].toUpperCase() + word.substring(1))
        .join(' ');

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Icon(Icons.location_city, size: 80, color: Colors.orange),
          const SizedBox(height: 24),
          Text(
            'Concerts in $displayCity',
            style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
          ),
          const SizedBox(height: 16),
          Text(
            'Route param: $city',
            style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
          ),
          const SizedBox(height: 32),
          const Padding(
            padding: EdgeInsets.symmetric(horizontal: 48),
            child: Text(
              'Showing all concerts happening in this city.',
              textAlign: TextAlign.center,
              style: TextStyle(fontSize: 16),
            ),
          ),
        ],
      ),
    );
  }
}

// Trending Page
class TrendingPage extends StatelessWidget {
  const TrendingPage({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        const Padding(
          padding: EdgeInsets.all(16),
          child: Text(
            '🔥 Trending Now',
            style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
          ),
        ),
        _buildTrendingCard('The Weeknd World Tour', '1.2M interested', 1),
        _buildTrendingCard('Taylor Swift Eras Tour', '980K interested', 2),
        _buildTrendingCard('Coldplay Concert', '850K interested', 3),
        _buildTrendingCard('Billie Eilish Live', '720K interested', 4),
        _buildTrendingCard('Ed Sheeran +–=÷× Tour', '690K interested', 5),
      ],
    );
  }

  Widget _buildTrendingCard(String title, String interest, int rank) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: rank <= 3 ? Colors.orange : Colors.grey,
          child: Text(
            '#$rank',
            style: const TextStyle(
              color: Colors.white,
              fontWeight: FontWeight.bold,
            ),
          ),
        ),
        title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
        subtitle: Text(interest),
        trailing: const Icon(Icons.trending_up, color: Colors.orange),
      ),
    );
  }
}

// Products Page - demonstrates widget-scoped routing with Routes widget
class ProductsPage extends StatelessWidget {
  const ProductsPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Products'),
        backgroundColor: Colors.purple,
        foregroundColor: Colors.white,
        leading: IconButton(
          icon: const Icon(Icons.arrow_back),
          onPressed: () => context.navigate.back(),
        ),
      ),
      body: Column(
        children: [
          // Info banner
          Container(
            padding: const EdgeInsets.all(16),
            color: Colors.purple.shade50,
            child: Row(
              children: [
                Icon(Icons.info_outline, color: Colors.purple.shade700),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    'This page uses Routes widget for widget-scoped routing',
                    style: TextStyle(
                      fontSize: 12,
                      color: Colors.purple.shade700,
                      fontStyle: FontStyle.italic,
                    ),
                  ),
                ),
              ],
            ),
          ),
          // Widget-scoped routes defined with Routes widget
          Expanded(
            child: Routes(const [
              Inlet(factory: ProductsList.new),
              Inlet(path: ':id', factory: ProductDetail.new),
            ]),
          ),
        ],
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return ListView(
      padding: const EdgeInsets.all(16),
      children: [
        const Padding(
          padding: EdgeInsets.symmetric(vertical: 16),
          child: Text(
            'All Products',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
        ),
        _buildProductCard(context, '1', 'Laptop', '\$999', Icons.laptop),
        _buildProductCard(context, '2', 'Phone', '\$699', Icons.phone_android),
        _buildProductCard(context, '3', 'Tablet', '\$499', Icons.tablet),
        _buildProductCard(
          context,
          '4',
          'Headphones',
          '\$199',
          Icons.headphones,
        ),
        _buildProductCard(context, '5', 'Watch', '\$299', Icons.watch),
      ],
    );
  }

  Widget _buildProductCard(
    BuildContext context,
    String id,
    String name,
    String price,
    IconData icon,
  ) {
    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      child: ListTile(
        leading: CircleAvatar(
          backgroundColor: Colors.purple,
          child: Icon(icon, color: Colors.white, size: 20),
        ),
        title: Text(name, style: const TextStyle(fontWeight: FontWeight.bold)),
        subtitle: Text(price),
        trailing: const Icon(Icons.arrow_forward_ios, size: 16),
        onTap: () => router.navigate(.parse('/products/$id')),
      ),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final state = context.routeState;
    final productId = state.params['id'] ?? 'unknown';

    final products = {
      '1': ('Laptop', '\$999', Icons.laptop),
      '2': ('Phone', '\$699', Icons.phone_android),
      '3': ('Tablet', '\$499', Icons.tablet),
      '4': ('Headphones', '\$199', Icons.headphones),
      '5': ('Watch', '\$299', Icons.watch),
    };

    final product = products[productId];

    return Center(
      child: Padding(
        padding: const EdgeInsets.all(32),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            CircleAvatar(
              radius: 48,
              backgroundColor: Colors.purple,
              child: Icon(
                product?.$3 ?? Icons.shopping_cart,
                color: Colors.white,
                size: 48,
              ),
            ),
            const SizedBox(height: 24),
            Text(
              product?.$1 ?? 'Unknown Product',
              style: const TextStyle(fontSize: 28, fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Text(
              product?.$2 ?? '\$0',
              style: TextStyle(
                fontSize: 24,
                color: Colors.purple.shade700,
                fontWeight: FontWeight.bold,
              ),
            ),
            const SizedBox(height: 16),
            Chip(
              label: Text('Product ID: $productId'),
              backgroundColor: Colors.purple.shade50,
            ),
            const SizedBox(height: 32),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton.icon(
                  onPressed: () => context.navigate.back(),
                  icon: const Icon(Icons.arrow_back),
                  label: const Text('Back to List'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.purple,
                    foregroundColor: Colors.white,
                  ),
                ),
                const SizedBox(width: 16),
                ElevatedButton.icon(
                  onPressed: () {},
                  icon: const Icon(Icons.add_shopping_cart),
                  label: const Text('Add to Cart'),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.green,
                    foregroundColor: Colors.white,
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}
5
likes
150
points
499
downloads

Publisher

verified publishermedz.dev

Weekly Downloads

A Flutter router that gives you routing flexibility: define routes centrally, scope them to widgets, or mix both - with browser-style history navigation.

Repository (GitHub)
View/report issues

Topics

#router #routing #navigation

Documentation

API reference

License

MIT (license)

Dependencies

flutter, meta, web

More

Packages that depend on unrouter