mwb 2.0.0 mwb: ^2.0.0 copied to clipboard
Flutter package for the Model-Widget-BLoC pattern
MWB samples #
These sample apps demonstrate how the MWB pattern could be implemented within a small scope.
Both app use the JSON placeholder API for simple data display/usage.
General Architecture #
The general approach with the MWB pattern is to separate business logic and data access from UI classes. This can benefit the app since you'll be able to test abstract logic separately, have a more clean architecture and refrain from mixing code in places where it doesn't really belong.
In the image above you see how the sample app uses this pattern. A page (analog to an Activity
or a ViewController
) accesses the Bloc instance
via the enveloping BlocProvider
widget and executes functions inside its bloc member variable.
Setup #
In order to provide a Bloc instance to it's widget scope you'll first need to create a BlocProvider
widget.
NOTE: Any instance of a BlocProvider
widget needs to be created "above" the widget-tree scope it provides for to work properly.
To keep things simple for that you can use a static "factory pattern" (but not an actual factory function) instead of a widget instance, which would always create a BlocProvider and a Bloc instance around your page.
class PostsPage extends StatefulWidget {
static Widget create() {
return BlocProvider(
bloc: PostsPageBloc(),
child: PostsPage(),
);
}
}
That way you'd have less of a hassle to make Bloc creation consistent.
Bloc access (example/reactive_ui) #
Whenever you'll need to access a bloc instance you should first assign it to a local variable inside the State of your widget.
@override
void initState() {
super.initState();
_bloc = BlocProvider.of(context);
}
You can then access data or execute functions of that Bloc.
For example, inside the reactive_ui sample, you can refresh your list of posts by tapping the IconButton
inside the AppBar
.
That Button executes a Bloc functions that updates the list of posts.
IconButton(
icon: Icon(Icons.refresh),
onPressed: _bloc.refreshList,
)
Reactive UI #
Oftentimes your UI needs to display data from another source and react to its changes along the way.
The MWB pattern does that by combining Subjects
from RxDart and simple StreamBuilders
.
The reactive_ui sample demonstrates this by storing the list of all loaded posts inside a
BehaviorSubject
.
final BehaviorSubject<List<Post>> _postsSubject =
BehaviorSubject.seeded(null);
Stream<List<Post>> get postsStream => _postsSubject.stream;
Short explanation: A Subject
in RxDart is essentially just a wrapper with a Stream
and a StreamSink
to receive and broadcast data.
A BehaviorSubject
in particular also stores it's latest received value and broadcasts it for every new listener.
After receiving a new list of posts within the pages Bloc (see: Setup)
the page UI can rebuild it's list according to the changes with a StreamBuilder
.
StreamBuilder(
stream: _bloc.postsStream,
builder: (BuildContext context, AsyncSnapshot<List<Post>> snapshot) {
if (snapshot.hasData) {
return ListView(
children: snapshot.data
.map((post) => _createPostListItem(post))
.toList(),
);
} else {
return Center(
child: Text("No posts yet"),
);
}
},
)
Why use RxDart? #
Unlike in other languages and frameworks that are supported by Rx, RxDart doesn't come with a whole lot of patterns and structures that need to be implemented first so that the developer/user can benefit from it. RxDart is merely an extension of the already existing Stream API of Dart, which you should be familiar with in Flutter. What RxDart brings along are a set of helper- and extension-functions and classes based on Dart Stream API which are especially useful for business logic.
Bloc-UI contracts (example/bloc_ui_contract) #
Even though using Subjects
and StreamBuilders
has very clear and powerful benefits, not all cases are covered in a simple way with them.
This is where MWB diverges from the usual Bloc pattern and becomes more like MVP. Sometimes it is just enough to use an interface and trigger actions of the UI directly.
The bloc_ui_contract sample shows this. Since a Bloc class is supposed to be kept independent from the UIs logic,
it can't execute any functions of a State
or a Widget
. That's why MWB uses an interface as a "contract" to tell a Bloc, what kind of functions can be executed
on the UI.
abstract class CreatePostReceiver {
void showMessage(String message);
}
The showMessage
function in this case is a one-time action which would be difficult to implement with a StreamBuilder
.
That's why the pages State
implements the defined contract and assigns itself as a receiver for any Bloc-driven actions
class _CreatePostState extends State<CreatePostPage> implements CreatePostReceiver{
@override
void initState() {
super.initState();
_bloc = BlocProvider.of(context);
_bloc.receiver = this;
}
@override
void showMessage(String message) {
_scaffoldKey.currentState.showSnackBar(SnackBar(
content: Text(message),
));
}
}
which the Bloc instance then uses.
receiver?.showMessage("New post created with ID ${post.id}");
Global app architecture #
The MWB pattern can also be used to create a global app state which provides access to different kind of objects from anywhere within the app.
This is done with an AppBloc
. Like an Application
class in Android this specific Bloc has only 1 instance for the entire app
and acts as a holder for all global singletons. This also applies for the AppBloc
's BlocProvider
widget instance.
In order to keep the AppBloc
instance global you should inject it at the highest possible place inside the widget tree.
A usual place would be where the MaterialApp
instance is being created.
class TestApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
bloc: AppBloc(),
child: MaterialApp(
title: 'Flutter Demo',
home: PostsPage.create(),
),
);
}
}
For global access to singletons inside your AppBloc
a useful thing to have is a mixin.
mixin AppBlocProvider {
AppBloc get appBloc => AppBloc._instance;
AppSettings get appSettings => appBloc.appSettings;
Repository get repository => appBloc.repository;
NavigationService get navService => appBloc.navigationService;
}