flutter_quick_router 0.0.4 copy "flutter_quick_router: ^0.0.4" to clipboard
flutter_quick_router: ^0.0.4 copied to clipboard

A Flutter package for easy route navigation with Navigator, supporting regular and restorable routes with various transitions.

example/lib/main.dart

// Example app for `flutter_quick_router`.
//
// This app wires up every screen through `MaterialApp.onGenerateRoute` via
// `QuickRouter.onGenerateRoute`, then exercises (almost) every extension
// method on `BuildContext` that the package provides.
//
// All five built-in transitions (`QuickFade`, `QuickSlide`, `QuickScale`,
// `QuickRotate`, `QuickSize`) are used TWICE each: once on a *named* route
// (registered in `appRoutes` below, so it's reused every time that route
// name is pushed) and once on a *normal* / unnamed push (passed inline to
// `context.to()`, `pushReplacement()`, `pushAndRemoveUntil()`, or a manual
// `QuickRouter.builder()` call), each time with different constructor
// parameters to show how each transition can be tuned. Each transition also
// gets its own accent color + icon (see `fadeStyle`, `slideStyle`, etc.
// below) purely for visual polish — that part has nothing to do with the
// package itself.
//
//   Transition   | Named route     | Unnamed / normal push
//   ------------ | ---------------- | -----------------------------------
//   QuickFade    | FadeScreen       | temporary auto-removed toast route
//   QuickSlide   | SlideScreen      | Home -> UnnamedDetailScreen (slide up)
//   QuickScale   | ScaleScreen      | RotateScreen -> UnnamedDetailScreen
//   QuickRotate  | RotateScreen     | Home -> pushAndRemoveUntil()
//   QuickSize    | SizeScreen       | ScaleScreen -> pushReplacement()
//
// (The restorable custom-route-builder demo and the Login -> Dashboard
// flow both omit `transitions` entirely, showing that `context.to()` and
// friends fall back to `QuickFade` by default when no transition is given.)
//
// NOT demonstrated here: `replace()`, `restorableReplace()`,
// `replaceRouteBelow()`, and `restorableReplaceRouteBelow()`. As currently
// implemented, these build a brand-new route for `old`/`anchor` instead of
// reusing the route object that is actually in the Navigator's history, so
// `Navigator` cannot find a match and throws an assertion error at runtime.
// (`replace()` also passes `old` instead of `to` when building the
// replacement route, so it would show the wrong screen even if the lookup
// succeeded.) Avoid these four methods until that's fixed upstream.
import 'package:flutter/material.dart';
import 'package:flutter_quick_router/Routers/router_builders.dart';
import 'package:flutter_quick_router/quick_router.dart';

// -----------------------------------------------------------------------
// Visual theme for each transition. Purely cosmetic — maps a transition
// name to an accent color + icon so every screen that uses it (named or
// unnamed) looks consistent.
// -----------------------------------------------------------------------

class TransitionStyle {
  const TransitionStyle(this.label, this.icon, this.color);

  final String label;
  final IconData icon;
  final Color color;
}

const TransitionStyle fadeStyle =
    TransitionStyle('Fade', Icons.blur_on_rounded, Color(0xFFEF6C00));
const TransitionStyle slideStyle =
    TransitionStyle('Slide', Icons.swipe_rounded, Color(0xFF1E88E5));
const TransitionStyle scaleStyle =
    TransitionStyle('Scale', Icons.zoom_out_map_rounded, Color(0xFF00897B));
const TransitionStyle rotateStyle =
    TransitionStyle('Rotate', Icons.rotate_right_rounded, Color(0xFFD81B60));
const TransitionStyle sizeStyle =
    TransitionStyle('Size', Icons.unfold_more_rounded, Color(0xFF5E35B1));
const TransitionStyle authStyle =
    TransitionStyle('Auth flow', Icons.lock_person_rounded, Color(0xFF2E7D32));

