flutter_stream_friends 0.1.0 flutter_stream_friends: ^0.1.0 copied to clipboard
Flutter's great. Streams are great. Let's mingle.
flutter_stream_friends #
Connect Flutter Widgets to Dart Streams! In Flutter, there's a wonderful distinction between StatefulWidgets
and StatelessWidgets
. When used well, StatefulWidgets
provide a convenient way to encapsulate your data coordination needs in one component, and keep the UI rendering in various "passive" StatelessWidgets
. In React terms, this is often called the "Smart Component / Dumb Component" pattern, and is similar to the "Active Presenter / Passive View" pattern in MVP.
However, what if you've got slightly more advanced data needs, such as loading
data from a database or web server? Furthermore, you may need to listen to a continuous stream of updates from a Store or EventBus. Finally, you may require more powerful control over your event-handling, such as being debounce
or buffer
the events passing through an event-handler. For these use cases, Streams provide a great way to manage the events and data needs of a StatefulWidget
!
In general: what if we could combine the power of StatefulWidgets
with the elegance of Streams
? That's just what this library aims to do.
How they work #
In order to understand the concept, let's compare the default usage of StatefulWidget
to a StreamWidget
, the core Widget
provided by this library.
Original #
Let's start with the simple counter example that comes out of the box when you create a new Flutter app. The important parts are:
- Create a
StatefulWidget
with a correspondingState
object - Within the
State
object, create widget state and event handlers - The event handlers are responsible for updating the local state of the widget
- Use these pieces of state within the
build
method.
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
// This widget is the home page of your application. It is stateful,
// meaning that it has a State object (defined below) that contains
// fields that affect how it looks.
// This class is the configuration for the state. It holds the
// values (in this case the title) provided by the parent (in this
// case the App widget) and used by the build method of the State.
// Fields in a Widget subclass are always marked "final".
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that
// something has changed in this State, which causes it to rerun
// the build method below so that the display can reflect the
// updated values. If we changed _counter without calling
// setState(), then the build method would not be called again,
// and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance
// as done by the _incrementCounter method above.
// The Flutter framework has been optimized to make rerunning
// build methods fast, so that you can just rebuild anything that
// needs updating rather than having to individually change
// instances of widgets.
return new Scaffold(
appBar: new AppBar(
// Here we take the value from the MyHomePage object that
// was created by the App.build method, and use it to set
// our appbar title.
title: new Text(config.title),
),
body: new Center(
child: new Text(
'Button tapped $_counter time${ _counter == 1 ? '' : 's' }.',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
), // This trailing comma tells the Dart formatter to use
// a style that looks nicer for build methods.
);
}
}
StreamWidget version #
Now, let's take a look at the version using streams! This code will produce the exact same UI, but the way it manages state is a bit different. Rather than relying on local state within a State
object, using handlers to update the local widget state, we use the power of the Dart Stream
to continually deliver new information to the Widget's build
method.
When the component is created, rather than createState
, thecreateStream
method will be called. Within the createStream
method, we build up a stream that can continuously deliver new information to the Widget. Any time the stream emits new items, setState
will automatically be called with the latest value.
In order to handle events, rather than using a normal Callback
, we'll use a StreamCallback
. This is a set of classes that act as event handlers for different types of Flutter callbacks, such as Tap or Drag events. They also act as a Stream
: you can listen
to the StreamCallback
to receive the latest value from the event handler it's wired up to.
Now that we've chatted a bit about how it works, let's see the code!
class StreamWidgetDemo extends StreamWidget<StreamWidgetDemoModel> {
// Normal state that can be passed into this component
final String title;
StreamWidgetDemo(this.title, {Key key}) : super(new StreamWidgetDemoModel(0, () {}), key: key);
/// When the component is initially built, createStream will be called and the
/// resulting stream will be listened to. When any new events are added to
/// the stream, the `Widget` will call `setState` with the value of the event.
@override
Stream<StreamWidgetDemoModel> createStream() {
// Here, we create a StreamCallback. This acts as both a stream and
// callback. This will be used as a stream informing us when the button has
// been tapped, and as the event handler on the FAB. Once set on the FAB in
// the build method, it will begin emitting events!
var onFabPressed = new VoidStreamCallback();
return observable(onFabPressed) // Every time the FAB is clicked
.map((_) => 1) // Emit the value of 1
.scan((int a, int b, int i) => a + b, 0) // Add that 1 to the total
.startWith([0]).map((int count) {
// Convert the latest count and the event handler into the Widget Model
return new StreamWidgetDemoModel(count, onFabPressed);
});
}
// The latest value of the StreamWidgetDemoModel from the created stream is
// passed into the this version of the build function!
Widget build(BuildContext context, StreamWidgetDemoModel model) {
print("Build: ${model}");
return new Scaffold(
appBar: new AppBar(
title: new Text(title),
),
body: new Center(
child: new Text(
'Button tapped ${ model.count } time${ model.count == 1
? ''
: 's' }.',
),
),
floatingActionButton: new FloatingActionButton(
// Use the `StreamCallback` here to wire up the events to the Stream.
onPressed: model.onFabPressed,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
}
class StreamWidgetDemoModel {
final int count;
final VoidCallback onFabPressed;
StreamWidgetDemoModel(this.count, this.onFabPressed);
// If you've got a custom data model for your widget, it's best to implement
// the == method in order to take advantage the performance optimizations
// offered by the `Streams#distinct()` method. This will ensure the Widget is
// repainted only when the Model has truly changed.
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is StreamWidgetDemoModel && this.count == other.count;
}
@override
int get hashCode {
return count.hashCode;
}
@override
String toString() {
return 'StreamWidgetDemoModel{count: $count, onFabPressed: $onFabPressed}';
}
}
Why would you do this madness!? #
- You may have more complex data needs, such as:
- calling a local database, file system, or web service
- keep up to date with the latest state from a data Store, such as a dart_redux Store.
- No longer need both a
Widget
and separateState
class - No longer make manual calls to
setState
. Just set up your stream and theStreamWidget
handles the rest. - You can use the power of observables to reduce the number redraws your UI performs. By using
Stream#distinct
under the hood, setState will only be called when data is truly fresh. - No need to worry about manually canceling any subscriptions.
- Helpful when you have more advanced event handling needs, such as needing to
debounce
orbuffer
the events.
Examples #
You can check out the example
directory showing the code above implemented as a real Flutter app.
Another project is being worked on that also demonstrates this concept when listening to a Redux store!