kaisel 0.19.0 copy "kaisel: ^0.19.0" to clipboard
kaisel: ^0.19.0 copied to clipboard

A Dart 3-native Flutter router built on sealed routes, pattern matching, and a stack-as-state model. No string paths, no codegen.

example/lib/main.dart

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

// 1. Routes
//
// In v0.4, each shell branch has its own sealed type. AppRoute contains
// only top-level concerns; HomeRoute / DiscoverRoute / ProfileRoute are
// separate hierarchies. The compiler enforces "you can't push a
// DiscoverRoute into the Home tab."

// Top-level routes (main router).
sealed class AppRoute extends KaiselRoute {
  const AppRoute();
}

final class Splash extends AppRoute {
  const Splash();
}

final class Login extends AppRoute {
  const Login();
}

abstract interface class RequiresAuth {}

final class MainShell extends AppRoute implements RequiresAuth {
  const MainShell();
}

final class Settings extends AppRoute implements RequiresAuth {
  const Settings();
}

// v0.6: a top-level marker route that mounts the Checkout module.
// The module owns its internal routes (CheckoutCart, CheckoutShipping,
// CheckoutConfirm) under its own sealed type. See CheckoutRoute below.
final class CheckoutMount extends AppRoute implements RequiresAuth {
  const CheckoutMount();
}

// Modal flow routes: still AppRoute, because flows run on the main
// router and overlay the entire app (including the shell chrome).
final class ConfirmAddToCart extends AppRoute
    implements RequiresAuth, KaiselModalRoute<int> {
  const ConfirmAddToCart(this.productId);
  final String productId;
  @override
  List<Object?> get props => [productId];
}

final class ConfirmAddToCartReview extends AppRoute implements RequiresAuth {
  const ConfirmAddToCartReview({
    required this.productId,
    required this.quantity,
  });
  final String productId;
  final int quantity;
  @override
  List<Object?> get props => [productId, quantity];
}

// Per-branch sealed hierarchies. Each branch's router is typed to its
// specific subtype.

sealed class HomeRoute extends KaiselRoute {
  const HomeRoute();
}

final class HomeRoot extends HomeRoute {
  const HomeRoot();
}

final class ProductDetail extends HomeRoute {
  const ProductDetail(this.id);
  final String id;
  @override
  List<Object?> get props => [id];
}

sealed class DiscoverRoute extends KaiselRoute {
  const DiscoverRoute();
}

final class DiscoverRoot extends DiscoverRoute {
  const DiscoverRoot();
}

final class FeedItem extends DiscoverRoute {
  const FeedItem(this.id);
  final String id;
  @override
  List<Object?> get props => [id];
}

sealed class ProfileRoute extends KaiselRoute {
  const ProfileRoute();
}

final class ProfileRoot extends ProfileRoute {
  const ProfileRoot();
}

// The Checkout module: a self-contained routing unit.
//
// In v0.6, a RouteModule packages a sealed subtype, an initial stack,
// a page builder, optional guards, and an optional page wrapper. The
// module is `const`-instantiable and would normally ship from a
// separate package (e.g. a payments SDK) without the host knowing
// about its internal routes.

sealed class CheckoutRoute extends KaiselRoute {
  const CheckoutRoute();
}

final class CheckoutCart extends CheckoutRoute {
  const CheckoutCart();
}

final class CheckoutShipping extends CheckoutRoute {
  const CheckoutShipping();
}

final class CheckoutConfirm extends CheckoutRoute {
  const CheckoutConfirm();
}

class CheckoutModule extends RouteModule<CheckoutRoute> {
  const CheckoutModule();

  @override
  List<CheckoutRoute> get initialStack => const [CheckoutCart()];

  @override
  Widget buildPage(BuildContext context, CheckoutRoute route) =>
      switch (route) {
        CheckoutCart() => const _CheckoutCartScreen(),
        CheckoutShipping() => const _CheckoutShippingScreen(),
        CheckoutConfirm() => const _CheckoutConfirmScreen(),
      };

