juice_routing

Declarative, state-driven navigation for Juice applications with Navigator 2.0 integration, route guards, and deep linking.

pub package License: MIT

Features

  • Declarative Routes - Define routes as configuration, not scattered push() calls
  • Route Guards - Async guards for auth, permissions, onboarding with redirect loop protection
  • Navigation Atomicity - Navigation either commits fully or not at all
  • Observable State - Current route, stack, params, pending navigation all in RoutingState
  • Navigator 2.0 - Full integration with Flutter's declarative navigation API
  • Deep Linking - Same path resolution for cold start, warm start, and in-app navigation

Installation

dependencies:
  juice_routing: ^0.1.0

Quick Start

1. Define Your Routes

import 'package:juice_routing/juice_routing.dart';

final appRoutes = RoutingConfig(
  routes: [
    RouteConfig(
      path: '/',
      title: 'Home',
      builder: (ctx) => const HomeScreen(),
    ),
    RouteConfig(
      path: '/profile/:userId',
      title: 'Profile',
      builder: (ctx) => ProfileScreen(userId: ctx.params['userId']!),
      guards: [AuthGuard()],
    ),
    RouteConfig(
      path: '/settings',
      title: 'Settings',
      builder: (ctx) => const SettingsScreen(),
      guards: [AuthGuard()],
      children: [
        RouteConfig(
          path: 'account',
          builder: (ctx) => const AccountSettingsScreen(),
        ),
        RouteConfig(
          path: 'privacy',
          builder: (ctx) => const PrivacySettingsScreen(),
        ),
      ],
    ),
  ],
  notFoundRoute: RouteConfig(
    path: '/404',
    builder: (ctx) => const NotFoundScreen(),
  ),
);

2. Create a Route Guard

class AuthGuard extends RouteGuard {
  @override
  String get name => 'AuthGuard';

  @override
  Future<GuardResult> check(RouteContext context) async {
    final authBloc = BlocScope.get<AuthBloc>();

    if (authBloc.state.isLoggedIn) {
      return const GuardResult.allow();
    }

    return GuardResult.redirect('/login', returnTo: context.targetPath);
  }
}

3. Initialize and Use

import 'package:flutter/material.dart';
import 'package:juice/juice.dart';
import 'package:juice_routing/juice_routing.dart';

void main() {
  // Register RoutingBloc
  BlocScope.register<RoutingBloc>(
    () => RoutingBloc(),
    lifecycle: BlocLifecycle.permanent,
  );

  // Initialize with config
  final routingBloc = BlocScope.get<RoutingBloc>();
  routingBloc.send(InitializeRoutingEvent(config: appRoutes));

  runApp(MyApp(routingBloc: routingBloc));
}

class MyApp extends StatelessWidget {
  final RoutingBloc routingBloc;

  const MyApp({required this.routingBloc});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: JuiceRouterDelegate(routingBloc: routingBloc),
      routeInformationParser: const JuiceRouteInformationParser(),
    );
  }
}

4. Navigate

final routingBloc = BlocScope.get<RoutingBloc>();

// Push a route
routingBloc.navigate('/profile/123');

// Replace current route
routingBloc.navigate('/home', replace: true);

// Pop
routingBloc.pop();

// Pop to root
routingBloc.popToRoot();

// Reset stack
routingBloc.resetStack('/login');

Route Guards

Guards run automatically before navigation commits:

Result Behavior
GuardResult.allow() Navigation proceeds
GuardResult.redirect('/path') Redirects to another route
GuardResult.block('reason') Blocks navigation, stays on current route

Guards support:

  • Priority ordering - Lower priority runs first
  • Async operations - Token refresh, permission checks
  • Redirect loop protection - Max 5 redirects before error
Method Guards Run? Description
navigate(path) Yes Push new route
navigate(path, replace: true) Yes Replace current route
pop() No Go back one route
popToRoot() No Clear stack to root
popUntil(predicate) No Pop until condition met
resetStack(path) Yes Clear and start fresh

Observable State

final state = routingBloc.state;

state.currentPath      // Current route path
state.stack            // Full navigation stack
state.stackDepth       // Number of routes in stack
state.canPop           // Whether pop is possible
state.isNavigating     // Guards currently running
state.history          // Navigation history
state.error            // Last navigation error

Rebuild Groups

Subscribe to specific state changes:

Group Updates When
routing.stack Stack changes (push, pop, replace)
routing.current Current route changes
routing.pending Navigation in progress
routing.history History entry added
routing.error Navigation error occurred

Contract Guarantees

Guarantee Behavior
Atomicity Navigation commits fully or not at all
Concurrency One pending navigation; new ones queue (latest wins)
Redirect cap Max 5 redirects before RedirectLoopError
Guard errors Exception becomes GuardExceptionError, navigation aborted
Pop behavior Pop events bypass guards, execute immediately

Documentation

Example App

See the example directory for a complete demo app showcasing:

  • Route configuration with guards
  • Navigation playground for testing all navigation types
  • Aviator pattern for loose coupling
  • History tracking and visualization

License

MIT License - see LICENSE for details.

Libraries

juice_routing
Declarative, state-driven navigation for Juice applications.