Stream first class citizen in Flutter

August 10, 2019

Stream plays an important role in Flutter. Stream often is used to handle event change and update the widget accordingly. In this post I want to demo some abilities of stream and how to make it become more easier to control in a Flutter application.

stream-first-citizen-in-flutter

1. A simple usecase of stream

First, stream is used to rebuild object whenever data changes. Consider we have a bloc that manage the counter value:

bloc_counter.dart
import 'package:rxdart/subjects.dart';

class BlocCounter {
  BehaviorSubject<int> counter;
  BlocCounter() {
    counter = BehaviorSubject();
  }
  dispose() {
    counter.close();
  }
}

Then we will use that stream to build the Text widget everytime the value of the counter subject changes:

home.dart
import 'package:example/bloc_counter.dart';
import 'package:flutter/material.dart';

class Home extends StatefulWidget {
  Home({Key key, this.title}) : super(key: key);

  final String title;

  
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  BlocCounter _blocCounter;
  
  void initState() {
    super.initState();
    _blocCounter = BlocCounter();
    _blocCounter.counter.value = 0;
  }

  void _incrementCounter() {
    _blocCounter.counter.value++;
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: StreamBuilder<int>(
            stream: _blocCounter.counter.stream,
            builder: (context, snapshot) {
              return Text(
                snapshot.data.toString(),
                style: Theme.of(context).textTheme.headline3,
              );
            }),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), 
    );
  }
}

2. Mapping a stream to another stream

We can do some advanced things to stream like listen to the change of the value and decide something with the new value. For example, I will create a numberType stream that will trigger a odd string or a even string according to the current value of counter.

bloc_counter.dart
import 'package:rxdart/subjects.dart';

class BlocCounter {
  BehaviorSubject<int> counter;
  Stream<String> numberType;
  BlocCounter() {
    counter = BehaviorSubject();
    numberType = counter.asyncMap((number) {
      if (number % 2 == 0) {
        return 'even';
      }
      return 'odd';
    });
  }
  dispose() {
    counter.close();
  }
}

Then show the value of numberType in UI using StreamBuilder

home.dart
class Home extends StatefulWidget {
  Home({Key key, this.title}) : super(key: key);

  final String title;

  
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  BlocCounter _blocCounter;
  
  void initState() {
    super.initState();
    _blocCounter = BlocCounter();
    _blocCounter.counter.value = 0;
  }

  void _incrementCounter() {
    _blocCounter.counter.value++;
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            StreamBuilder<int>(
              stream: _blocCounter.counter.stream,
              builder: (context, snapshot) {
                return Text(
                  snapshot.data.toString(),
                  style: Theme.of(context).textTheme.headline3,
                );
              },
            ),
            StreamBuilder<String>(
              stream: _blocCounter.numberType,
              builder: (context, snapshot) {
                return Text(
                  snapshot.data.toString(),
                  style: Theme.of(context).textTheme.headline3,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), 
    );
  }
}

3. Stream events with generator and class inheritance

In the above example we already transformed an input value stream to map with an output value stream. However, in practice, we also want to map an input stream with a series of events, i.e. api request events.

Stream allows us to do that by transform each input value to a Stream of type Event - the parent event. The process method we use to transform is often written as a generator function. Foreach event we want to trigger in the method, we will yield it as an instance of a subclass of the generic class of the output stream. The example below will demonstrate how this approach will be implemented.

First we will define how many kinds of events we want to receive. They are EventStart, EventSuccess and EventFailure.

event.dart
class Event {}

class EventStart extends Event {
  
  String toString() {
    return "Start calculating";
  }
}

class EventSuccess extends Event {
  final dynamic _data;
  EventSuccess(this._data);
  
  String toString() {
    return "Success with even number: $_data";
  }
}

class EventFailure extends Event {
  final dynamic _error;
  EventFailure(this._error);
  
  String toString() {
    return "Failure with error: $_error";
  }
}

Then, we will construct a BLoC to define the logic. We will have an input stream of type int and an output stream of type Event above. For each input value, we will fire a EventStart instance, and wait for 1 second, then calculate the data as the input value pluses one. Finally we will trigger EventSuccess if the data value is even and EventFailure if the data value is odd.

bloc_event.dart
import 'package:example/event.dart';
import 'package:rxdart/subjects.dart';

class BlocEvent {
  BehaviorSubject<int> input;
  Stream<Event> event;
  BlocEvent() {
    input = BehaviorSubject();
    event = input.stream.asyncExpand(_mapStream).asBroadcastStream();
  }

  Stream<Event> _mapStream(dynamic input) async* {
    yield EventStart();

    await Future.delayed(Duration(seconds: 1));

    int data = input + 1;

    if (data % 2 == 0) {
      yield EventSuccess(data);
      return;
    }
    
    yield EventFailure("number is odd");
  }

  dispose() {
    input.close();
    event.drain();
  }
}

After we have all logics defined, we will implement them to the UI.

home.dart
import 'package:example/bloc_event.dart';
import 'package:example/event.dart';
import 'package:flutter/material.dart';

class Home extends StatefulWidget {
  Home({Key key, this.title}) : super(key: key);

  final String title;

  
  _HomeState createState() => _HomeState();
}

class _HomeState extends State<Home> {
  BlocEvent _blocEvent;
  int _currentInput;
  
  void initState() {
    super.initState();
    _blocEvent = BlocEvent();
    _currentInput = 0;
  }

  void _increaseInputAndCalculate() {
    _blocEvent.input.value = _currentInput;
    _currentInput++;
  }

  
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            StreamBuilder<int>(
              stream: _blocEvent.input.stream,
              builder: (context, snapshot) {
                if (snapshot.hasData == false) return SizedBox();
                return Text(
                  "Input value: " + snapshot.data.toString(),
                  style: Theme.of(context).textTheme.headline4,
                );
              },
            ),
            Container(
              height: 120,
              padding: const EdgeInsets.only(top: 30, right: 10, left: 10),
              child: StreamBuilder<Event>(
                stream: _blocEvent.event,
                builder: (context, snapshot) {
                  if (snapshot.hasData == false)
                    return SizedBox(
                      height: 30,
                    );
                  return Text(
                    snapshot.data.toString(),
                    textAlign: TextAlign.center,
                    style: Theme.of(context).textTheme.headline4,
                  );
                },
              ),
            ),
            SizedBox(
              height: 30,
            ),
            RaisedButton(
              onPressed: _increaseInputAndCalculate,
              child: Text('Increase input and get an even number'),
            ),
          ],
        ),
      ),
    );
  }
}

4. Conclusion

In this post, we have gone through from the basic usages to advanced usages of Stream in Flutter. You can see how powerful stream is and there are a lot things that can do with stream that are not recommended in this post like processing controllers, subscribing to data change.

There is one caveat of stream is that it seems that stream is suitable for local data management. For the data flow management in the app, there are another solution like Provider or InheritedWidget.

You can find source code in this post here here

Thank you for reading, I hope that you find this post useful.