  // v0.7: the module owns its own URL structure, relative to whatever
  // prefix the host mounts it at. The host's main codec stays
  // checkout-agnostic; ConfigCodecWithModules wires the prefix.
  @override
  ModuleStackCodec<CheckoutRoute>? get codec => const _CheckoutModuleCodec();
}

class _CheckoutModuleCodec extends ModuleStackCodec<CheckoutRoute> {
  const _CheckoutModuleCodec();

  @override
  List<String> encode(List<CheckoutRoute> stack) => switch (stack.last) {
    CheckoutCart() => const [],
    CheckoutShipping() => const ['shipping'],
    CheckoutConfirm() => const ['confirm'],
  };

  // A deep link to .../confirm restores the full [Cart, Shipping,
  // Confirm] stack so back unwinds through the flow rather than
  // popping straight out of the module.
  @override
  List<CheckoutRoute>? decode(List<String> segments) => switch (segments) {
    [] => const [CheckoutCart()],
    ['shipping'] => const [CheckoutCart(), CheckoutShipping()],
    ['confirm'] => const [
      CheckoutCart(),
      CheckoutShipping(),
      CheckoutConfirm(),
    ],
    _ => null,
  };
}

// 2. Auth state

class AuthState extends ValueNotifier<bool> {
  AuthState() : super(false);
  void logIn() => value = true;
  void logOut() => value = false;
}

final auth = AuthState();

// 3. Guards (main router only; once you're past MainShell the user is
//    authenticated for the rest of the session, so branch routers
//    don't need their own auth guards in this example).

KaiselGuard<AppRoute> authGuard(AuthState auth) {
  return (current, proposed) {
    final needsAuth = proposed.any((r) => r is RequiresAuth);
    if (needsAuth && !auth.value) return const [Login()];
    return proposed;
  };
}

KaiselGuard<AppRoute> splashRedirectGuard(AuthState auth) {
  return (current, proposed) {
    if (proposed.length == 1 && proposed.single is Splash) {
      return [auth.value ? const MainShell() : const Login()];
    }
    return proposed;
  };
}

// 4. URL codec: v0.5 KaiselConfigCodec, with shell URLs.
//
// v0.5's configuration carries both the main stack and (when a shell
// is mounted) the active branch's stack. We encode each branch into a
// distinct URL prefix so deep links can reach into the shell:
//
//   /                    → Splash
//   /login               → Login
//   /home                → MainShell, Home tab at root
//   /home/products/x     → MainShell, Home tab with ProductDetail(x)
//   /discover            → MainShell, Discover tab at root
//   /discover/items/y    → MainShell, Discover tab with FeedItem(y)
//   /profile             → MainShell, Profile tab
//   /settings            → MainShell, Settings on top

// The host's MAIN-stack codec. After v0.7 it doesn't know anything
// about the Checkout module's URL structure. That ships with the
// module. The composer below wires them together.
class _MainAppCodec implements KaiselConfigCodec<AppRoute> {
  const _MainAppCodec();

  static const _branchPrefixes = ['home', 'discover', 'profile'];

  @override
  Uri encode(KaiselConfig<AppRoute> config) {
    // Modal flow routes overlay the app; they don't take URLs of
    // their own. Encode the underlying state instead.
    final top = config.mainStack.last;
    if (top is ConfirmAddToCart || top is ConfirmAddToCartReview) {
      final base = config.mainStack
          .where((r) => r is! ConfirmAddToCart && r is! ConfirmAddToCartReview)
          .toList();
      return encode(
        KaiselConfig(
          mainStack: base.isEmpty ? const [Splash()] : base,
          nestedState: config.nestedState,
        ),
      );
    }
    // CheckoutMount is intentionally absent from this switch. The
    // composer ([ConfigCodecWithModules]) handles any URL whose top
    // route is a registered module mount, BEFORE delegating to this
    // base codec. By the time we get here, the top is non-module.
    return switch ((top, config.nestedState)) {
      (Splash(), _) => Uri(path: '/'),
      (Login(), _) => Uri(path: '/login'),
      (Settings(), _) => Uri(path: '/settings'),
      (MainShell(), final KaiselShellConfig shell) => _encodeShell(shell),
      (MainShell(), _) => Uri(path: '/home'),
      (ConfirmAddToCart() || ConfirmAddToCartReview(), _) => Uri(path: '/'),
      // CheckoutMount can't reach here in normal flow; the composer
      // catches it first. The fall-through is for defensive
      // exhaustiveness.
      (CheckoutMount(), _) => Uri(path: '/'),
    };
  }

