flutter_stream_friends 0.8.0

  • README.md
  • CHANGELOG.md
  • Example
  • Installing
  • Versions
  • 94

flutter_stream_friends

Build Status codecov

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 able to 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 help with.

How it works

In order to understand the concept, let's compare the default usage of StatefulWidget to a StreamBuilder version. This library used to provide a StreamWidget, but we now recommend using the new StreamBuilder widget provided by the Flutter framework.

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.
    );
  }
}

StreamBuilder 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 setState, we use the power of the Dart Stream to continually listen to and deliver new information to the Widget in response to button presses!

How it works:

  • Create a Stateless widget that contains a StreamBuilder
  • The StreamBuilder takes a stream parameter. Instead of creating a State object to manage the counter state, we'll create a Stream instead that will deliver the current count.
  • The Stream we build contains a VoidStreamCallback that acts as both the onPressed handler on the floatingActionButton and as the stream we'll listen to so we know when the button is pressed.
  • Then, as the button is pressed, the Stream will deliver the latest value to the

Now that we've chatted a bit about how it works, let's see the code!

class MyApp extends StatelessWidget {
  static String appTitle = "Flutter Stream Friends";

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: appTitle,
      theme: new ThemeData(
        primarySwatch: Colors.purple,
      ),
      home: new StreamBuilder(
          stream: new CounterScreenStream(appTitle),
          builder: (context, snapshot) => buildHome(
              context,
              snapshot.hasData
                  // If our stream has delivered data, build our Widget properly
                  ? snapshot.data
                  // If not, we pass through a dummy model to kick things off
                  : new CounterScreenModel(0, () {}, appTitle))),
    );
  }

  // The latest value of the CounterScreenModel from the CounterScreenStream is
  // passed into the this version of the build function!
  Widget buildHome(BuildContext context, CounterScreenModel model) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(model.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 CounterScreenStream extends Stream<CounterScreenModel> {
  final Stream<CounterScreenModel> _stream;

  CounterScreenStream(String title,
      [VoidStreamCallback onFabPressed, int initialValue = 0])
      : this._stream = createStream(
            title, onFabPressed ?? new VoidStreamCallback(), initialValue);

  @override
  StreamSubscription<CounterScreenModel> listen(
          void onData(CounterScreenModel event),
          {Function onError,
          void onDone(),
          bool cancelOnError}) =>
      _stream.listen(onData,
          onError: onError, onDone: onDone, cancelOnError: cancelOnError);

  // The method we use to create the stream that will continually deliver data
  // to the `buildHome` method.
  static Stream<CounterScreenModel> createStream(
      String title, VoidStreamCallback onFabPressed, int initialValue) {
    return new Observable(onFabPressed) // Every time the FAB is clicked
        .map((_) => 1) // Emit the value of 1
        .scan(
            (int a, int b, int i) => a + b, // Add that 1 to the total
            initialValue)
        // Before the button is clicked, kick everything off by emitting 0
        .startWith(initialValue)
        // Convert the latest count and the event handler into the Widget Model
        .map((int count) => new CounterScreenModel(count, onFabPressed, title));
  }
}

class CounterScreenModel {
  final String title;
  final int count;
  final VoidCallback onFabPressed;

  CounterScreenModel(this.count, this.onFabPressed, this.title);

  // 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) =>
      identical(this, other) ||
      other is CounterScreenModel &&
          runtimeType == other.runtimeType &&
          title == other.title &&
          count == other.count &&
          onFabPressed == other.onFabPressed;

  @override
  int get hashCode => title.hashCode ^ count.hashCode ^ onFabPressed.hashCode;

  @override
  String toString() =>
      'CounterScreenModel{title: $title, count: $count, onFabPressed: $onFabPressed}';
}

Why would you do this madness!?

You might ask: Why would you do this? The second version is so much more code! And you're right, for a super simple example, such as a counter, this is indeed much more code.

However, there are some important advantages: First, separation of concerns. The state logic is now properly encapsulated as a Stream is easily testable. This should not be undervalued.

Second, it makes your state management fundamentally reactive! That means your Widgets can stay up to date with a variety of data sources that emit state changes (think Firebase or WebSockets or Redux). For example:

  • You may have more complex data needs, such as:
    • calling a local database, file system, or web service when your Widget initializes
    • Keeping your Widgets up to date with a reactive data source, such as a Firebase Database, WebSocket, or Redux Store
  • No longer make manual calls to setState. Just set up your stream and the StreamWidget handles the rest.
  • You can use the power of Streams 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 StreamSubscriptions.
  • 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!

Changelog

0.8.0

  • Support Dart 2

0.7.0

  • Automatically closes the streamCotroller when all subscriptions to a StreamCallback are cancelled
  • Add InfiniteScrollStream. Emits events when a list is scrolled to the end so you can fetch more data!

0.6.2

  • Add RefreshStreamCallback

0.6.1

  • Move to github

0.6.0

  • Updated the lib for compatibility with the latest version of Flutter

0.5.0

  • Updated the lib for compatibility with the latest version of Flutter

0.1.0

Initial version. This introduces the concept of aStreamWidget. This is a StatefulWidget that manages its state using streams rather than manual calls to setState within a State object. Also includes a number of StreamCallbacks. These are a special type of class that act as both an event handler and a Stream. Simply pass the StreamCallback as an event-hanler for a given event, such as onTap, and then listen to the StreamCallback for any time the Widget is tapped.

The following Flutter Callbacks are covered by StreamCallbacks:

  • VoidCallback
  • GestureDragDownCallback
  • GestureDragStartCallback
  • GestureDragUpdateCallback
  • GestureDragEndCallback
  • GestureDragCancelCallback
  • GestureLongPressCallback
  • GestureMultiTapDownCallback
  • GestureMultiTapUpCallback
  • GestureMultiTapCallback
  • GestureMultiTapCancelCallback
  • GestureMultiTapDownCallback
  • GestureMultiTapUpCallback
  • GestureScaleStartCallback
  • GestureScaleUpdateCallback
  • GestureScaleEndCallback
  • GestureTapDownCallback
  • GestureTapUpCallback
  • GestureTapCallback
  • GestureTapCancelCallback
  • DismissDirectionCallback
  • DraggableCanceledCallback

example/lib/main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_stream_friends/flutter_stream_friends.dart';
import 'package:rxdart/rxdart.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  static String appTitle = "Flutter Stream Friends";

  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: appTitle,
      theme: new ThemeData(
        primarySwatch: Colors.purple,
      ),
      home: new StreamBuilder<CounterScreenModel>(
        stream: new CounterScreenStream(appTitle),
        initialData: new CounterScreenModel(0, () {}, appTitle),
        builder: (context, snapshot) => buildHome(context, snapshot.data),
      ),
    );
  }

  // The latest value of the CounterScreenModel from the CounterScreenStream is
  // passed into the this version of the build function!
  Widget buildHome(BuildContext context, CounterScreenModel model) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(model.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 CounterScreenStream extends StreamView<CounterScreenModel> {
  CounterScreenStream(
    String title, [
    VoidStreamCallback onFabPressed,
    int initialValue = 0,
  ])
      : super(createStream(
          title,
          onFabPressed ?? new VoidStreamCallback(),
          initialValue,
        ));

  // The method we use to create the stream that will continually deliver data
  // to the `buildHome` method.
  static Stream<CounterScreenModel> createStream(
    String title,
    VoidStreamCallback onFabPressed,
    int initialValue,
  ) {
    return new Observable(onFabPressed) // Every time the FAB is clicked
        .map((_) => 1) // Emit the value of 1
        .scan(
            (int a, int b, int i) => a + b, // Add that 1 to the total
            initialValue)
        // Before the button is clicked, kick everything off by emitting 0
        .startWith(initialValue)
        // Convert the latest count and the event handler into the Widget Model
        .map((int count) => new CounterScreenModel(count, onFabPressed, title));
  }
}

class CounterScreenModel {
  final String title;
  final int count;
  final VoidCallback onFabPressed;

  CounterScreenModel(this.count, this.onFabPressed, this.title);

  // 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) =>
      identical(this, other) ||
      other is CounterScreenModel &&
          runtimeType == other.runtimeType &&
          title == other.title &&
          count == other.count;

  @override
  int get hashCode => title.hashCode ^ count.hashCode;

  @override
  String toString() =>
      'CounterScreenModel{title: $title, count: $count, onFabPressed: $onFabPressed}';
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  flutter_stream_friends: "^0.8.0"

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter packages get

Alternatively, your editor might support flutter packages get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


      import 'package:flutter_stream_friends/flutter_stream_friends.dart';
  
Version Uploaded Documentation Archive
0.8.0 Apr 1, 2018 Go to the documentation of flutter_stream_friends 0.8.0 Download flutter_stream_friends 0.8.0 archive
0.7.0 Feb 25, 2018 Go to the documentation of flutter_stream_friends 0.7.0 Download flutter_stream_friends 0.7.0 archive
0.6.2 Jan 9, 2018 Go to the documentation of flutter_stream_friends 0.6.2 Download flutter_stream_friends 0.6.2 archive
0.6.1 Nov 25, 2017 Go to the documentation of flutter_stream_friends 0.6.1 Download flutter_stream_friends 0.6.1 archive
0.5.0 May 25, 2017 Go to the documentation of flutter_stream_friends 0.5.0 Download flutter_stream_friends 0.5.0 archive
0.4.0 Jan 11, 2017 Go to the documentation of flutter_stream_friends 0.4.0 Download flutter_stream_friends 0.4.0 archive
0.3.0 Jan 11, 2017 Go to the documentation of flutter_stream_friends 0.3.0 Download flutter_stream_friends 0.3.0 archive
0.2.0 Dec 6, 2016 Go to the documentation of flutter_stream_friends 0.2.0 Download flutter_stream_friends 0.2.0 archive
0.1.0 Dec 4, 2016 Go to the documentation of flutter_stream_friends 0.1.0 Download flutter_stream_friends 0.1.0 archive

Analysis

We analyzed this package on Jun 19, 2018, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.0.0-dev.63.0
  • pana: 0.11.3
  • Flutter: 0.5.4

Scores

Popularity:
Describes how popular the package is relative to other packages. [more]
89 / 100
Health:
Code health derived from static analysis. [more]
99 / 100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100 / 100
Overall score:
Weighted score of the above. [more]
94
Learn more about scoring.

Platforms

Detected platforms: Flutter

References Flutter, and has no conflicting libraries.

Suggestions

  • The description is too short.

    Add more detail about the package, what it does and what is its target use case. Try to write at least 60 characters.

  • Package is pre-v1 release.

    While there is nothing inherently wrong with versions of 0.*.*, it usually means that the author is still experimenting with the general direction API.

  • Fix analysis and formatting issues.

    Analysis or formatting checks reported 1 hint.

    Run flutter format to format lib/src/infinite_scroll_stream.dart.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.0.0-dev.33.0 <3.0.0
flutter 0.0.0
meta >=1.0.4 <2.0.0 1.1.5
Transitive dependencies
collection 1.14.6 1.14.10
sky_engine 0.0.99
typed_data 1.1.5
vector_math 2.0.6 2.0.7
Dev dependencies
flutter_test
test >=0.12.0 <0.13.0