Dart & Flutter Packages by dev-cetera.com & contributors.

pub tag license


Summary

A lightweight router designed for ease of use and efficient state management. Explore it in action with this live web app: https://dev-cetera.github.io/df_router/chat?chatId=123456.

Features

  • Declarative Routing: Define your routes and their corresponding widgets in a clean, list-based manner.
  • Stateful Routes: RouteState objects represent unique routes, including paths, query parameters, and strongly-typed extra data.
  • Widget Caching & Preservation: Control whether route widgets are preserved in memory (shouldPreserve) or pre-built (shouldPrebuild) for performance.
  • Customizable Transitions: Easily define custom page transitions called "effects" or use the provided MaterialEffect and CupertinoEffect.
  • Persistent UI Wrapper: Add common UI elements like headers, footers, or navigation bars that persist across route changes.
  • Easy Navigation: Navigate using RouteState objects or simple path strings.
  • Typed extra Data: Pass strongly-typed data between routes.

Usage

1. Define Your RouteStates

Create classes that extend RouteState for each distinct route in your application. These classes encapsulate the path and can manage query parameters.

final class HomeRouteState extends RouteState {
  HomeRouteState()
    : super.parse(
        '/home',
        // Use QuickForwardtEffect() as the default transtion effect for this
        // route. This can be overridden when pushing this route.
        animationEffect: const QuickForwardEffect(),
      );
}

// This route is only used in the RouteManager, so it does not need to
// be pushed directly. It is a base route for the chat feature.
final class BaseChatRouteState extends RouteState {
  BaseChatRouteState({Map<String, String>? queryParameters})
    : super.parse(
        '/chat',
        queryParameters: queryParameters,
        // Use a different animation effect for this route.
        animationEffect: const SlideDownEffect(),
      );

  BaseChatRouteState.from(RouteState other) : super(other.uri);
}

final class ChatRouteState extends BaseChatRouteState {
  final String chatId;

  ChatRouteState({required this.chatId}) : super(queryParameters: {'chatId': chatId});

  ChatRouteState.from(super.other)
    : chatId = other.uri.queryParameters['chatId'] ?? '',
      super.from();
}

2. Configure RouteStateManager

In your MaterialApp (or CupertinoApp), use the RouteStateManager widget to define your application's routing configuration.

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      //home: // Do not use "home", as it conflicts with RouteManager. Use
      // "builder" instead.
      builder: (context, child) {
        return RouteManager(
          fallbackRouteState: () => HomeRouteState(),
          builders: [
            RouteBuilder(
              routeState: HomeRouteState(),
              // Pre-build the HomeScreen even if the initial route is not
              // HomeRouteState. This is useful for performance optimization.
              shouldPrebuild: true,
              // Preserve the HomeScreen widget to avoid rebuilding it.
              shouldPreserve: true,
              builder: (context, routeState) => HomeScreen(routeState: HomeRouteState()),
            ),
            RouteBuilder(
              // Use the BaseChatRouteState instead of the ChatRouteState
              // since it does not require a chatId to be pushed.
              routeState: BaseChatRouteState(),
              builder:
                  (context, routeState) => ChatScreen(routeState: ChatRouteState.from(routeState)),
            ),
          ],
        );
      },
    );
  }
}

3. Create Your Screen Widgets

Your screen widgets should use the RouteWidgetMixin to easily access the current RouteState.

class HomeScreen extends StatelessWidget with RouteWidgetMixin {
  @override
  final HomeRouteState? routeState;
  const HomeScreen({super.key, this.routeState});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Home')),
      backgroundColor: Colors.green,
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            final controller = RouteController.of(context);
            controller.push(
              ChatRouteState(chatId: '123456'),
              // Override the default animation effect for this push.
              animationEffect: const CupertinoEffect(),
            );
          },
          child: const Text('Go to Chat'),
        ),
      ),
    );
  }
}

