CI & Coverage pub package

All Contributors

Flutter Modular

flutter_modular

What is Flutter Modular?

As an application project grows and becomes complex, it's hard to keep your code and project structure maintainable and reusable. Modular provides a bunch of Flutter-suiting solutions to deal with this problem, like dependency injection, routing system and the "disposable singleton" system (that is, Modular disposes the injected module automatically when it goes out of scope).

Modular's dependency injection system has out-of-the-box support for any state management system, managing your application memory usage.

Modular also supports Dynamic and Relative Routing like on the Web.

Modular Structure

Modular structure consists in decoupled and independent modules that will represent the features of the application. Each module is located in its own directory, and controls its own dependencies, routes, pages, widgets and business logic. Consequently, you can easily detach one module from your project and use it wherever you want.

Modular Pillars

These are the main aspects that Modular focus on:

  • Automatic Memory Management.
  • Dependency Injection.
  • Dynamic and Relative Routing.
  • Code Modularization.

Getting started with Modular

Migration Guide: Modular 2.0 to 3.0

Guide link here!

Installation

Open your project's pubspec.yaml and add flutter_modular as a dependency:

dependencies:
  flutter_modular: any

Using in a new project

To use Modular in a new project, you will have to make some initial setup:

  1. Create your main widget with a MaterialApp and call the MaterialApp().modular() method.
//  app_widget.dart
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';

class AppWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: "/",
    ).modular();
  }
}
  1. Create your project module file extending Module:
// app_module.dart
class AppModule extends Module {

  // Provide a list of dependencies to inject into your project
  @override
  final List<Bind> binds = [];

  // Provide all the routes for your module
  @override
  final List<ModularRoute> routes = [];

}
  1. In main.dart file, wrap the main module in ModularApp to initialize it with Modular:
// main.dart
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';

import 'app/app_module.dart';

void main() => runApp(ModularApp(module: AppModule(), child: AppWidget()));
  1. Done! Your app is set and ready to work with Modular!

Creating child modules

You can create as many modules in your project as you wish:

class HomeModule extends Module {
  @override
  final List<Bind> binds = [
    Bind.singleton((i) => HomeBloc()),
  ];

  @override
  final List<ModularRoute> routes = [
    ChildRoute('/', child: (_, args) => HomeWidget()),
    ChildRoute('/list', child: (_, args) => ListWidget()),
  ];

}

You may then pass the submodule to a Route in your main module through the module parameter:

class AppModule extends Module {

  @override
  final List<ModularRoute> routes = [
    ModuleRoute('/home', module: HomeModule()),
  ];
}

We recommend that you split your code in various modules, such as AuthModule, and place all the routes related to this module within it. By doing so, it will be much easier to maintain and share your code with other projects.

NOTE: Use the ModuleRoute object to create a complex Route.

Adding routes

The module's routes are provided by overriding the routes property.

// app_module.dart
class AppModule extends Module {

  // Provide a list of dependencies to inject into your project
  @override
  final List<Bind> binds = [];

  // Provide all the routes for your module
  @override
  final List<ModularRoute> routes = [
      // Simple route using the ChildRoute
      ChildRoute('/', child: (_, __) => HomePage()),
      ChildRoute('/login', child: (_, __) => LoginPage()),
  ];
}

NOTE: Use the ChildRoute object to create a simple Route.

Dynamic routes

You can use dynamic routing system to provide parameters to your Route:

// Use :parameter_name syntax to provide a parameter in your route.
// Route arguments will be available through `args`, and may be accessed in `params` property,
// using square brackets notation (['parameter_name']).

@override
final List<ModularRoute> routes = [
  ChildRoute(
    '/product/:id',
    child: (_, args) => Product(id: args.params['id']),
  ),
];

The parameters will be pattern-matched when calling the given route. For example:

// In this case, `args.params['id']` will have the value `1`.
Modular.to.pushNamed('/product/1');

You can use it with more than one page too. For example:

@override
final List<ModularRoute> routes = [
  // We are sending an ID to the DetailPage
  ChildRoute(
    '/product/:id/detail',
    child: (_, args) => DetailPage(id: args.params['id']),
  ),
  // We are sending an ID to the RatingPage
  ChildRoute(
    '/product/:id/rating',
    child: (_, args) => RatingPage(id: args.params['id']),
  ),
];

The same as in the first example, we just need to call the route. For example:

// In this case, modular will open the page DetailPage with the id of the product equals 1
Modular.to.navigate('/product/1/detail');
// We can use the pushNamed too

