flutter_stream_friends 0.1.0 copy "flutter_stream_friends: ^0.1.0" to clipboard
flutter_stream_friends: ^0.1.0 copied to clipboard

outdatedDart 1 only

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 corresponding State 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 separate State class
  • No longer make manual calls to setState. Just set up your stream and the StreamWidget 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 or buffer 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!

1
likes
0
pub points
20%
popularity

Publisher

unverified uploader

Flutter's great. Streams are great. Let's mingle.

Repository (GitLab)
View/report issues

License

unknown (LICENSE)

Dependencies

flutter

More

Packages that depend on flutter_stream_friends