Use RxDart Streams with Flutter Hooks

Thomas Gazzoni

5 min read

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#

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.

with_pure_flutter.darts
class CounterApp extends StatefulWidget {
  const CounterApp({Key key})
      : super(key: key);

  
  _CounterAppState createState() => _CounterAppState();
}

class _CounterAppState extends State<CounterApp> {
  StreamController<int> controller;
  int count = 0;

  
  void initState() {
    super.initState();
    controller = StreamController<int>.broadcast();
  }

  
  void dispose() {
    super.dispose();
    controller.dispose();
  }

  
  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:

with_flutter_hooks.dart
import 'package:flutter_hooks/flutter_hooks.dart';

class CounterApp extends HookWidget {
  const CounterApp({Key key})
      : super(key: key);

  
  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

use_behavior_stream_controller_hook.dart
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;

  
  _BehaviorStreamControllerHookState<T> createState() =>
      _BehaviorStreamControllerHookState<T>();
}

class _BehaviorStreamControllerHookState<T>
    extends HookState<BehaviorSubject<T>, _BehaviorStreamControllerHook<T>> {
  BehaviorSubject<T> _controller;

  
  void initHook() {
    super.initHook();
    _controller = BehaviorSubject<T>(
      sync: hook.sync,
      onCancel: hook.onCancel,
      onListen: hook.onListen,
    );
  }

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

  
  BehaviorSubject<T> build(BuildContext context) {
    return _controller;
  }

  
  void dispose() {
    _controller.close();
  }

  
  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:

with_behavior_stream_controller.dart
import 'package:flutter_hooks/flutter_hooks.dart';

class CounterApp extends HookWidget {
  const CounterApp({Key key})
      : super(key: key);

  
  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.

Additional Resources#


Subscribe to the newsletter