Build reactive mobile apps in Flutter — companion article
--
UPDATE (March 2019) — If you’re just starting to learn about Flutter and state management, I recommend you read the state management section at flutter.dev instead of this article. I wrote both this article and the section at flutter.dev, and I think the latter is more approachable and exhaustive. It is also newer.
At Google I/O, Matt and I gave a talk about state management called Build reactive mobile apps in Flutter. If you missed it, you can watch it below:
The code for the talk is on github:
This article is about the things that didn’t fit into the 33-minute session.
ValueNotifier
ValueNotifier
is a basic implementation of observable that comes with the Flutter framework. It has a value, listeners (callbacks), and it calls the listeners any time the value changes. The functionality is implemented in about 200 lines of Dart code, the vast majority of which are documentation comments.
You can use ValueNotifier
like this:
final counter = ValueNotifier(0);counter.addListener(_myCallback);counter.value = 10; // Calls _myCallback.
counter.value += 1; // Also calls _myCallback.counter.removeListener(_myCallback);
counter.value += 1; // Doesn't call anything.counter.dispose();
In other words, whenever you set the value
to something new, all the registered listeners are called.
You can extend ValueNotifier
to get more functionality. This makes the most sense with mutable values.
class CartObservable extends ValueNotifier<Cart> {
CartObservable(Cart value) : super(value); void add(Product product) {
value.add(product);
notifyListeners();
}
}
The advantage of ValueNotifier
is that it’s simple, easy to understand, and included with Flutter. It’s also completely synchronous, which might be an advantage in some cases. In itself, it won’t help you with access (you’ll have to pass around the object somehow — through InheritedWidget
or, for shallow widget trees, through constructors), and you’ll still have to manually call setState()
and manage listeners.
Firebase / Cloud Firestore references
In the talk, we show how to use the StreamBuilder
widget to automatically rebuild UI on state change.
Cloud Firestore (and its older sibling, Firebase Realtime Database) have great Flutter plugins (cloud_firestore
and firebase_database
), and these plugins use Streams. So, for example, you can just plug your Firestore snapshots into a StreamBuilder, and you’re done.
StreamBuilder(
stream: Firestore.instance.collection('baby').snapshots(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const Text('Loading...');
return ListView.builder(
itemCount: snapshot.data.documents.length,
itemBuilder: (context, index) {
DocumentSnapshot ds = snapshot.data.documents[index];
return Text("${ds['name']} ${ds['votes']}");
}
);
},
);
This is really all you need if your app only presents data, or if its business logic is very straightforward. You can learn more in the Firebase for Flutter codelab.
Of course, once your app grows, you can move away from exposing the raw Firebase stream to the widgets, and wrap it instead. For example, a business logic component can internally communicate with a real time database but the widgets don’t need to know about it.
InheritedWidget
We used InheritedWidget
extensively in our talk: implicitly with ScopedModel
, which builds on InheritedWidget
, and explicitly with BLoC, to pass around references to data streams. InheritedWidget
is part of the core Flutter platform, and provides an easy means to make data accessible anywhere in a widget tree.
Defining an InheritedWidget
is simple:
class MyInheritedValue extends InheritedWidget {
const MyInheritedValue({
Key key,
@required this.value,
@required Widget child,
}) : assert(value != null),
assert(child != null),
super(key: key, child: child); final int value; static MyInheritedValue of(BuildContext context) {
return context.inheritFromWidgetOfExactType(MyInheritedValue);
} @override
bool updateShouldNotify(MyInheritedValue old) =>
value != old.value;
}
It’s also easy to provide it to an ancestor:
Widget build(BuildContext context) {
return MyInheritedValue(
value: 42,
child: ...
);
}
And it’s easy to use it down the tree.
MyInheritedValue.of(context).value
While InheritedWidget
is useful for keeping your widget tree DRY and encapsulated (since references to data do not need to be passed explicitly), note that the data is immutable. In order to use InheritedWidget
to track changing data, either a) wrap it in a StatefulWidget
or b) use streams or ValueNotifier
inside the InheritedWidget
.
Redux
What more can be said about Redux that hasn’t already been said elsewhere? Suffice to say that Dart has an implementation of Redux, and there’s a Flutter extension that provides an elegant Widget framework on top.
In practice, the widget structure looks very similar to Scoped Model, where a StoreProvider
gives access to state throughout the widget tree, via the StoreConnector
.
There’s a simple example of using Redux to manage state in Flutter’s default incrementer app on the pub site. There’s also a more sophisticated example with a todo sample app. If you’re interested in using Redux in Flutter, check these out.
Business Logic Components (BLoC)
If you’re interested in the concept of business logic components, check out Paolo Soares’ talk from DartConf (January 2018):
The BLoC pattern was conceived by Cong Hui, Paolo Soares, and Wenfan Huang at Google. As you can see in Paolo’s talk, there is a lot more to BLoC than what we discussed in our I/O session. Among other things, BLoC allows Google to share code between Flutter (mobile) and AngularDart (web) apps, and Paolo shares some guidance about that. Here, I’d like to focus on the things that apply to Flutter regardless of whether you’re also using the component elsewhere.
With that, some additional notes about this pattern:
- BLoC doesn’t assume a particular way to get access to the component. In the talk, I’m showing how you might do that with
InheritedWidget
but nothing stops you from passing it down manually through constructors, or using some form of automatic dependency injection. - You should avoid having one BLoC as a parameter of another BLoC. Instead, plug only the required outputs to the applicable inputs. This helps avoid tight coupling.
- Large apps need more than one BLoC. A good pattern is to use one top-level component for each screen, and one for each complex-enough widget. Too many BLoCs can become cumbersome, though. Also, if you have hundreds upon hundreds of observables (streams) in your app, that has a negative impact on performance. In other words: don’t over-engineer your app.
- In a hierarchy of BLoCs, the top-level (screen) BLoC is normally responsible for plugging streams of children BLoCs to each other. More on that in a later article.
- BLoC is compatible with server logic. The pattern doesn’t force you to re-implement that logic on the client (like Flux/Redux would). Just wrap the server-side logic with a component.
- One disadvantage that stems from the asynchronicity of streams is that when you build a
StreamBuilder
, it always showsinitialData
first (because it is available synchronously) and only then does it show the first event coming from theStream
. This can lead to a flash (one frame) of missing data. There are ways to prevent this — stay tuned for a more detailed article. UPDATE: If usingrxdart
version0.19.0
and above, you can just useValueObservable
for outputs and the flash of async is no longer an issue. - The inside of the BLoC is often implemented in a purely functional-reactive way (no auxiliary state, pure transformations of one stream to another). But don’t feel obligated to do it this way. Sometimes, it’s easier and more readable/maintainable to express the business logic through hybrid imperative-functional approach (like I was doing in the I/O talk, although that was mainly to save time).
Some people asked for a more complex BLoC sample. I recreated the shopping app into a more realistic example where the catalog of products is fetched from the network page by page, and we have an infinite list of these products. Also, for each product in the catalog, we want to change the presentation of the ProductSquare slightly when the product is already in the catalog. The network calls are simulated but the complexity of wiring different things together is there.
ProductSquare
s show whether the product is in the cart (by underlining the product name, for simplicity).You can find the code in the companion repository, at lib/src/bloc_complex
. There’s a README file with more information about this version.
We hope the talk and this article have been useful. You might want to watch other Flutter talks and videos on YouTube.