flutter_modular 1.2.0 flutter_modular: ^1.2.0 copied to clipboard
Smart project structure with dependency injection and route management
Flutter Modular #
Read this in other languages: English, Brazilian Portuguese.
What is Flutter Modular? #
When a project is getting bigger and more complex, we unfortunately end up joining a lot of archives in just one, it makes harder the code maintenance and reusability too. The Modular give us a bunch of adapted solutions for Flutter, such a dependency injection, routes controller and a "Disposable Singletons" System(When a code provider call automatically dispose and clear the injection). The Modular came up prepared for adapt to any state management approach to its smart injection system, managing the memory use of your application.
Modular Structure #
Modular gives us a structure that allows us to manage dependency injection and routes in just one file per module, so we can organize our files with that in mind. When all pages, controllers, blocs (and so on) are in a folder and recognized by this main file, we call this a module, as it will provide us with easy maintainability and especially the TOTAL decoupling of code for reuse in other projects.
Modular Pillars #
Here are our main focuses with this package.
- Automatic Memory Management.
- Dependency Injection.
- Dynamic Routes Control.
- Modularization of Code.
Examples #
Getting started with Modular #
Installation #
Open pubspec.yaml of your Project and type:
dependencies:
flutter_modular:
or install directly from Git to try out new features and fixes:
dependencies:
flutter_modular:
git:
url: https://github.com/Flutterando/modular
Using in a New Project #
You need to do some initial setup.
Create a file to be your main widget, thinking of configuring named routes within MaterialApp
: (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(
// set your initial route
initialRoute: "/",
// add Modular to manage the routing system
onGenerateRoute: Modular.generateRoute,
);
}
}
Create a file to be your main module: (app_module.dart)
// extends from MainModule
class AppModule extends MainModule {
// here will be any class you want to inject into your project (eg bloc, dependency)
@override
List<Bind> get binds => [];
// here will be the routes of your module
@override
List<Router> get routers => [];
// add your main widget here
@override
Widget get bootstrap => AppWidget();
}
Finish the configuration in your main.dart file to start Modular.
import 'package:example/app/app_module.dart';
import 'package:flutter/material.dart';
import 'package:flutter_modular/flutter_modular.dart';
void main() => runApp(ModularApp(module: AppModule()));
Ready! Your app is already set to Modular!
Adding Routes #
You can add routes to your module using the getter 'routers';
class AppModule extends MainModule {
// here will be any class you want to inject into your project (eg bloc, dependency)
@override
List<Bind> get binds => [];
// here will be the routes of your module
@override
List<Router> get routers => [
Router("/", child: (_, args) => HomePage()),
Router("/login", child: (_, args) => LoginPage()),
];
// add your main widget here
@override
Widget get bootstrap => AppWidget();
}
And to access the route use Navigator.pushNamed
or Modular.to.pushNamed
:
Navigator.pushNamed(context, '/login');
//or
Modular.to.pushNamed('/login');
Current Module Navigation #
Use Modular.to for literal paths or Modular.link for routes in current module.
//Modules home>product
Modular.to.pushNamed('/home/product/list');
Modular.to.pushNamed('/home/product/detail/:id');
//into product module, use Modular.link and navigate between routes of current Module (Product)
Modular.link.pushNamed('/list');
Modular.link.pushNamed('/detail/:id');
Dynamic Routes #
You can use the dynamic route system to pass a value per parameter and get it in your view.
//use (: parameter_name) to use dynamic routes;
//use the args object that is a (ModularArguments) to get the value
@override
List<Router> get routers => [
Router("/product/:id", child: (_, args) => Product(id: args.params['id'])),
];
A dynamic route is considered valid when the value corresponding to the parameter is filled. From this you can use:
Navigator.pushNamed(context, '/product/1'); //args.params['id']) gonna be 1
//or
Modular.to.pushNamed('/product/1'); //args.params['id']) gonna be 1
You can also pass an object using the "arguments" property in the navigation:
Navigator.pushNamed(context, '/product', arguments: ProductModel()); //args.data
//or
Modular.to.pushNamed('/product', arguments: ProductModel()); //args.data
getting on the route
@override
List<Router> get routers => [
Router("/product", child: (_, args) => Product(model: args.data)),
];
Route Guard #
We may protect our routes with middleware that will verify that the route is available within a given Route.
First create a RouteGuard
:
class MyGuard implements RouteGuard {
@override
bool canActivate(String url) {
if(url != '/admin'){
//code of authorization
return true;
} else {
//access denied
return false
}
}
}
Now put in the 'guards' property of your Router.
@override
List<Router> get routers => [
Router("/", module: HomeModule()),
Router("/admin", module: AdminModule(), guards: [MyGuard()]),
];
If placed on a module route, RouterGuard will be global to that route.
Route Transition Animation #
You can choose which type of animation you want by setting the Router's transition parameter using the TransitionType enum.
Router("/product",
module: AdminModule(),
transition: TransitionType.fadeIn), //use for change transition
If you use transition in a module, all routes in that module will inherit this transition animation.
Custom Transition Animation Route #
You can also use a custom transition animation by setting the Router parameters transistion and customTransition with TransitionType.custom and the CustomTransition, respectively.
Router("/product",
module: AdminModule(),
transition: TransitionType.custom,
customTransition: myCustomTransition),
// ...
And, for example, in a custom transitions file declare your custom transitions.
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,
),
),
)
;
},
);
Grouping Routes #
You can group routes that contains one (or more) properties in common. Properties guards, transition and customTransition can be used together or just one to group routes in common.
List<Router> get routers => [
Router("/", module: HomeModule()),
]..addAll(Router.group(guards: [MyGuard()], routes: [
Router("/admin", module: AdminModule()),
Router("/profile", module: ProfileModule()),
])); // Adiciona as rotas agrupadas ao final da lista
Another way is using Sperad Operator, introduced in Dart 2.3:
List<Router> get routers => [
Router("/", module: HomeModule()),
...Router.group(guards: [MyGuard()],
transition: TransitionType.rightToLeftWithFade,
routes: [
Router("/admin", module: AdminModule()),
Router("/profile", module: ProfileModule()),
]),
]; // Mesclar usando
Router generic types #
You may need to navigate to a specific page and request a return value in the pop()
, You can type the Router object with the value of that return;
@override
List<Router> get routers => [
//type router with return type
Router<String>('/event', child: (_, args) => EventPage()),
]
Now you can type your pushNamed and pop
String name = await Modular.to.pushNamed<String>();
//and
Modular.to.pop('Jacob Moura');
Flutter Web url Routes #
The Routing System also recognizes what is typed in the website url (flutter web) so what you type in the browser url will open in the app. We hope this makes it easier for Flutter Web sites to make SEO more unique.
Dynamic routes apply here as well:
https://flutter-website.com/#/product/1
this will open the Product view and args.params ['id']) will be equal to 1.
Dependency Injection #
You can inject any class into your module using getter 'binds', for example classes BLoC, ChangeNotifier or Stores.
Bind is responsible for configuring object injection.
class AppModule extends MainModule {
// here will be any class you want to inject into your project (eg bloc, dependency)
@override
List<Bind> get binds => [
Bind((i) => AppBloc()), // using bloc
Bind((i) => Counter()), // using ChangeNotifier
];
// here will be the routes of your module
@override
List<Router> get routers => [
Router("/", child: (_, args) => HomePage()),
Router("/login", child: (_, args) => LoginPage()),
];
// add your main widget here
@override
Widget get bootstrap => AppWidget();
}
Let's assume that for example we want to retrieve AppBloc
inside HomePage
.
//code in bloc
import 'package:flutter_modular/flutter_modular.dart' show Disposable;
// you can extend or implement from Disposable to define a discard for your class, if not.
class AppBloc extends Disposable {
StreamController controller = StreamController();
@override
void dispose() {
controller.close();
}
}
Retrieving in view using injection #
You have some ways to retrieve your injected classes.
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// You can use the object Inject to retrieve..
final appBloc = Modular.get<AppBloc>();
//...
}
}
By default, objects in Bind are singletons and lazy. When Bind is lazy, the object will only be instantiated when it is called for the first time. You can use 'lazy:false' if you want your object to be instantiated immediately.
Bind((i) => OtherWidgetNotLazy(), lazy: false),
If you do not want the injected object to have a single instance, just use 'singleton: false', this will cause your object to be instantiated every time it is called
Bind((i) => OtherWidgetNotLazy(), singleton: false),
Using Modular widgets to retrieve your classes #
ModularState #
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends ModularState<MyWidget, HomeController> {
//variable controller
//automatic dispose off HomeController
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Modular"),
),
body: Center(child: Text("${controller.counter}"),),
);
}
}
Consuming a ChangeNotifier Class #
Example of a ChangeNotifier
Class:
import 'package:flutter/material.dart';
class Counter extends ChangeNotifier {
int counter = 0;
increment() {
counter++;
notifyListeners();
}
}
you can use the Consumer
to manage the state of a widget block.
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Home"),
),
body: Center(
// recognize the ChangeNotifier class and rebuild when notifyListeners () is called
child: Consumer<Counter>(
builder: (context, value) {
return Text('Counter ${value.counter}');
}
),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
// retrieving the class directly and executing the increment method
get<Counter>().increment();
},
),
);
}
}
Creating Child Modules #
You can create other modules in your project, so instead of inheriting from MainModule
, you should inherit from ChildModule
.
class HomeModule extends ChildModule {
@override
List<Bind> get binds => [
Bind((i) => HomeBloc()),
];
@override
List<Router> get routers => [
Router("/", child: (_, args) => HomeWidget()),
Router("/list", child: (_, args) => ListWidget()),
];
static Inject get to => Inject<HomeModule>.of();
}
From this you can call your modules on the main module route.
class AppModule extends MainModule {
@override
List<Router> get routers => [
Router("/home", module: HomeModule()),
//...
];
}
//...
Consider splitting your code into modules such as LoginModule
, and into it placing routes related to that module. Maintaining and sharing code in another project will be much easier.
WidgetModule #
The same structure as ChildModule
. Very useful for modular TabBar visualizations.
class TabModule extends WidgetModule {
@override
List<Bind> get binds => [
Bind((i) => TabBloc(repository: i.get<TabRepository>())),
Bind((i) => TabRepository()),
];
Widget get view => TabPage();
}
RouterOutlet #
RouterOutlet is a solution to use another route system totally detached from the Main Navigation. This is useful when you need that an element to have its own set of routes even though its inside a page on the main route. A practical example of this is its use in a TabBar or Drawer
PageView(
controller: controller
children: [
RouterOutlet(
module: Tab1Module()
),
RouterOutlet(
module: Tab2Module()
),
RouterOutlet(
module: Tab3Module()
),
]
),
NOTE: Navigation within these modules is made only using Nvigator.of(context) using the routes paths literally.
Lazy Loading #
Another benefit you get when working with modules is to load them "lazily". This means that your dependency injection will only be available when you navigate to a module, and as you exit that module, Modular will wipe memory by removing all injections and executing the dispose() methods (if available) on each module. injected class refers to that module.
Unit Test #
You can use the dependency injection system to replace Links from mock links,as an example of a repository. You can also do it using "Inversion of Control"
@override
List<Bind> get binds => [
Bind<ILocalStorage>((i) => LocalStorageSharePreferences()),
];
We have to import the "flutter_modular_test" to use the methods that will assist with Injection in the test environment.
import 'package:flutter_modular/flutter_modular_test.dart';
import 'package:flutter_test/flutter_test.dart';
...
main() {
test('change bind', () {
initModule(AppModule(), changeBinds: [
Bind<ILocalStorage>((i) => LocalMock()),
]);
expect(Modular.get<ILocalStorage>(), isA<LocalMock>());
});
}
DebugMode #
Remove prints debug:
Modular.debugMode = false;
Roadmap #
This is currently our roadmap, please feel free to request additions/changes.
Feature | Progress |
---|---|
DI by Module | ✅ |
Routes by Module | ✅ |
Widget Consume for ChangeNotifier | ✅ |
Auto-dispose | ✅ |
Integration with flutter_bloc | ✅ |
Integration with mobx | ✅ |
Multiple routes | ✅ |
Pass arguments by route | ✅ |
Pass url parameters per route | ✅ |
Route Transition Animation | ✅ |
Features and bugs #
Please send feature requests and bugs at the issue tracker.
Created from templates made available by Stagehand under a BSD-style license. license.