  Uri _encodeShell(KaiselShellConfig shell) {
    final prefix = _branchPrefixes[shell.activeBranch];
    final stack = shell.activeBranchStack;
    if (stack.length == 1) return Uri(path: '/$prefix');
    return switch (shell.activeBranch) {
      0 => switch (stack) {
        [HomeRoot(), ProductDetail(:final id)] => Uri(
          path: '/home/products/$id',
        ),
        _ => Uri(path: '/home'),
      },
      1 => switch (stack) {
        [DiscoverRoot(), FeedItem(:final id)] => Uri(
          path: '/discover/items/$id',
        ),
        _ => Uri(path: '/discover'),
      },
      2 => Uri(path: '/profile'),
      _ => Uri(path: '/home'),
    };
  }

  @override
  KaiselConfig<AppRoute>? decode(Uri uri) {
    // The composer has already attempted module mounts before
    // delegating here, so /checkout/* URLs never reach this method.
    return switch (uri.pathSegments) {
      [] || [''] => KaiselConfig(mainStack: const [Splash()]),
      ['login'] => KaiselConfig(mainStack: const [Login()]),
      ['settings'] => KaiselConfig(mainStack: const [MainShell(), Settings()]),
      ['home'] => _shellAt(0, const [HomeRoot()]),
      ['home', 'products', final id] => _shellAt(0, [
        const HomeRoot(),
        ProductDetail(id),
      ]),
      ['discover'] => _shellAt(1, const [DiscoverRoot()]),
      ['discover', 'items', final id] => _shellAt(1, [
        const DiscoverRoot(),
        FeedItem(id),
      ]),
      ['profile'] => _shellAt(2, const [ProfileRoot()]),
      _ => null,
    };
  }

  KaiselConfig<AppRoute> _shellAt(int branch, List<KaiselRoute> stack) =>
      KaiselConfig(
        mainStack: const [MainShell()],
        nestedState: KaiselShellConfig(
          activeBranch: branch,
          activeBranchStack: stack,
        ),
      );
}

// v0.7: the composer. Takes a base codec for main-stack routes plus a
// list of module mounts. URLs under a mount's prefix go through the
// module's own codec; everything else falls through to the base.
//
// Each module's URL structure lives inside the module. _MainAppCodec
// is checkout-agnostic. Adding more modules means appending to
// [modules], not editing the main codec.
const appCodec = ConfigCodecWithModules<AppRoute>(
  baseCodec: _MainAppCodec(),
  modules: [
    ModuleMount(
      mountRoute: CheckoutMount(),
      prefix: '/checkout',
      codec: _CheckoutModuleCodec(),
    ),
  ],
);

// 5. Wire it up.
//
// The main router is global, bundled in _config. Branch routers are created in
// _MainShellScreen's State so they're tied to the shell's lifecycle.
//
// One value bundles the router, delegate, and URL parser (appCodec is a
// KaiselConfigCodec, so it passes straight in). Hold it as a top-level final.
final _config = KaiselRouterConfig<AppRoute>(
  initial: const Splash(),
  guards: [splashRedirectGuard(auth), authGuard(auth)],
  builder: _buildMainPage,
  modalBuilder: _buildModal,
  codec: appCodec,
  fallback: const [Splash()],
);

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  auth.addListener(() {
    if (!auth.value) _config.router.set(const [Login()]);
  });

  runApp(
    MaterialApp.router(
      title: 'Kaisel v0.4 example',
      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.indigo),
      routerConfig: _config,
    ),
  );
}

