Package that helps you implementing BloC Pattern by Dependency Injection in your project.
BLoC stands for Business Logic Components. The gist of BLoC is that everything in the app should be represented as stream of events: widgets submit events; other widgets will respond. BLoC sits in the middle, managing the conversation. It will be created separately from the view, isolating the logic of the code.
It's perfect to organize, and follow the best practices in your code, taking vantage of Dependency Injection. And it's the best package to use with slidy
(created to structure your Flutter project).
Add bloc_pattern
in your pubspec.yaml.
dependencies:
bloc_pattern: ^2.3.2
Or you can use slidy
to add in your dependencies:
slidy install bloc_pattern
Create the BloC class of your module, and extends from BlocBase.
import 'package:bloc_pattern/bloc_pattern.dart';
import 'package:rxdart/rxdart.dart';
class CounterBloc extends BlocBase{
Observable<int> counter; // observable (Stream)
CounterBloc() {
counter = Observable.merge([ //merges the both streams
_increment,
_decrement,
]).startWith(0) //starts with the value 0(the initial data)
.scan((acc, curr, i) => acc + curr, 0 /* inital value: 0 */) // scans the old(acc) and the current(curr) value, and sum them
.asBroadcastStream(); //turns the stream into a Broadcast straem(it can be listened to more than once)
}
final _increment = new BehaviorSubject<int>(); //the BehaviorSubject gets the last value
final _decrement = new BehaviorSubject<int>();
void increment() => _increment.add(1); //method to increment
void decrement() => _decrement.add(-1);//method to decrement
@override
void dispose() {// will be called automatically
_increment.close();
_decrement.close();
}
}
Now wrap your MaterialApp into a BlocProvider. Obs.: BlocProvider is the widget where you can Inject all the BloCs, and then recover them anywhere in your application.
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return BlocProvider(
blocs: [
Bloc((i) => CounterBloc()),
],
child: MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}
...
//recovering your BloC
final bloc = BlocProvider.getBloc<CounterBloc>();
The StreamBuilder widgets lets you change the UI reactively without needing to call setState()(that rebuilds the stateful widget);
StreamBuilder(
stream: bloc.outCounter, //here you call the flux of data(stream)
builder: (BuildContext context, AsyncSnapshot snapshot) {
return Text(
'${snapshot.data}',
style: Theme.of(context).textTheme.display1,
);
},
),
You can consume your BloC directly from the target Widget using the Consumer() class.
Everytime the CounterBloc adds a new value, the widgets within the Consumer will have new data.
You can see the project source code here
.
class CounterBloc {
...
int counter = 0;
onChanged(){
counter++;
notifyListeners(); //notifies when occurs a change
}
}
Consumer<CounterBloc>(
builder: (BuildContext context, CounterBloc bloc) {
return Text(bloc.counter.toString()); //calling the counter value
),
SizedBox(
height: 25.0,
),
Consumer<CounterBloc>(
builder: (BuildContext context, CounterBloc bloc) {
return
RaisedButton(
onPressed: () {
bloc.onChanged(); //calling onChanged() that will increment the value
},
child: Icon(Icons.add),
);
},
),
You can also inject other dependencies aside BloC:
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return BlocProvider(
child: MaterialApp(
home: IncrementWidget(),
),
//add yours BLoCs controlles
blocs: [
Bloc((i) => IncrementController(i.get<GeneralApi>({"name":"John"}))),
Bloc((i) => DecrementController())
],
//add Other Object to provider
dependencies: [
Dependency((i) => GeneralApi(i.params['name'])), //repository
],
);
}
You can define if the dependency will be a Singleton or not:
Bloc((i) => CounterBloc(), singleton: false)
To inject the dependency in your class use:
@override
Widget build(BuildContext context) {
//recovering your API dependency
final GeneralApi api = BlocProvider.getDependency<GeneralApi>(); //repository
//Passing Data through parameters
final UserModel user = BlocProvider.getDependency<UserModel>({
"id": 1,
"name": "João"
});
....
}
You can create new BlocProviders independently.
- Use the property "tagText" giving a name for your new BlocProvider.
- When you have more than one BlocProvider, you will need to use its tag to indentificate, otherwise it should return an error.
...
@override
Widget build(BuildContext context) {
return BlocProvider(
//tag module
tagText: "newModule",
...
Calling the dependencies and BloCs from other classes:
BlocProvider.tag("newModule").getBloc<BlocController>();
...
The ModuleWidget uses Tag Module in its structure:
- Implicity creates a tag for the module class.
- Gets automatically the tag when you call the module BloC.
You can use
Slidy
to create the module
class HomeModule extends ModuleWidget {
//Inject the blocs
@override
List<Bloc<BlocBase>> get blocs => [
Bloc((i) => IncrementController())),
Bloc((i) => DecrementController())
];
//Inject the dependencies
@override
List<Dependency> get dependencies => [
Dependency((i) => GeneralApi(i.params['name'])),
];
//main widget
@override
Widget get view => HomeWidget();
//shortcut to pick up dependency injections from this module
static Inject get to => Inject<HomeModule>.of();
}
So instead of using BlocProvider and tags, you can just use ModuleWidget, it will also organize and modularize your project.
//use
HomeModule.to.bloc<HomeBloc>();
//instead of
BlocProvider.tag("HomeModule").bloc<HomeBloc>();
It's important to always call the dispose(), to make sure that the objects won't continue processing when you don't have any data.
The BlocBase already comes with Disposable, you just need to override it and will be called automatically in your ModuleWidget
class YourBloc extends BlocBase {
@override
void dispose(){
super.dispose
//dispose Objects
}
}
To do this manually or restart some injected singleton, use:
//dispose BLoC
BlocProvider.disposeBloc<BlocController>();
//dispose dependency
BlocProvider.disposeDependency<GeneralApi>();
//dispose BLoC in Module
BlocProvider.tag("HomeModule").disposeBloc<BlocController>();
//dispose BLoC in ModuleWidget
HomeModule.to.disposeBloc<BlocController>();
[Optional] Extends Disposable in your service or repository for automatic dispose.
class Repository extends Disposable {
@override
void dispose(){
//dispose Objects
}
}
You can start your modules in the test environment and use dependency injections directly.
...
import 'package:flutter_test/flutter_test.dart';
void main() {
//start Module and Dependency Injection
initModule(AppModule());
AppBloc bloc;
setUp(() {
//get bloc
bloc = AppModule.to.bloc<AppBloc>();
});
group('AppBloc Test', () {
test("Counter Test", () {
expect(bloc.counter, 1);
});
test("Class Test", () {
expect(bloc, isInstanceOf<AppBloc>());
});
});
}
You can also override injection elements in initModule. Use this to replace your client with a Mock.
import 'package:flutter_test/flutter_test.dart';
class MockClient extends Mock implements Dio {}
void main() {
//start Module and Dependency Injection
initModule(AppModule(), changeDependencies: [
Dependency((i) => MockClient() as Dio),
]);
}
You can create your mocks based on the "BlocProvider.isTest" static property, which returns a boolean.
Access Flutterando Blog.