// The same here, but with RatingPage
Modular.to.navigate('/product/1/rating');

This notation, however, is only valid for simple literals.

Sending Objects

If you want to pass a complex object to your route, provide it in the arguments parameter:

Modular.to.navigate('/product', arguments: ProductModel());

And it will be available in the args.data property instead of args.params:

@override
List<ModularRoute> get routes => [
  ChildRoute(
    '/product',
    child: (_, args) => Product(model: args.data),
  ),
];

You can also retrieve the arguments directly within binds:


@override
List<Bind> get binds => [
  Bind.singleton((i) => MyController(data: i.args.data)),
];

Route generic types

You can return values from navigation, just like with Navigator.pop. To achieve this, pass the type you expect to return as a type parameter to the Route:

@override
final List<ModularRoute> routes = [
  // This router expects to receive a `String` when popped.
  ChildRoute<String>('/event', child: (_, __) => EventPage()),
]

Now, use Modular's .pop as you would use Navigator.pop:

// Push route
String name = await Modular.to.pushNamed<String>();

// And pass the value when popping
Modular.to.pop('banana');

Route guard

Route guards are middleware-like objects that allow you to control the access of a given route from another route. You can implement a route guard by making a class that implements RouteGuard.

For example, the following class will only allow a redirection from /admin route:

class MyGuard implements RouteGuard {
  @override
  Future<bool> canActivate(String url, ModularRoute route) {
    if (url != '/admin'){
      // Return `true` to allow access
      return Future.value(true);
    } else {
      // Return `false` to disallow access
      return Future.value(false);
    }
  }
}

To use your RouteGuard in a route, pass it to the guards parameter:

@override
final List<ModularRoute> routes = [
  final ModuleRoute('/', module: HomeModule()),
  final ModuleRoute(
    '/admin',
    module: AdminModule(),
    guards: [MyGuard()],
  ),
];

If placed on a module route, RouteGuard will be global to that route.

Add a fallback route to be used if RouteGuard validation fails by adding the guardedRoute property:

@override
final List<ModularRoute> routes = [
    ChildRoute(
      '/home',
      child: (context, args) => HomePage(),
      guards: [AuthGuard()],
      guardedRoute: '/login',
    ),
    ChildRoute(
      '/login',
      child: (context, args) => LoginPage(),
    ),
];

When and How to use navigate or pushNamed

You can use both in your application but need to understand each one.

pushNamed

This method places the desired route above the current route whenever used, and you can go back to the previous page using the back button that you can see on the AppBar. It's like a modal and it's better suited for Mobile Applications.

Imagine that you need to go deeper in your routes, for example:

// Initial route
Modular.to.pushNamed('/home');
// User route
Modular.to.pushNamed('/home/user');
// User profile route
Modular.to.pushNamed('/home/user/profile');

In the end, you can see the back button to go back to the previous page, reinforcing the idea of a modal page opening on top of the previous page.

This one removes all the routes previously in the stack and puts the new route on this stack. Because of this, you'll not see the back button in the AppBar in this case. This is better suited for Web Applications.

Imagine that you need to make a logout feature for your Mobile Application. As such, you need to clean all routes from the stack.

// Initial route
Modular.to.pushNamed('/home');
// User route
Modular.to.pushNamed('/home/user');
// User profile route
Modular.to.pushNamed('/home/user/profile');

// Then you need to go again to the Login page, only use the navigation to clean all the stack.
Modular.to.navigate('/login');

Relative Navigation

To navigate between pages, use Modular.to.navigate.

Modular.to.navigate('/login');

You can use Relative Navigation to navigate like in the web

// Modules Home → Product
Modular.to.navigate('/home/product/list');
Modular.to.navigate('/home/product/detail/3');

// Relative Navigation inside /home/product/list
Modular.to.navigate('detail/3'); // it's the same as /home/product/detail/3
Modular.to.navigate('../config'); // it's the same as /home/config

You can still stack pages using the old Navigator API.

Navigator.pushNamed(context, '/login');

Alternatively, you can use Modular.to.pushNamed, to which you don't have to provide a BuildContext:

Modular.to.pushNamed('/login');

The routing system can recognize what is in the URL and navigate to a specific part of the application. Dynamic routes apply here as well. The following URL, for instance, will open the Product view, with args.params['id'] set to 1.

https://flutter-website.com/#/product/1

It can deal with query parameters or fragments as well:

https://flutter-website.com/#/product?id=1

Route transition animation

You can choose which type of animation you want to be used on your page transition by setting the Route's transition parameter, providing a TransitionType.

ModuleRoute('/product',
  module: AdminModule(),
  transition: TransitionType.fadeIn,
), //use for change transition

If you specify a transition in a module, all routes within that module will inherit this transition animation.

Custom transition animation route

You can also use a custom transition animation by setting the Router's transition and customTransition parameters respectively to TransitionType.custom and your CustomTransition instance:

ModuleRoute('/product',
  module: AdminModule(),
  transition: TransitionType.custom,
  customTransition: myCustomTransition,
),

For example, this is a custom transition that could be declared in a separated file and used in the customTransition parameter:

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

CustomTransition get myCustomTransition => CustomTransition(
    transitionDuration: Duration(milliseconds: 500),
    transitionBuilder: (context, animation, secondaryAnimation, child){
      return RotationTransition(turns: animation,
        child: SlideTransition(
          position: Tween<Offset>(
            begin: const Offset(-1.0, 0.0),
            end: Offset.zero,
          ).animate(animation),
          child: ScaleTransition(
            scale: Tween<double>(
              begin: 0.0,
              end: 1.0,
            ).animate(CurvedAnimation(
              parent: animation,
              curve: Interval(
                0.00,
                0.50,
                curve: Curves.linear,
              ),
            ),
            ),
            child: child,
          ),
        ),
      )
      ;
    },
  );

Dependency Injection

You can inject any class into your module by overriding the binds getter of your module. Typical examples to inject are BLoCs, ChangeNotifier instances or stores (MobX).

A Bind object is responsible for configuring the object injection. We have 4 Bind factory types and one AsyncBind.

class AppModule extends Module {

  // Provide a list of dependencies to inject into your project
  @override
  List<Bind> get binds => [
    Bind((i) => AppBloc()),
    Bind.factory((i) => AppBloc()),
    Bind.instance(myObject),
    Bind.singleton((i) => AppBloc()),
    Bind.lazySingleton((i) => AppBloc()),
    AsyncBind((i) => SharedPreferences.getInstance())
  ];
...
}

Factory

Instantiate the class whenever it gets called.

  @override
  List<Bind> get binds => [
    Bind.factory((i) => AppBloc()),
  ];

Instance

Use an object that has already been instantiated.

  @override
  List<Bind> get binds => [
    Bind.instance((i) => AppBloc()),
  ];

Singleton

Create a Global instance of a class.

  @override
  List<Bind> get binds => [
    Bind.singleton((i) => AppBloc()),
  ];

LazySingleton

Create a Global instance of a class only when it gets called for the first time.

@override
  List<Bind> get binds => [
    Bind.lazySingleton((i) => AppBloc()),
  ];

AsyncBind

Some methods from several classes return a Future. To inject instances returned by those specific methods you should use AsyncBind instead a normal sync bind. Use Modular.isModuleReady<Module>() to wait for all AsyncBinds to resolve in order to release the module for use.

IMPORTANT: The order of AsyncBind matters if there are interdependencies of other asynchronous binds. For example, if there are two AsyncBinds where A depends on B, AsyncBind B must be declared before A. Pay attention to this type of order!

import 'package:flutter_modular/flutter_modular.dart' show Disposable;

// In Modular, `Disposable` classes are automatically disposed when out of the module scope.

class AppBloc extends Disposable {
  final controller = StreamController();

  @override
  void dispose() {
    controller.close();
  }
}

isModuleReady

If you want to ensure that all AsyncBinds are resolved before a Module is loaded into memory, isModuleReady is the way to go. One way to use it is with RouteGuard, adding an AsyncBind into your AppModule, and a RouteGuard to your ModuleRoute.

class AppModule extends Module {
  @override
  List<Bind> get binds => [
    AsyncBind((i)=> SharedPreferences.getInstance()),
  ];

  @override
  List<ModularRoute> get routes => [
    ModuleRoute(Modular.initialRoute, module: HomeModule(), guards: [HomeGuard()]),
  ];
}

Then, create a RouteGuard like follows. This way Modular will evaluate all your async dependencies before going to HomeModule.

import 'package:flutter_modular/flutter_modular.dart';

class HomeGuard extends RouteGuard {
  @override
  Future<bool> canActivate(String path, ModularRoute router) async {
    await Modular.isModuleReady<AppModule>();
    return true;
  }
}

Retrieving your injected dependencies in the view