// Main router's pageBuilder: exhaustive over AppRoute (top-level
// routes + modal flow routes). It does NOT handle HomeRoute /
// DiscoverRoute / ProfileRoute. Those are each branch's concern.
Widget _buildMainPage(BuildContext context, AppRoute route) => switch (route) {
  Splash() => const _SplashScreen(),
  Login() => const _LoginScreen(),
  MainShell() => const _MainShellScreen(),
  Settings() => const _SettingsScreen(),
  CheckoutMount() => const KaiselModuleMount<CheckoutRoute>(
    module: CheckoutModule(),
  ),
  ConfirmAddToCart(:final productId) => _ConfirmAddToCartScreen(
    productId: productId,
  ),
  ConfirmAddToCartReview(:final productId, :final quantity) =>
    _ConfirmAddToCartReviewScreen(productId: productId, quantity: quantity),
};

Widget _buildModal(
  BuildContext context,
  KaiselModalRoute<Object?> route,
  Widget flowChild,
) {
  return Material(
    color: Colors.black54,
    child: Center(
      child: ConstrainedBox(
        constraints: const BoxConstraints(maxWidth: 420, maxHeight: 520),
        child: Padding(
          padding: const EdgeInsets.all(24),
          child: Material(
            clipBehavior: Clip.hardEdge,
            borderRadius: BorderRadius.circular(16),
            child: flowChild,
          ),
        ),
      ),
    ),
  );
}

// 6. Screens

class _SplashScreen extends StatefulWidget {
  const _SplashScreen();
  @override
  State<_SplashScreen> createState() => _SplashScreenState();
}

class _SplashScreenState extends State<_SplashScreen> {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _config.router.replaceTop(const Splash());
    });
  }

  @override
  Widget build(BuildContext context) =>
      const Scaffold(body: Center(child: CircularProgressIndicator()));
}

class _LoginScreen extends StatelessWidget {
  const _LoginScreen();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Sign in')),
      body: Center(
        child: FilledButton(
          onPressed: () {
            auth.logIn();
            _config.router.set(const [MainShell()]);
          },
          child: const Text('Log in'),
        ),
      ),
    );
  }
}

/// Creates branch routers and the shell aggregator, keeps them alive
/// for the lifetime of the main shell, and renders them with
/// `KaiselBranchedShell`.
class _MainShellScreen extends StatefulWidget {
  const _MainShellScreen();
  @override
  State<_MainShellScreen> createState() => _MainShellScreenState();
}

class _MainShellScreenState extends State<_MainShellScreen> {
  late final KaiselRouter<HomeRoute> _homeRouter = KaiselRouter<HomeRoute>(
    initial: const HomeRoot(),
  );
  late final KaiselRouter<DiscoverRoute> _discoverRouter =
      KaiselRouter<DiscoverRoute>(initial: const DiscoverRoot());
  late final KaiselRouter<ProfileRoute> _profileRouter =
      KaiselRouter<ProfileRoute>(initial: const ProfileRoot());
  late final BranchedShellRouter _shell = BranchedShellRouter(
    branches: [_homeRouter, _discoverRouter, _profileRouter],
  );

  @override
  void dispose() {
    _shell.dispose();
    _homeRouter.dispose();
    _discoverRouter.dispose();
    _profileRouter.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return KaiselBranchedShell(
      shell: _shell,
      branches: [
        KaiselBranch<HomeRoute>(
          router: _homeRouter,
          pageBuilder: (context, route) => switch (route) {
            HomeRoot() => const _HomeScreen(),
            ProductDetail(:final id) => _ProductDetailScreen(id: id),
          },
        ),
        KaiselBranch<DiscoverRoute>(
          router: _discoverRouter,
          pageBuilder: (context, route) => switch (route) {
            DiscoverRoot() => const _DiscoverScreen(),
            FeedItem(:final id) => _FeedItemScreen(id: id),
          },
        ),
        KaiselBranch<ProfileRoute>(
          router: _profileRouter,
          pageBuilder: (context, route) => switch (route) {
            ProfileRoot() => const _ProfileScreen(),
          },
        ),
      ],
      chromeBuilder: (context, active, branchContent, switchBranch) {
        return Scaffold(
          body: branchContent,
          bottomNavigationBar: NavigationBar(
            selectedIndex: active,
            onDestinationSelected: switchBranch,
            destinations: const [
              NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
              NavigationDestination(
                icon: Icon(Icons.explore),
                label: 'Discover',
              ),
              NavigationDestination(icon: Icon(Icons.person), label: 'Profile'),
            ],
          ),
        );
      },
    );
  }
}

