Use RxDart Streams with Flutter Hooks
Updated (November 2020)
There are many way to use Stream in Flutter and also many way to write the same code.
In this article we will see three different way to write a Counter App using Streams
, RxDart
and Flutter Hooks
.
Table of Contents
- What is a Stream?
- What are Flutter Hooks?
- Use Streams with Hooks
- Enhanced Streams with RxDart
- The final optimized result
- Additional Resources
What is a Stream
Streams are just a sequence of data that flows in a asynchronous way, in Flutter are widely use by many Widgets to listen for new data/changes to then rebuild part of the widget tree.
The most common Streams are:
Stream
: just listen for new data;StreamController
: allow to both listen and add/emit data to a stream
What are Flutter Hooks?
In short, Flutter Hooks are basically a simplify version of StatefulWidget
, they will handle the lifecycle of an Object inside the build method of a simple StatelessWidget
.
If you are familiar with React Native it will just take seconds to get started with Flutter Hooks, if you are not, don't worry, the concept is simple, but you still need to be careful when using it to avoid unnecessary rebuild of the widget tree.
Flutter hooks package already provide a full list of reusable hooks and creating Custom Hooks is so a simple task that can easily create new ones that fit our need.
In our case, we need a custom Hook, but first let's see some code should we?
Use Streams with Hooks
Let's start from an example, the classic counter app
, but implemented using Streams.
Pure Flutter
First we use a Pure Flutter approach, no third party library needed.
class CounterApp extends StatefulWidget {
const CounterApp({Key key})
: super(key: key);
@override
_CounterAppState createState() => _CounterAppState();
}
class _CounterAppState extends State<CounterApp> {
StreamController<int> controller;
int count = 0;
@override
void initState() {
super.initState();
controller = StreamController<int>.broadcast();
}
@override
void dispose() {
super.dispose();
controller.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter App'),
),
body: GestureDetector(
onTap: () => controller.add(count++),
child: StreamBuilder<int>(
stream: controller.stream,
initialData: 0,
builder: (context, snapshot) => Text('You tapped me ${snapshot.data} times.'),
),
),
);
}
}
We need to use a StatefulWidget
because we need to safely dispose
the StreamController, we don't want any memory leaks.
We also need a count
variable to store the current count since we can't access the current StreamController value inside the onTap
function.
Some people will be "fine" with this is code, it works, is not that verbose, but we can do better.
Using Flutter Hooks
Among the Flutter hooks example there is similar counter app that uses Stream and StreamController hooks, it also use shared_preferences
to store the counter value, but we don't need that for this example, so the simplified code will be like this:
import 'package:flutter_hooks/flutter_hooks.dart';
class CounterApp extends HookWidget {
const CounterApp({Key key})
: super(key: key);
@override
Widget build(BuildContext context) {
final controller = useStreamController<int>();
return Scaffold(
appBar: AppBar(
title: Text('Counter App'),
),
body: HookBuilder(
builder: (context) {
final count = useStream(controller.stream);
return GestureDetector(
onTap: () => controller.add(count.data + 1),
child: Text('You tapped me ${count.data} times.'),
);
}
),
);
}
}
We simplified a bit, we don't need to dispose the StreamController, Flutter Hooks will handle this for us.
We also don't need a count variable, we can use the useStream
, but we need to use HookBuilder
widget to avoid rebuilding the whole app when the stream changes.
Can we do even better? let's try 8)
Enhanced Streams with RxDart
There is another type of Stream, provided by the RxDart
package called BehaviorSubject
. If you are familiar with the Reactive Extensions for Async Programming
you might already know if not, this is a Stream with Memory
, once subscribed, it will emit the previous last value.
It also provide a way to access the current value of the stream.
In order to use it in a HookWidget, we need to create Custom Widget
import 'package:rxdart/rxdart.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
BehaviorSubject<T> useBehaviorStreamController<T>(
{bool sync = false,
VoidCallback onListen,
VoidCallback onCancel,
List<Object> keys}) {
return use(_BehaviorStreamControllerHook(
onCancel: onCancel,
onListen: onListen,
sync: sync,
keys: keys,
));
}
class _BehaviorStreamControllerHook<T> extends Hook<BehaviorSubject<T>> {
const _BehaviorStreamControllerHook(
{this.sync = false, this.onListen, this.onCancel, List<Object> keys})
: super(keys: keys);
final bool sync;
final VoidCallback onListen;
final VoidCallback onCancel;
@override
_BehaviorStreamControllerHookState<T> createState() =>
_BehaviorStreamControllerHookState<T>();
}
class _BehaviorStreamControllerHookState<T>
extends HookState<BehaviorSubject<T>, _BehaviorStreamControllerHook<T>> {
BehaviorSubject<T> _controller;
@override
void initHook() {
super.initHook();
_controller = BehaviorSubject<T>(
sync: hook.sync,
onCancel: hook.onCancel,
onListen: hook.onListen,
);
}
@override
void didUpdateHook(_BehaviorStreamControllerHook<T> oldHook) {
super.didUpdateHook(oldHook);
if (oldHook.onListen != hook.onListen) {
_controller.onListen = hook.onListen;
}
if (oldHook.onCancel != hook.onCancel) {
_controller.onCancel = hook.onCancel;
}
}
@override
BehaviorSubject<T> build(BuildContext context) {
return _controller;
}
@override
void dispose() {
_controller.close();
}
@override
String get debugLabel => 'useBehaviorStreamController';
}
We create a new hooks called useBehaviorStreamController
, the code is exactly like the useStreamController
hooks, but it use the RxDart BehaviorSubject
instead of the Flutter StreamController
.
The final optimized result
With is useBehaviorStreamController hooks, we can write our counter app in a more compact way:
import 'package:flutter_hooks/flutter_hooks.dart';
class CounterApp extends HookWidget {
const CounterApp({Key key})
: super(key: key);
@override
Widget build(BuildContext context) {
final controller = useBehaviorStreamController<int>();
return Scaffold(
appBar: AppBar(
title: Text('Counter App'),
),
body: GestureDetector(
onTap: () => controller.add(controller.value + 1),
child: StreamBuilder<int>(
stream: controller.stream,
initialData: 0,
builder: (context, snapshot) => Text('You tapped me ${snapshot.data} times.'),
),
),
);
}
}
We are using a StreamBuilder, but this time in the onTap
function we can directly access the current value of the stream.
With this last optimization our code now is much shorter, we don't need the HookBuilder and we will just rebuild the Text widget using a StreamBuilder.