Flutter State Management with Provider — NotifyListeners x StreamProvider — Part 2
By: Wilker Oliveira
Tags: Flutter, Stream, Provider, Dart, State Management

So, in Part 1 we saw how we can work with NotifyListeners using Consumer and Selector and the difference between them.

In this second part I will demonstrate a way to work with Stream using StreamProvider and handle changes on a specific value.

What is Stream?

In a nutshell, we can imagine Stream as a PIPE, where we can insert something at one end and take that value at the other. But how we can do this?

The StreamController provides the StreamSink class, allowing us to insert something inside the pipe (using the sink property) and getting the value that was inserted, the Stream, using the stream property.

Let’s create a class that will handle this. I will use the same example used in Part 1 but with some changes:

class MyModel { int _secondCount = 0; int _thirdCount = 0; int _value = 0; StreamController<int> _secondCountController = StreamController(); Stream<int> get secondCountStream => _secondCountController.stream; StreamController<int> _thirdCountController = StreamController(); Stream<int> get thirdCountStream => _thirdCountController.stream; StreamController<int> _valueController = StreamController(); Stream<int> get valueStream => _valueController.stream; void updateSecond() { this._secondCountController.sink.add(_secondCount++); } void updateThird() { this._thirdCountController.sink.add(_thirdCount++); } void changeValue() { this._value += 1; _valueController.sink.add(this._value); } void dispose() { this._secondCountController.close(); this._thirdCountController.close(); this._valueController.close(); } }

Let’s look at this code:

StreamController<int> _secondCountController = StreamController(); Stream<int> get secondCountStream => _secondCountController.stream; .

.

. this._secondCountController.sink.add(_secondCount++);

We created a StreamController that will provide a stream and sink properties and a Stream property that will be used by StreamProvider widget.

To add a value to the stream, we use the sink property:

this._secondCountController.sink.add(_secondCount++);

Remember to close the StreamController to avoid memory leaks:

void dispose() { this._secondCountController.close(); this._thirdCountController.close(); this._valueController.close(); }

The StreamProvider

The StreamProvider is a class that listens to a stream and exposes the value to its descendants.

StreamProvider<int>.value( initialData: 0, value: model.secondCountStream, child: SecondWidget(), ),

To work with it we just need to set:

the stream type “<int>”, in this case was an integer type;

the “initialData” that will be used until the stream emits a value;

and the “value” that is the stream that we will listen to.

In the “child” we will implement the widget that we want to update when the stream changes.

class SecondWidget extends StatelessWidget { @override Widget build(BuildContext context) { var model = Provider.of<MyModel>(context); return Column( children: <Widget>[ Text('Second Current Date :' + DateTime.now().toString(),), FlatButton( onPressed: () {model.updateSecond();}, child: Text("Increment Second: " + Provider.of<int>(context).toString()), ), ], ); } }

Using Provider.of<int>(context).toString() we can get the value emitted by the stream.

But, wait a minute!!

Why did you create a new widget to get the value emitted by the stream?

Are you asking me why I didn’t do this?

StreamProvider<int>.value( initialData: 0, value: model.secondCountStream, child: Column( children: <Widget>[ Text('Second Current Date :' + DateTime.now().toString(),), FlatButton( onPressed: () {model.updateSecond();}, child: Text("Increment Second: " + Provider.of<int>(context).toString()),), ], ), ),

For this question I’ll show an image that explains it:

The StreamProvider exposes the value to its descendants which means you can’t use the same “context” as StreamProvider. And a solution here is to create a new widget that will be used as a child of StreamProvider.

The full implementation:

void main() => runApp(MyApp()); class MyApp extends StatelessWidget { @override

Widget build(BuildContext context) { return Provider( create: (context) => new MyModel(), //DI child: MaterialApp( title: 'Provider with StreamProvider', theme: ThemeData(primarySwatch: Colors.blue,), home: MyHomePage(title: 'Home Page'), ), ); } } class MyHomePage extends StatefulWidget { MyHomePage({Key key, this.title}) : super(key: key); final String title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { MyModel model; @override Widget build(BuildContext context) { this.model = Provider.of<MyModel>(context); return Scaffold( appBar: AppBar( title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text('Date Without StreamProvider :' + DateTime.now().toString(),), StreamProvider<int>.value( initialData: 0, value: model.secondCountStream, child: SecondWidget(), ), StreamProvider<int>.value( value: model.thirdCountStream, child: ThirdWidget(), ), StreamProvider<int>.value( value: model.valueStream, child: ValueWidget(), ), ], ), ), ); } @override void dispose() { if(this.model != null) { model.dispose(); } super.dispose(); } } class SecondWidget extends StatelessWidget { @override Widget build(BuildContext context) { var model = Provider.of<MyModel>(context); return Column( children: <Widget>[ Text('Second Current Date :' + DateTime.now().toString(),), FlatButton( onPressed: () {model.updateSecond();}, child: Text("Increment Second: " + Provider.of<int>(context).toString()), ), ], ); } } class ThirdWidget extends StatelessWidget { @override Widget build(BuildContext context) { var model = Provider.of<MyModel>(context); return Column( children: <Widget>[ Text('Third Current Date :' + DateTime.now().toString(),), FlatButton( onPressed: () {model.updateThird();}, child: Text("Increment Third: " + Provider.of<int>(context).toString()), ), ], ); } }

class ValueWidget extends StatelessWidget { @override Widget build(BuildContext context) { var model = Provider.of<MyModel>(context); return Column( children: <Widget>[ Text('Listening a value :' + Provider.of<int>(context).toString(),), FlatButton( onPressed: () {model.changeValue();}, child: Text("Change Value"), ), ], ); } }

When you create a widget to manipulate each stream, it makes your code more readable and easier to maintain. I encourage you to always do this.

To learn more about the StreamProvider and another constructors, check out the documentation here.

Conclusion

It’s simple and straightforward to work with Stream and StreamProvider, and you don’t worry so much about rebuilding the widget tree. And of course, you can do more with that than shown here.

In these two parts, I have shown three ways to work with the Provider as State Management, Consumer, Selector, and StreamProvider, and each one of them has its benefits.

I always try to use the KISS principle and choose to use a simpler way, either in architecture or in componentization.

So, in my opinion, Selector is the simplest way to work with State Management, because we can listen to a specific value with a little less code than StreamProvider. However, working with ChangeNotifier has some limitations and you will need to create an architecture that circumvents this limitation.

I hope you enjoyed this article and “have a nice code”!

References