class _HomeScreen extends StatelessWidget {
  const _HomeScreen();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      body: ListView(
        children: [
          for (final id in ['sku-1', 'sku-2', 'sku-42'])
            ListTile(
              title: Text('Product $id'),
              trailing: const Icon(Icons.chevron_right),
              onTap: () => context.router<HomeRoute>().push(ProductDetail(id)),
            ),
        ],
      ),
    );
  }
}

class _DiscoverScreen extends StatelessWidget {
  const _DiscoverScreen();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Discover')),
      body: ListView(
        children: [
          for (final id in ['post-1', 'post-2', 'post-3'])
            ListTile(
              title: Text('Feed item $id'),
              onTap: () => context.router<DiscoverRoute>().push(FeedItem(id)),
            ),
        ],
      ),
    );
  }
}

class _FeedItemScreen extends StatelessWidget {
  const _FeedItemScreen({required this.id});
  final String id;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Feed item $id')),
      body: Center(child: Text('Discover-only screen for $id')),
    );
  }
}

class _ProfileScreen extends StatelessWidget {
  const _ProfileScreen();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Profile')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            FilledButton(
              // Top-level navigation: push on the MAIN router
              // (context.router<AppRoute>()) so Settings appears OVER
              // the shell. URL becomes /settings.
              onPressed: () =>
                  context.router<AppRoute>().push(const Settings()),
              child: const Text('Settings'),
            ),
            const SizedBox(height: 12),
            FilledButton.tonal(
              // v0.6: pushing CheckoutMount on the main stack mounts
              // the Checkout module. URL becomes /checkout.
              onPressed: () =>
                  context.router<AppRoute>().push(const CheckoutMount()),
              child: const Text('Start checkout'),
            ),
            const SizedBox(height: 12),
            TextButton(
              onPressed: () => context.shell().switchTo(0),
              child: const Text('Go to Home tab'),
            ),
            const SizedBox(height: 12),
            TextButton(onPressed: auth.logOut, child: const Text('Log out')),
          ],
        ),
      ),
    );
  }
}

class _ProductDetailScreen extends StatelessWidget {
  const _ProductDetailScreen({required this.id});
  final String id;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Product $id')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Detail page for $id',
              style: Theme.of(context).textTheme.headlineSmall,
            ),
            const SizedBox(height: 24),
            FilledButton.icon(
              icon: const Icon(Icons.add_shopping_cart),
              label: const Text('Add to cart'),
              onPressed: () async {
                // Run a typed modal flow on the MAIN router.
                // Branch routers don't have a delegate to render
                // overlays, so flows always go through the main.
                final qty = await _config.router.run(ConfirmAddToCart(id));
                if (qty != null && context.mounted) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Added $qty × $id to cart.')),
                  );
                }
              },
            ),
          ],
        ),
      ),
    );
  }
}

class _SettingsScreen extends StatelessWidget {
  const _SettingsScreen();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Settings')),
      body: const Center(
        child: Text(
          'Top-level page reached at /settings. Back goes to the shell.',
        ),
      ),
    );
  }
}

// Modal flow screens (still AppRoute; flows are top-level)

class _ConfirmAddToCartScreen extends StatefulWidget {
  const _ConfirmAddToCartScreen({required this.productId});
  final String productId;
  @override
  State<_ConfirmAddToCartScreen> createState() =>
      _ConfirmAddToCartScreenState();
}