class ChatScreen extends StatelessWidget with RouteWidgetMixin {
  @override
  final ChatRouteState? routeState;

  const ChatScreen({super.key, this.routeState});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Chat - ${routeState?.chatId}')),
      backgroundColor: Colors.blue,
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () {
                final controller = RouteController.of(context);
                controller.pushBack();
              },
              child: const Text('Go Back - Default Effect'),
            ),
            ElevatedButton(
              onPressed: () {
                final controller = RouteController.of(context);
                controller.pushBack(animationEffect: const QuickBackEffect());
              },
              child: const Text('Go Back - Quick Back Effect'),
            ),
            ElevatedButton(
              onPressed: () {
                final controller = RouteController.of(context);
                controller.push(HomeRouteState());
              },
              child: const Text('Go Home - Default Effect'),
            ),
            ElevatedButton(
              onPressed: () {
                final controller = RouteController.of(context);
                controller.push(HomeRouteState().copyWith(animationEffect: const MaterialEffect()));
              },
              child: const Text('Go Home - Material Effect'),
            ),
            ElevatedButton(
              onPressed: () {
                final controller = RouteController.of(context);
                controller.push(HomeRouteState(), animationEffect: const PageFlapDown());
              },
              child: const Text('Go Home - Page Flap Down Effect'),
            ),
          ],
        ),
      ),
    );
  }
}

4. Navigation

Access the RouteStateController to navigate:

final controller = RouteStateController.of(context);

// Push the home screen with a material effect.
controller.push(HomeRouteState(), animationEffect: const CupertinoEffect());
controller.push(HomeRouteState().copyWith(animationEffect: const MaterialEffect()));

// Go back.
controller.pushBack();

// Remove a specific route to free up resources.
controller.removeStatesFromCache([HomeRouteState()]);

// Clear the entire widget cache.
controller.clearCache([HomeRouteState()]);

// Add a route to the cache without navigating. This will preload it.
controller.addToCache([HomeRouteState()]);

// Reset the cache to its initial state. This honours the shouldPrebuild property.
controller.resetState();

// Get the current state, e.g. HomeRouteState();
final current = controller.current;

// Get the requested state, which is what was typed in the URL bar, e.g. "/home?query=123" may return an instance of HomeRouteState(). This will return nul if the URL does not match any defined route.
final requested = controller.requested;

Please refer to the API reference for more information.


💬 Contributing and Discussions

This is an open-source project, and we warmly welcome contributions from everyone, regardless of experience level. Whether you're a seasoned developer or just starting out, contributing to this project is a fantastic way to learn, share your knowledge, and make a meaningful impact on the community.

☝️ Ways you can contribute

  • Buy me a coffee: If you'd like to support the project financially, consider buying me a coffee. Your support helps cover the costs of development and keeps the project growing.
  • Find us on Discord: Feel free to ask questions and engage with the community here: https://discord.gg/gEQ8y2nfyX.
  • Share your ideas: Every perspective matters, and your ideas can spark innovation.
  • Help others: Engage with other users by offering advice, solutions, or troubleshooting assistance.
  • Report bugs: Help us identify and fix issues to make the project more robust.
  • Suggest improvements or new features: Your ideas can help shape the future of the project.
  • Help clarify documentation: Good documentation is key to accessibility. You can make it easier for others to get started by improving or expanding our documentation.
  • Write articles: Share your knowledge by writing tutorials, guides, or blog posts about your experiences with the project. It's a great way to contribute and help others learn.

No matter how you choose to contribute, your involvement is greatly appreciated and valued!

☕ We drink a lot of coffee...

If you're enjoying this package and find it valuable, consider showing your appreciation with a small donation. Every bit helps in supporting future development. You can donate here: https://www.buymeacoffee.com/dev_cetera

License

This project is released under the MIT License. See LICENSE for more information.

Libraries

df_router
A lightweight router designed for ease of use and efficient state management.
main