final List<QuickNamedRoute> appRoutes = <QuickNamedRoute>[
  QuickNamedRoute(
    name: HomeScreen.routeName,
    builder: (context, arguments) => const HomeScreen(),
  ),
  QuickNamedRoute(
    name: FadeScreen.routeName,
    builder: (context, arguments) => const FadeScreen(),
    transitions: const QuickFade(),
  ),
  QuickNamedRoute(
    name: SlideScreen.routeName,
    builder: (context, arguments) => SlideScreen(message: arguments as String?),
    transitions: const QuickSlide(),
  ),
  QuickNamedRoute(
    name: ScaleScreen.routeName,
    builder: (context, arguments) => const ScaleScreen(),
    transitions: const QuickScale(),
  ),
  QuickNamedRoute(
    name: RotateScreen.routeName,
    builder: (context, arguments) =>
        RotateScreen(message: arguments as String?),
    transitions: const QuickRotate(),
  ),
  QuickNamedRoute(
    name: SizeScreen.routeName,
    builder: (context, arguments) => const SizeScreen(),
    transitions: const QuickSize(),
  ),
];

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

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

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme =
        ColorScheme.fromSeed(seedColor: const Color(0xFF6750A4));

    return MaterialApp(
      title: 'Quick Router',
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: scheme,
        scaffoldBackgroundColor: const Color(0xFFF5F3FA),
        appBarTheme: const AppBarTheme(
          elevation: 0,
          centerTitle: false,
          backgroundColor: Colors.transparent,
          foregroundColor: Colors.white,
        ),
        chipTheme: ChipThemeData(
          backgroundColor: scheme.surfaceContainerHighest,
          labelStyle: TextStyle(
              color: scheme.onSurfaceVariant, fontWeight: FontWeight.w600),
          padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
          shape: const StadiumBorder(),
        ),
        filledButtonTheme: FilledButtonThemeData(
          style: FilledButton.styleFrom(
            padding: const EdgeInsets.symmetric(vertical: 14),
            shape:
                RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
            textStyle: const TextStyle(fontWeight: FontWeight.w600),
          ),
        ),
        outlinedButtonTheme: OutlinedButtonThemeData(
          style: OutlinedButton.styleFrom(
            padding: const EdgeInsets.symmetric(vertical: 14),
            shape:
                RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
            textStyle: const TextStyle(fontWeight: FontWeight.w600),
          ),
        ),
      ),
      restorationScopeId: 'quick_router_example',
      initialRoute: HomeScreen.routeName,
      // Required for QuickNamedRoute.transitions to take effect.
      onGenerateRoute: QuickRouter.onGenerateRoute(appRoutes),
    );
  }
}

/// Route builder used with `restorablePushAndRemoveUntil`. Restorable route
/// builders must be top-level or static functions annotated with
/// `@pragma('vm:entry-point')` so they survive tree-shaking / AOT builds.
class AppRouteBuilders {
  @pragma('vm:entry-point')
  static Route<Object?> unnamedDetailRouteBuilder(
    BuildContext context,
    Object? arguments,
  ) {
    return QuickRouter.builder<Object?>(
      UnnamedDetailScreen(
        title: 'Restorable detail screen',
        subtitle:
            arguments as String? ?? 'Pushed via restorablePushAndRemoveUntil',
        style: fadeStyle,
      ),
      // Passing `null` here falls back to QuickRouter's default transition,
      // which is a QuickFade().
      null,
    );
  }
}

// ---------------------------------------------------------------------------
// Home screen: unnamed navigation, "remove until", and restorable pushes.
// ---------------------------------------------------------------------------

class HomeScreen extends StatelessWidget {
  static const String routeName = '/';

  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    final ColorScheme scheme = Theme.of(context).colorScheme;