class _ConfirmAddToCartScreenState extends State<_ConfirmAddToCartScreen> {
  int _qty = 1;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Add ${widget.productId}'),
        leading: IconButton(
          icon: const Icon(Icons.close),
          onPressed: context.dismissFlow,
        ),
      ),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            const Text('How many?'),
            const SizedBox(height: 8),
            Row(
              children: [
                IconButton(
                  icon: const Icon(Icons.remove),
                  onPressed: _qty > 1 ? () => setState(() => _qty--) : null,
                ),
                Text('$_qty', style: Theme.of(context).textTheme.headlineSmall),
                IconButton(
                  icon: const Icon(Icons.add),
                  onPressed: () => setState(() => _qty++),
                ),
              ],
            ),
            const Spacer(),
            Align(
              alignment: Alignment.centerRight,
              child: FilledButton(
                onPressed: () {
                  // context.router<AppRoute>() inside the flow resolves
                  // to the flow's sub-router (not the main router).
                  context.router<AppRoute>().push(
                    ConfirmAddToCartReview(
                      productId: widget.productId,
                      quantity: _qty,
                    ),
                  );
                },
                child: const Text('Next'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class _ConfirmAddToCartReviewScreen extends StatelessWidget {
  const _ConfirmAddToCartReviewScreen({
    required this.productId,
    required this.quantity,
  });
  final String productId;
  final int quantity;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Review')),
      body: Padding(
        padding: const EdgeInsets.all(24),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Adding $quantity × $productId to your cart.'),
            const Spacer(),
            Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                TextButton(
                  onPressed: () => context.router<AppRoute>().pop(),
                  child: const Text('Back'),
                ),
                FilledButton(
                  onPressed: () => context.completeFlow<int>(quantity),
                  child: const Text('Confirm'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// Checkout module screens (typed to CheckoutRoute)
//
// These screens live inside the Checkout module's typed router.
// `context.router<CheckoutRoute>()` resolves to the module's router;
// `context.router<AppRoute>()` resolves to the host's main router.
// same lookup-by-exact-type semantics as branched shells.

class _CheckoutCartScreen extends StatelessWidget {
  const _CheckoutCartScreen();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Cart')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('1 item in your cart'),
            const SizedBox(height: 24),
            FilledButton(
              onPressed: () => context.router<CheckoutRoute>().push(
                const CheckoutShipping(),
              ),
              child: const Text('Continue to shipping'),
            ),
            const SizedBox(height: 12),
            TextButton(
              // Exit the module entirely. Pops CheckoutMount off the
              // main stack.
              onPressed: () => context.router<AppRoute>().pop(),
              child: const Text('Cancel'),
            ),
          ],
        ),
      ),
    );
  }
}

class _CheckoutShippingScreen extends StatelessWidget {
  const _CheckoutShippingScreen();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Shipping')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Standard shipping selected'),
            const SizedBox(height: 24),
            FilledButton(
              onPressed: () =>
                  context.router<CheckoutRoute>().push(const CheckoutConfirm()),
              child: const Text('Review order'),
            ),
          ],
        ),
      ),
    );
  }
}

class _CheckoutConfirmScreen extends StatelessWidget {
  const _CheckoutConfirmScreen();
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Confirm')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Place order?'),
            const SizedBox(height: 24),
            FilledButton(
              onPressed: () {
                // Complete: pop the whole module off the main stack
                // and surface a confirmation. In a real SDK the
                // module would expose a completion callback or
                // return a Future to the host.
                context.router<AppRoute>().pop();
                ScaffoldMessenger.of(
                  context,
                ).showSnackBar(const SnackBar(content: Text('Order placed.')));
              },
              child: const Text('Place order'),
            ),
          ],
        ),
      ),
    );
  }
}
10
likes
160
points
850
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

A Dart 3-native Flutter router built on sealed routes, pattern matching, and a stack-as-state model. No string paths, no codegen.

Repository (GitHub)
View/report issues

Topics

#router #navigation #routing #deep-linking #go-router

License

Apache-2.0 (license)

Dependencies

flutter, kaisel_core, meta

More

Packages that depend on kaisel