Let's assume the following BLoC has been defined and injected into our module (as in the previous example):

import 'package:flutter_modular/flutter_modular.dart' show Disposable;

// In Modular, `Disposable` classes are automatically disposed when out of the module scope.

class AppBloc extends Disposable {
  final controller = StreamController();

  @override
  void dispose() {
    controller.close();
  }
}

NOTE: Modular automatically calls destruction methods for Binds of the types: Sink/Stream, ChangeNotifier and Store/Triple

There are several ways to retrieve our injected AppBloc.

class HomePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {

    // You can use the object Inject to retrieve..

    final appBloc = Modular.get<AppBloc>();
    //or for no-ready AsyncBinds
    final share = Modular.getAsync<SharedPreferences>();
  }
}

Using Modular widgets to retrieve your instances

ModularState

In this example, we'll use the following MyWidget as our page because it's a page that needs to be a StatefulWidget.

Let's understand the usage of ModularState. When we define class _MyWidgetState extends ModularState<MyWidget, HomeStore> we are linking Modular with our Store for this widget (in this case the HomeStore). When we enter this page, the HomeStore will be created and the store/controller variable will be provided to us to be used inside MyWidget.

After this, we can use store/controller without any problems. Modular will auto dispose of the HomeStore after we close the page.

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends ModularState<MyWidget, HomeStore> {
  store.myVariableInsideStore = 'Hello!';
  controller.myVariableInsideStore = 'Hello!';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Modular"),
      ),
      body: Center(child: Text("${store.counter}"),),
    );
  }
}

WidgetModule

WidgetModule has the same structure as Module. It is very useful if you want to have a TabBar with Modular pages.

class TabModule extends WidgetModule {

  @override
  List<Bind> binds => [
    Bind((i) => TabBloc(repository: i())),
    Bind((i) => TabRepository()),
  ];

  final Widget view = TabPage();

}

Mock the navigation system

We though it would be interesting to provide a native way to mock the navigation system when used with Modular.to and Modular.link. To do this, you can just implement IModularNavigator and pass your implementation to Modular.navigatorDelegate.

Example using Mockito:

main() {
    var navigatorMock = MyNavigatorMock();

    // Modular.to and Modular.link will be called MyNavigatorMock implements!
    Modular.navigatorDelegate = navigatorMock;

    test('test navigator mock', () async {
        when(navigatorMock.pushNamed('/test')).thenAnswer((_) async => {});

        Modular.to.pushNamed('/test');
        verify(navigatorMock.pushNamed('/test')).called(1);
    });
}

class MyNavigatorMock extends Mock implements IModularNavigator {
  @override
  Future<T?> pushNamed<T extends Object?>(String? routeName, {Object? arguments, bool? forRoot = false}) =>
      (super.noSuchMethod(Invocation.method(#pushNamed, [routeName], {#arguments: arguments, #forRoot: forRoot}), returnValue: Future.value(null)) as Future<T?>);
}

This example uses a manual implementation, but you can also use the code generator to create your mocks.

RouterOutlet

Each ModularRoute can have a list of ModularRoutes, so that it can be displayed within the parent ModularRoute. The widget that reflects these internal routes is called RouterOutlet. You can only have one RouterOutlet per page and it is only able to browse the children of that page.


  class StartModule extends Module {
      @override
      List<Bind> get binds => [];

      @override
      List<ModularRoute> get routes => [
        ChildRoute(
          '/start',
          child: (context, args) => StartPage(),
          children: [
            ChildRoute('/home', child: (_, __) => HomePage()),
            ChildRoute('/product', child: (_, __) => ProductPage()),
            ChildRoute('/config', child: (_, __) => ConfigPage()),
          ],
        ),
      ];
    }

 @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: RouterOutlet(),
      bottomNavigationBar: BottomNavigationBar(
        onTap: (id) {
          if (id == 0) {
            Modular.to.navigate('/start/home');
          } else if (id == 1) {
            Modular.to.navigate('/start/product');
          } else if (id == 2) {
            Modular.to.navigate('/start/config');
          }
        },
        currentIndex: currentIndex,
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.control_camera),
            label: 'product',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.settings),
            label: 'Config',
          ),
        ],
      ),
    );
  }

Features and bugs

Please send feature requests and bugs at the issue tracker.

This README was created based on templates made available by Stagehand under a BSD-style license.

This project follows the all-contributors specification. Contributions of any kind are welcome!

Libraries

flutter_modular