Flutter Modular Documentation

repository·master·Indexed 23 days ago

https://github.com/flutterando/modular

A framework for building scalable Flutter applications through modularized route management and dependency injection. It enables a 'Smart Structure' by organizing code into modules, allowing for isolated routes and dependencies. Key features include app-scoped and page-scoped state management, reactive dependency access via context.watch and context.read, and a flexible DI container supporting singletons and lazy instances.

Tokens
32.3K
Snippets
98
Records
141
Agent score
79%

What's inside Flutter Modular

  1. What is Flutter Modular?

    master

    Flutter Modular is a framework designed to implement a 'Smart Structure' in Flutter applications by modularizing two core concerns: Routes and Dependency Injection (DI).

    Instead of a monolithic architecture where all routes and dependencies are global, Modular allows you to group them into Modules. This approach improves scalability, maintainability, and feature isolation by ensuring each scope (feature) has its own independent routes and injections.

  2. What is a Module and how to create one

    master

    A Module is the core building block of Modular, responsible for declaring a scope's Dependency Injection (DI) and its routes. You create a module using the createModule function.

    Inside the register callback, you use the module configuration object to define routes. Routes can include provide to declare page-scoped state (dependencies that are built when the page mounts and disposed when it leaves).

    Important: Modules are deduplicated by identity. Always store your modules in a top-level final variable and reference that same instance to avoid duplicate registration.

    import 'package:flutter_modular/flutter_modular.dart';
    
    final appModule = createModule(
      register: (c) {
        c.route(
          '/',
          provide: (s) => s.addChangeNotifier<CounterViewModel>(CounterViewModel.new),
          child: (context, state) => const CounterPage(),
        );
      },
    );
  3. What is a Module and its lifecycle

    master

    A Module clusters routes and binds relative to a specific scope or feature of the application. It can contain sub-modules to form a single composition.

    Key behaviors:

    • Visibility: To access a bind, it must be in a parent module that is already started; otherwise, the bind will not be visible via system injection.
    • Lifetime: A Module's lifetime ends when the last page associated with it is closed.
  4. What is Modular and how does it structure an app?

    master

    Modular is a framework for Flutter that provides structure by turning Dependency Injection (DI) and Routes into self-contained Modules.

    Instead of a monolithic MVC where all models, controllers, and views are grouped in global folders, Modular encourages a Smart Structure where the app is divided by scope. Each feature (or Module) owns its own MVC (Model-View-Controller) triad. This approach improves feature isolation, reduces breaking changes, and makes developer turnover easier.

    Key architectural principles:

    • Modules combine DI + Routes: A module with a path represents a feature; a module without a path provides shared DI.
    • Page-scoped state: State (view models) declared via provide is built when a page mounts and automatically dispose()d when the page leaves the stack.
    • Single Source of Truth: Durable data should live in root-owned singletons (repositories/services) registered in DI. View models should only be disposable projections of this truth.
    • Web-like Routing: Navigation uses paths and stacks. Routes can be relative to the current location, supporting dynamic :params, query strings, and nested routes.
  5. Difference between module flattening and persistent UI shells

    master

    When you use c.module(...), Modular flattens the sub-module's routes under its path. There is no automatic 'shell' or wrapper UI created around the sub-module.

    If you need persistent UI elements (like a bottom navigation bar or a sidebar) that remain visible while navigating between child routes, you must explicitly declare a RouterOutlet inside the children of a route.

  6. Navigate within a RouterOutlet: navigate() vs. pushNamed()

    master

    When working with a RouterOutlet, the navigation method you choose determines how the sub-stack behaves:

    • navigate(path): Replaces the current outlet sub-stack. This is used for tab switching (e.g., in a bottom navigation bar) where you don't want a history to pop back through.
    • pushNamed(path): Stacks a new page inside the existing outlet. The shell remains visible, and popping the page returns the user to the previous state within that same outlet.

    Usage Contexts:

    • From a sibling of the outlet (e.g., a Shell's BottomBar): Use a GlobalKey<RouterOutletState> to call _outlet.currentState?.navigate(path).
    • From a widget inside the outlet: Use context.pushNamed(path) directly.
  7. Migrate State Management to v7

    master

    For managing state in v7, follow these patterns:

    • Page-local state: Use the route's provide mechanism. This ensures the state is built and disposed automatically with the page lifecycle.
    • App-global state (e.g., theme, session): Use ModularApp.provide.
    • Durable Singletons: Keep the actual source of truth as a root-owned singleton in Dependency Injection (DI).
  8. How module `path` affects scope and lifecycle

    master

    In Modular v7, the presence of a path determines the module's role and lifecycle:

    Feature Modules (with a path)

    When a module has a path (e.g., path: '/products'), it is considered a feature.

    • Route Flattening: Its routes are flattened under that path prefix.
    • Feature-scoped DI: Dependencies are bound when the first route of the module enters the stack and are disposed when the last route leaves.
    • Path Constraints: The path must be a static prefix starting with / and cannot contain dynamic segments like :params.

    Shared DI Modules (without a path)

    When a module has no path, it is used for shared DI.

    • Root-owned: Dependencies are bound eagerly and live for the entire lifetime of the app.
    • Use Case: This is where your Single Source of Truth (e.g., AppSession, ProductRepository) should reside.
    // Feature Module Example
    final productsModule = createModule(
      path: '/products',
      register: (c) {
        c
          ..route('/', child: (ctx, state) => const ProductListPage())
          ..route('/:id', child: (ctx, state) => ProductDetailPage(id: state['id']!));
      },
    );
    
    // Shared DI Module Example
    final coreModule = createModule(
      register: (c) {
        c
          ..addSingleton<ProductService>(ProductService.new)
          ..addSingleton<ProductRepository>(ProductRepository.new)
          ..addSingleton<AppSession>(AppSession.new);
      },
    );
  9. How RouterOutlet affects the URL

    master

    The RouterOutlet synchronizes its state with the browser/app URL:

    • Tab Switching (navigate): Changes the URL (e.g., /dashboard $\rightarrow$ /dashboard/search) because the outlet reports its base sub-route to the root delegate.
    • Stacking (pushNamed): Does not change the base URL. A page pushed inside the outlet stays out of the URL, consistent with the stack-base URL model.
  10. Use RouterOutlet for nested navigation

    master

    A RouterOutlet allows for nested navigation within a specific part of the widget tree. A ChildRoute can define children routes, and you must place a RouterOutlet() widget in the parent route's UI to render those children. Note that RouterOutlet provides nested navigation and does not support page caching.

    // Module definition with children
    @override
    void routes(r) {
      r.child('/', child: (context) => HomePage(), children: [
        ChildRoute('/page1', child: (context) => InternalPage(title: 'page 1', color: Colors.red)),
        ChildRoute('/page2', child: (context) => InternalPage(title: 'page 2', color: Colors.amber)),
      ]);
    }
    
    // In HomePage widget
    Expanded(child: RouterOutlet()),
  11. Understand the difference between App-scoped and Page-scoped state

    master

    Modular allows you to manage state at different lifecycle levels:

    1. App-scoped state: Provided via the provide parameter of ModularApp. This state lives above the MaterialApp and is accessible by all routes. It is suitable for global configurations like ThemeMode.
    2. Page-scoped state: Provided via the provide parameter of a c.route definition within a module. This state is tied to the lifecycle of the route; it is created when the page is mounted and automatically disposed of when the user navigates away from that route.
  12. Understand Bind Lifecycles (Root-owned vs Feature-scoped)

    master

    The lifecycle of a dependency is determined by where it is registered in the module hierarchy.

    Root-owned (Shared DI)

    Dependencies registered in a path-less module are committed eagerly at bootstrap and live for the entire duration of the app. These are suitable for "sources of truth" like repositories, services, or app sessions. They are never disposed when navigating.

    final coreModule = createModule(            // no path → root-owned
      register: (c) => c.addSingleton<ProductRepository>(ProductRepository.new),
    );

    Feature-scoped

    Dependencies registered in a module with a path are bound lazily when the feature's first route enters the stack and are automatically disposed when the last route leaves.

    Disposal is automatic for ChangeNotifiers and classes implementing Disposable (their dispose() method is called).

    final productsModule = createModule(
      path: '/products',                        // feature → binds disposed when it leaves
      register: (c) {
        c.add<ProductSearchController>(ProductSearchController.new);
        // ...routes...
      },
    );

    Decision Guide

    • Source of Truth (Repositories, Services, Sessions): Use a root-owned module.
    • Feature-local machinery: Use a feature-scoped module.
    • Page-specific state: Use page-scoped provide instead of a module bind.
    // Root-owned example
    final coreModule = createModule(            // no path → root-owned
      register: (c) => c.addSingleton<ProductRepository>(ProductRepository.new),
    );
    
    // Feature-scoped example
    final productsModule = createModule(
      path: '/products',                        // feature → binds disposed when it leaves
      register: (c) {
        c.add<ProductSearchController>(ProductSearchController.new);
        // ...routes...
      },
    );