Stateful navigation topic

Sometimes you want to have a separate navigation stack. For example:

  • A bottom bar with multiple pages, each with their own navigation stack.
  • A bottom sheet, or any other flow-like navigation, that allows users to go forward and backward.
  • Cases where you want to have a wrapper element around a set of routes

StatefulLocation is an interface that tries to simplify the creation of these patterns, while still being flexible. For simple flows, we provide a convenience class FlowLocation.

Below we provide examples of how to implement common use cases.

Bottom bar

class RootLocation extends StatefulLocation {
  @override
  String get path => 'root';

  @override
  List<Location> get children => [
        const Child1Location(),
        const Child2Location(),
      ];

  /// Note: here, we have implemented the childBuilder in place. We of
  /// course recommend making this its own class.
  @override
  StatefulLocationBuilder get childBuilder => (c, shell) => Scaffold(
        body: shell,
        bottomNavigationBar: BottomNavigationBar(
          items: const [
            BottomNavigationBarItem(
              icon: Icon(Icons.home),
              label: 'Page 1',
            ),
            BottomNavigationBarItem(
              icon: Icon(Icons.settings),
              label: 'Page 2',
            ),
          ],
          onTap: (value) => shell.switchChild(value),
        ),
      );
}

class Child1Location extends Location {
  const Child1Location();

  @override
  String get path => 'child1';

  @override
  LocationBuilder get builder => (context) => const Page1Screen();
}

class Child2Location extends Location {
  const Child2Location();

  @override
  String get path => 'child2';

  @override
  LocationBuilder get builder => (context) => const Page2Screen();
}

That's it. Then, when navigating you have two options:

// Navigate while still showing the bottom bar, i.e. inside the child navigator
DuckRouter.of(context).navigate(to: const DetailLocation());

// Navigate while not showing the bottom bar, i.e. on root navigator
DuckRouter.of(context).navigate(to: const DetailLocation(), root: true);

Bottom sheet

This example shows how one might implement a bottom sheet containing a login/registration flow. It uses the modal_bottom_sheet package to accomplish that. To achieve cases like these, DuckRouter provides the convenience class FlowLocation, a wrapper around StatefulLocation. This example combines usage of a FlowLocation with custom pages, see also Custom Pages.

class SheetPage<T> extends DuckPage<T> {
  const SheetPage({
    required this.builder,
  }) : super.custom();

  final WidgetBuilder builder;

  @override
  Route<T> createRoute(BuildContext context, RouteSettings? settings) {
    return modalSheetBuilder(context, builder, settings);
  }
}

class BottomSheetContainer extends StatelessWidget {
  final Widget child;
  final Radius topRadius;

  const BottomSheetContainer({
    super.key,
    required this.child,
    required this.topRadius,
  });

  @override
  Widget build(BuildContext context) {
    final theme = DesignSystem.of(context);

    final topSafeAreaPadding = MediaQuery.of(context).padding.top;
    final topPadding = topSafeAreaPadding + theme.spacing.s800;

    const shadow = BoxShadow(
      blurRadius: 30,
      color: Colors.black26,
      spreadRadius: 15,
    );

    return Padding(
      padding: EdgeInsets.only(top: topPadding),
      child: Container(
        decoration: BoxDecoration(
          color: theme.colors.surfaceDefaultDefault,
          boxShadow: const [shadow],
          borderRadius: BorderRadius.only(
            topLeft: topRadius,
            topRight: topRadius,
          ),
        ),
        width: double.infinity,
        child: ClipRRect(
          borderRadius: BorderRadius.vertical(top: topRadius),
          child: MediaQuery.removePadding(
            context: context,
            removeTop: true,
            child: child,
          ),
        ),
      ),
    );
  }
}

Route<T> modalSheetBuilder<T>(
    BuildContext context, WidgetBuilder builder, RouteSettings? settings) {
  final theme = DesignSystem.of(context);

  return CupertinoModalBottomSheetRoute(
    settings: settings,
    builder: builder,
    expanded: false,
    enableDrag: true,
    containerBuilder: (context, animation, child) => BottomSheetContainer(
      topRadius: Radius.circular(theme.spacing.s400),
      child: child,
    ),
  );
}

Your actual flow would then look like this:


class LoginFlowLocation extends FlowLocation {
  @override
  String get path => 'login-flow';

  @override
  Location get start => EmailLocation();

  @override
  StatefulLocationPageBuilder? get containerBuilder => (context, builder) => SheetPage(
        builder: builder,
      );
}

You would start the flow by navigating to it:

DuckRouter.of(context).navigate(to: const LoginFlowLocation());
// Within the flow, you can navigate back and forth like so:
DuckRouter.of(context).navigate(to: const PasswordLocation());
// Or go all the way back to the root:
DuckRouter.of(context).root();
// Or close the sheet entirely:
DuckRouter.of(context).exit();

Classes

DuckRouter Configuration Stateful navigation Deep linking Custom pages and transitions
Creates a DuckRouter.
DuckShell Stateful navigation
The DuckShell is a Widget that manages state for a StatefulLocation. It allows switching between the children of that location.