    return Scaffold(
      body: CustomScrollView(
        slivers: [
          SliverAppBar(
            pinned: true,
            expandedHeight: 160,
            backgroundColor: scheme.primary,
            flexibleSpace: FlexibleSpaceBar(
              titlePadding: const EdgeInsets.only(left: 20, bottom: 16),
              title: const Text(
                'Quick Router',
                style:
                    TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
              ),
              background: Container(
                decoration: BoxDecoration(
                  gradient: LinearGradient(
                    begin: Alignment.topLeft,
                    end: Alignment.bottomRight,
                    colors: [scheme.primary, scheme.tertiary],
                  ),
                ),
                child: const Align(
                  alignment: Alignment.bottomRight,
                  child: Padding(
                    padding: EdgeInsets.only(right: 8, bottom: 56),
                    child: Icon(Icons.route_rounded,
                        size: 90, color: Colors.white24),
                  ),
                ),
              ),
            ),
          ),
          SliverPadding(
            padding: const EdgeInsets.fromLTRB(16, 20, 16, 32),
            sliver: SliverList.list(
              children: [
                const Text(
                  'Tap around to try every navigation method the package '
                  'ships with, each paired with a distinctly styled transition.',
                  style: TextStyle(color: Colors.black54, height: 1.4),
                ),
                const SizedBox(height: 20),
                _SectionCard(
                  icon: Icons.label_rounded,
                  iconColor: scheme.primary,
                  title: 'Named navigation',
                  subtitle:
                      'Routes registered in appRoutes via onGenerateRoute',
                  children: [
                    _DemoButton(
                      icon: fadeStyle.icon,
                      color: fadeStyle.color,
                      label: 'Push named fade screen',
                      onPressed: () => context.pushNamed(FadeScreen.routeName),
                    ),
                    _DemoButton(
                      icon: rotateStyle.icon,
                      color: rotateStyle.color,
                      label: 'Push named rotate screen (restorable)',
                      onPressed: () => context.restorablePushNamed(
                        RotateScreen.routeName,
                        arguments: 'Pushed via restorablePushNamed',
                      ),
                    ),
                    _DemoButton(
                      icon: slideStyle.icon,
                      color: slideStyle.color,
                      label:
                          'Push named slide, dropping this stack (restorable)',
                      onPressed: () =>
                          context.restorablePushNamedAndRemoveUntil(
                        SlideScreen.routeName,
                        (route) => false,
                        arguments:
                            'Pushed via restorablePushNamedAndRemoveUntil',
                      ),
                    ),
                    _DemoButton(
                      icon: sizeStyle.icon,
                      color: sizeStyle.color,
                      label: 'Push named size, keep only Home below it',
                      onPressed: () => context.pushNamedAndRemoveUntil(
                        SizeScreen.routeName,
                        (route) => route.isFirst,
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                _SectionCard(
                  icon: Icons.widgets_rounded,
                  iconColor: scheme.tertiary,
                  title: 'Unnamed navigation',
                  subtitle: 'Widgets pushed directly — no route name required',
                  children: [
                    _DemoButton(
                      icon: slideStyle.icon,
                      color: slideStyle.color,
                      label: 'context.to() an unnamed screen (Slide)',
                      onPressed: () => context.to(
                        const UnnamedDetailScreen(
                          title: 'Unnamed detail screen',
                          subtitle:
                              'Pushed directly with context.to(), no named route required.',
                          style: slideStyle,
                        ),
                        transitions: const QuickSlide(start: Offset(0, 1)),
                      ),
                    ),
                    _DemoButton(
                      icon: rotateStyle.icon,
                      color: rotateStyle.color,
                      label: 'pushAndRemoveUntil() an unnamed screen (Rotate)',
                      onPressed: () => context.pushAndRemoveUntil(
                        const UnnamedDetailScreen(
                          title: 'Unnamed detail screen',
                          subtitle: 'The whole stack below it was cleared.',
                          style: rotateStyle,
                        ),
                        (route) => false,
                        transitions: const QuickRotate(),
                      ),
                    ),
                    _DemoButton(
                      icon: Icons.restore_rounded,
                      color: scheme.secondary,
                      label:
                          'Restorable push & remove until (custom builder, default Fade)',
                      onPressed: () => context.restorablePushAndRemoveUntil(
                        AppRouteBuilders.unnamedDetailRouteBuilder,
                        (route) => false,
                        arguments: 'Pushed via restorablePushAndRemoveUntil',
                      ),
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                _SectionCard(
                  icon: Icons.layers_clear_rounded,
                  iconColor: fadeStyle.color,
                  title: 'removeRoute()',
                  subtitle:
                      'Drops a route from history directly, without popping',
                  children: [
                    _DemoButton(
                      icon: Icons.timer_outlined,
                      color: fadeStyle.color,
                      label:
                          'Push a temporary screen that removes itself after 2s',
                      onPressed: () {
                        // removeRoute() needs the actual Route object, which
                        // context.to() does not expose. Build and push it
                        // manually with QuickRouter.builder() to keep a
                        // reference.
                        final Route<void> tempRoute = QuickRouter.builder<void>(
                          const UnnamedDetailScreen(
                            title: 'Temporary screen',
                            subtitle:
                                'This route calls context.removeRoute() on itself '
                                'after 2 seconds \u2014 no back button needed.',
                            style: fadeStyle,
                          ),
                          const QuickFade(),
                        );
                        Navigator.of(context).push(tempRoute);
                        Future.delayed(const Duration(seconds: 2), () {
                          if (context.mounted) {
                            context.removeRoute(tempRoute);
                          }
                        });
                      },
                    ),
                  ],
                ),
                const SizedBox(height: 16),
                _SectionCard(
                  icon: Icons.vertical_align_bottom_rounded,
                  iconColor: authStyle.color,
                  title: 'removeRouteBelow()',
                  subtitle:
                      'A realistic login flow that drops itself from history',
                  children: [
                    _DemoButton(
                      icon: authStyle.icon,
                      color: authStyle.color,
                      label: 'Login \u2192 Dashboard flow',
                      onPressed: () => context.to(const LoginDemoScreen()),
                    ),
                  ],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Fade screen: back(), pushReplacementNamed(), popAndPushNamed(), maybePop().
// ---------------------------------------------------------------------------

class FadeScreen extends StatelessWidget {
  static const String routeName = '/fade';

  const FadeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return _TransitionScaffold(
      style: fadeStyle,
      title: 'Fade Screen',
      description: 'Registered as a named route with QuickFade().',
      children: [
        _CanPopBadge(canPop: context.canPop()),
        const SizedBox(height: 20),
        _DemoButton(
          icon: Icons.arrow_back_rounded,
          color: fadeStyle.color,
          label: 'Go back with a result',
          onPressed: () => context.back('Hello from FadeScreen'),
        ),
        _DemoButton(
          icon: slideStyle.icon,
          color: slideStyle.color,
          label: 'Replace with named slide screen',
          onPressed: () => context.pushReplacementNamed(
            SlideScreen.routeName,
            arguments: 'Hello from FadeScreen',
          ),
        ),
        _DemoButton(
          icon: scaleStyle.icon,
          color: scaleStyle.color,
          label: 'Pop and push named scale screen',
          onPressed: () => context.popAndPushNamed(ScaleScreen.routeName),
        ),
        _DemoButton(
          icon: Icons.exit_to_app_rounded,
          color: fadeStyle.color,
          label: 'maybePop()',
          onPressed: () async {
            final bool popped = await context.maybePop('Hello from FadeScreen');
            if (!popped && context.mounted) {
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(content: Text('Nothing left to pop.')),
              );
            }
          },
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Slide screen: reads arguments, restorable replace/pop-and-push.
// ---------------------------------------------------------------------------

class SlideScreen extends StatelessWidget {
  static const String routeName = '/slide';

  const SlideScreen({super.key, this.message});

  final String? message;

  @override
  Widget build(BuildContext context) {
    return _TransitionScaffold(
      style: slideStyle,
      title: 'Slide Screen',
      description: 'Registered as a named route with QuickSlide().',
      children: [
        if (message != null) ...[
          _MessageBanner(message: message!, color: slideStyle.color),
          const SizedBox(height: 20),
        ],
        _DemoButton(
          icon: Icons.home_rounded,
          color: slideStyle.color,
          label: 'Push named home screen',
          onPressed: () => context.pushNamed(HomeScreen.routeName),
        ),
        _DemoButton(
          icon: rotateStyle.icon,
          color: rotateStyle.color,
          label: 'Replace with named rotate screen (restorable)',
          onPressed: () => context.restorablePushReplacementNamed(
            RotateScreen.routeName,
            arguments: 'Hello from SlideScreen',
          ),
        ),
        _DemoButton(
          icon: sizeStyle.icon,
          color: sizeStyle.color,
          label: 'Pop and push named size screen (restorable)',
          onPressed: () =>
              context.restorablePopAndPushNamed(SizeScreen.routeName),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Scale screen: pushReplacement() with an unnamed widget, popUntil().
// ---------------------------------------------------------------------------

class ScaleScreen extends StatelessWidget {
  static const String routeName = '/scale';

  const ScaleScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return _TransitionScaffold(
      style: scaleStyle,
      title: 'Scale Screen',
      description: 'Registered as a named route with QuickScale().',
      children: [
        _DemoButton(
          icon: Icons.arrow_back_rounded,
          color: scaleStyle.color,
          label: 'Go back with a result',
          onPressed: () => context.back('Hello from ScaleScreen'),
        ),
        _DemoButton(
          icon: sizeStyle.icon,
          color: sizeStyle.color,
          label: 'Replace with an unnamed screen (pushReplacement, Size)',
          onPressed: () => context.pushReplacement(
            const UnnamedDetailScreen(
              title: 'Unnamed detail screen',
              subtitle: 'Reached via context.pushReplacement() with a '
                  'horizontal QuickSize transition.',
              style: sizeStyle,
            ),
            transitions:
                const QuickSize(axis: Axis.horizontal, axisAlignment: -1.0),
          ),
        ),
        _DemoButton(
          icon: Icons.first_page_rounded,
          color: scaleStyle.color,
          label: 'Pop back to the first route (popUntil)',
          onPressed: () => context.popUntil((route) => route.isFirst),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Rotate screen: reads arguments, context.to() with the QuickScale transition.
// ---------------------------------------------------------------------------

class RotateScreen extends StatelessWidget {
  static const String routeName = '/rotate';

  const RotateScreen({super.key, this.message});

  final String? message;

  @override
  Widget build(BuildContext context) {
    return _TransitionScaffold(
      style: rotateStyle,
      title: 'Rotate Screen',
      description: 'Registered as a named route with QuickRotate().',
      children: [
        if (message != null) ...[
          _MessageBanner(message: message!, color: rotateStyle.color),
          const SizedBox(height: 20),
        ],
        _DemoButton(
          icon: Icons.arrow_back_rounded,
          color: rotateStyle.color,
          label: 'Go back with a result',
          onPressed: () => context.back('Hello from RotateScreen'),
        ),
        _DemoButton(
          icon: scaleStyle.icon,
          color: scaleStyle.color,
          label: 'context.to() an unnamed screen (Scale)',
          onPressed: () => context.to(
            const UnnamedDetailScreen(
              title: 'Unnamed detail screen',
              subtitle: 'Reached via context.to() with a QuickScale '
                  'transition anchored to the top-right corner.',
              style: scaleStyle,
            ),
            transitions: const QuickScale(alignment: Alignment.topRight),
          ),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Size screen: just showcases the QuickSize transition + canPop().
// ---------------------------------------------------------------------------

class SizeScreen extends StatelessWidget {
  static const String routeName = '/size';

  const SizeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return _TransitionScaffold(
      style: sizeStyle,
      title: 'Size Screen',
      description: 'Registered as a named route with QuickSize().',
      children: [
        _CanPopBadge(canPop: context.canPop()),
        const SizedBox(height: 20),
        _DemoButton(
          icon: Icons.arrow_back_rounded,
          color: sizeStyle.color,
          label: 'Go back with a result',
          onPressed: () => context.back('Hello from SizeScreen'),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// A plain, unnamed screen reused by several context.to() / pushReplacement()
// / pushAndRemoveUntil() / removeRoute() demos above. It is never registered
// in `appRoutes` \u2014 unnamed navigation doesn't require that. It takes a
// `style` so it visually matches whichever transition pushed it.
// ---------------------------------------------------------------------------

class UnnamedDetailScreen extends StatelessWidget {
  const UnnamedDetailScreen({
    super.key,
    required this.title,
    this.subtitle,
    this.style = fadeStyle,
  });

  final String title;
  final String? subtitle;
  final TransitionStyle style;

  @override
  Widget build(BuildContext context) {
    return _TransitionScaffold(
      style: style,
      title: title,
      description: 'An unnamed widget, not a registered named route.',
      children: [
        if (subtitle != null) ...[
          _MessageBanner(message: subtitle!, color: style.color),
          const SizedBox(height: 20),
        ],
        _CanPopBadge(canPop: context.canPop()),
        const SizedBox(height: 20),
        _DemoButton(
          icon: Icons.arrow_back_rounded,
          color: style.color,
          label: 'Go back',
          onPressed: () => context.back(),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Login \u2192 Dashboard flow: demonstrates removeRouteBelow() with a real
// anchor route (the current, already-on-the-stack ModalRoute), used to drop
// the Login screen from history once the user is signed in.
// ---------------------------------------------------------------------------

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

  @override
  Widget build(BuildContext context) {
    return _TransitionScaffold(
      style: authStyle,
      title: 'Login (demo)',
      description:
          'Pushed with context.to() and no transitions argument \u2014 '
          'falls back to the default QuickFade().',
      children: [
        _DemoButton(
          icon: Icons.login_rounded,
          color: authStyle.color,
          label: 'Log in',
          onPressed: () => context.to(const DashboardDemoScreen()),
        ),
      ],
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    return _TransitionScaffold(
      style: authStyle,
      title: 'Dashboard (demo)',
      description: 'You are logged in.',
      children: [
        _DemoButton(
          icon: Icons.layers_clear_rounded,
          color: authStyle.color,
          label: 'Drop Login screen from history',
          onPressed: () {
            final Route<dynamic>? current = ModalRoute.of(context);
            if (current != null) {
              // The Login screen sits directly below this Dashboard route
              // in the stack, so a system back / context.back() from here
              // will now return straight to Home.
              context.removeRouteBelow(current);
              ScaffoldMessenger.of(context).showSnackBar(
                const SnackBar(
                    content: Text('Login screen removed from history.')),
              );
            }
          },
        ),
        const SizedBox(height: 4),
        _DemoButton(
          icon: Icons.home_rounded,
          color: authStyle.color,
          label: 'Back to Home',
          onPressed: () => context.popUntil((route) => route.isFirst),
        ),
      ],
    );
  }
}

// ---------------------------------------------------------------------------
// Shared, styled scaffold for every screen reached through a transition: a
// colored gradient header with a back button + icon avatar sitting above a
// rounded white sheet holding the demo content. Purely cosmetic.
// ---------------------------------------------------------------------------

class _TransitionScaffold extends StatelessWidget {
  const _TransitionScaffold({
    required this.style,
    required this.title,
    required this.description,
    required this.children,
  });

  final TransitionStyle style;
  final String title;
  final String description;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: style.color,
      body: SafeArea(
        bottom: false,
        child: Column(
          children: [
            Padding(
              padding: const EdgeInsets.fromLTRB(8, 8, 20, 24),
              child: Row(
                children: [
                  IconButton(
                    onPressed: () => context.maybePop(),
                    icon: const Icon(Icons.arrow_back_rounded,
                        color: Colors.white),
                  ),
                  CircleAvatar(
                    radius: 22,
                    backgroundColor: Colors.white.withValues(alpha: 0.22),
                    child: Icon(style.icon, color: Colors.white),
                  ),
                  const SizedBox(width: 14),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          title,
                          style: const TextStyle(
                            color: Colors.white,
                            fontSize: 20,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        const SizedBox(height: 2),
                        Text(
                          description,
                          style: TextStyle(
                              color: Colors.white.withValues(alpha: 0.85),
                              fontSize: 12.5),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            Expanded(
              child: Container(
                width: double.infinity,
                decoration: const BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
                ),
                padding: const EdgeInsets.fromLTRB(20, 28, 20, 20),
                child: ListView(children: children),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

// ---------------------------------------------------------------------------
// Small shared widgets used across the demo screens.
// ---------------------------------------------------------------------------

/// A rounded, elevated group of related demo buttons with an icon + title
/// header, used on the Home screen.
class _SectionCard extends StatelessWidget {
  const _SectionCard({
    required this.icon,
    required this.iconColor,
    required this.title,
    required this.subtitle,
    required this.children,
  });

  final IconData icon;
  final Color iconColor;
  final String title;
  final String subtitle;
  final List<Widget> children;

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(22),
        boxShadow: [
          BoxShadow(
            color: Colors.black.withValues(alpha: 0.04),
            blurRadius: 16,
            offset: const Offset(0, 6),
          ),
        ],
      ),
      padding: const EdgeInsets.fromLTRB(18, 18, 18, 14),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(
            children: [
              Container(
                width: 40,
                height: 40,
                decoration: BoxDecoration(
                  color: iconColor.withValues(alpha: 0.12),
                  borderRadius: BorderRadius.circular(12),
                ),
                child: Icon(icon, color: iconColor, size: 22),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Text(title,
                        style: const TextStyle(
                            fontWeight: FontWeight.bold, fontSize: 16)),
                    Text(subtitle,
                        style: const TextStyle(
                            color: Colors.black54, fontSize: 12.5)),
                  ],
                ),
              ),
            ],
          ),
          const SizedBox(height: 14),
          ...children,
        ],
      ),
    );
  }
}

/// A full-width, icon-leading button used for every navigation demo.
class _DemoButton extends StatelessWidget {
  const _DemoButton({
    required this.icon,
    required this.color,
    required this.label,
    required this.onPressed,
  });

  final IconData icon;
  final Color color;
  final String label;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 10),
      child: SizedBox(
        width: double.infinity,
        child: OutlinedButton.icon(
          style: OutlinedButton.styleFrom(
            foregroundColor: color,
            side: BorderSide(color: color.withValues(alpha: 0.35)),
            backgroundColor: color.withValues(alpha: 0.06),
            alignment: Alignment.centerLeft,
          ),
          onPressed: onPressed,
          icon: Icon(icon, size: 20),
          label: Text(label, textAlign: TextAlign.left),
        ),
      ),
    );
  }
}

/// Shows the arguments string received by a screen in a soft, colored banner.
class _MessageBanner extends StatelessWidget {
  const _MessageBanner({required this.message, required this.color});

  final String message;
  final Color color;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(14),
      decoration: BoxDecoration(
        color: color.withValues(alpha: 0.08),
        borderRadius: BorderRadius.circular(14),
        border: Border.all(color: color.withValues(alpha: 0.25)),
      ),
      child: Row(
        children: [
          Icon(Icons.mail_outline_rounded, color: color, size: 18),
          const SizedBox(width: 10),
          Expanded(
            child: Text(
              'Argument received: "$message"',
              style: TextStyle(
                  color: color, fontWeight: FontWeight.w600, fontSize: 13),
            ),
          ),
        ],
      ),
    );
  }
}

/// Small badge reporting the live result of `context.canPop()`.
class _CanPopBadge extends StatelessWidget {
  const _CanPopBadge({required this.canPop});

  final bool canPop;

  @override
  Widget build(BuildContext context) {
    final Color color =
        canPop ? const Color(0xFF2E7D32) : const Color(0xFFC62828);
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
      decoration: BoxDecoration(
        color: color.withValues(alpha: 0.1),
        borderRadius: BorderRadius.circular(10),
      ),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        children: [
          Icon(canPop ? Icons.lock_open_rounded : Icons.lock_outline_rounded,
              color: color, size: 16),
          const SizedBox(width: 8),
          Text(
            'context.canPop() == $canPop',
            style: TextStyle(
                color: color, fontWeight: FontWeight.w600, fontSize: 12.5),
          ),
        ],
      ),
    );
  }
}
8
likes
150
points
144
downloads

Documentation

Documentation
API reference

Publisher

verified publishermetra.dev

Weekly Downloads

A Flutter package for easy route navigation with Navigator, supporting regular and restorable routes with various transitions.

Homepage
Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_quick_router