RxDart Tutorial for Flutter: Getting Started

Learn how to develop Flutter apps using the Reactive Programming paradigm with RxDart. By Ayush.

3 (2) · 1 Review

Download materials
Save for later

Developing highly responsive, fault-tolerant, event-driven asynchronous applications that are scalable requires a different way of thinking from the traditional synchronous programming architecture.

This is why the reactive programming principle exists. It uses streams to build these types of reactive applications.

In this tutorial, you’ll use RxDart, a Dart implementation of the reactive programming principle, to develop ‘Gravity Pop’, a very simple game based off a very popular block falling videogame. In this game, blocks fall and disappear if a row is filled Along the way, you’ll learn:

  • RxDart streams.
  • Extension functions.
  • Subjects.
  • The concept of Backpressure.
Note: This tutorial assumes a basic knowledge of Dart and Flutter. You can learn these topics from our Getting Started With Flutter and Your First Flutter App: an App From Scratch tutorials.

Getting Started

Click the Download Materials button at the top or bottom of this tutorial to download the starter project. The starter project has a few models to represent the various states/events you’ll be manipulating using RxDart. These are:

  • GameState: An enum to store the game state: whether the game has started, is playing or is over.
  • GameData: A model that represents the current state of the game and all the collected tetriminos.
  • Input: A model representing change events that affect the current tetrimino in play. This includes xOffset, yOffset and angle.
  • Piece: An enum that represents the type of block from the available seven.
  • Tetrimino: represents the current block animating in the screen.
Note: A group of falling blocks is also known as a tetrimino.

Along with the models, here are some other things to note about the starter project:

  • The project contains utility functions for common use cases.
  • There are classes for the board, the player and a layer that only handles user interactions.
  • We paint each tetrimino using the custom painter class.

Now, get packages by running flutter pub get in your terminal/PowerShell. Then build and run to see the starter app:

Starter screen for app

Tap the Play button to enter play mode. You’ll see the screen below:
Gravity pop game start screen

You’ll notice that it only contains a white screen; there are no blocks, and the buttons don’t work. You’ll change that and make this into a simple game using Reactive streams in RxDart.

At the end, you will get to see a game like this:
The final game of Gravity Pop

Are you ready? Time to get stacking.

The Reactive Programming Paradigm

An event is a general term used to represent the press of a button or any of the many sensors in your device recording their data. Various components process events to produce something meaningful in the end, to make updates in the UI, for instance.

But this process is not a one-time deal. As long as events are produced, the process repeats itself. A “stream” in programming is this flow of any raw input event to a useful result:

A red dot moving between two big black circles

Above is a basic illustration of a stream showing relation between a source and a sink.

All streams have two required components: a source (for example, an event) and a sink or receiver (a logical or UI component that can consume that stream.) Reactive programming uses such streams as a backbone to build applications.

Flutter offers built-in support for streams using the Stream class. This class handles creating streams from various sources such as I/O updates, sensor data capture, UI event capture and so much more.

A sink can be any component that has the power to consume stream events. Moving forward, you’ll use StreamBuilder to consume streams and update UI.

For your first task, open lib/player.dart and replace //TODO: add a stream builder with the following code snippet:

child: StreamBuilder<Tetrimino>(
  //TODO: replace with staticPlayerStream
  stream: _engine.blankPlayerStream,
  builder: ((context, snapshot) {
    if (snapshot.hasData) {
      if (snapshot.data?.current == Piece.Empty)
        return const SizedBox.shrink();

      return ClipRect(
        child: CustomPaint(
              _getNextPiece(snapshot.data!, _engine.extent.toDouble()),
          child: SizedBox(
            width: _engine.effectiveWidth.toDouble(),
            height: _engine.effectiveHeight.toDouble(),
    return const SizedBox.shrink();

Here’s what’s happening in the preceding code snippets:

  1. You used Flutter StreamBuilder as a sink to accept events from a stream, _engine.blankPlayerStream.
  2. _engine.blankPlayerStream creates a stream that returns an empty stream at the moment. You’ll expand on this as you get further into the tutorial.
  3. The snapshot variable references stream events and processes them. In this case, you use each event in a Text widget.

Build and run the app and you will see the same white board. But now the building blocks for streams are in place with the source(stream instances) and sink(StreamBuilder widget).

 Current state of the app with the stream schematic to the side At this point, it may seem trivial, but as you go on you will implement more features of the reactive programming paradigm using RxDart.

Creating RxDart Streams

Dart Stream API fulfils the basic requirements to follow the reactive programming style. RxDart offers various new features that increase its usefulness in real-world applications to enhance it more.

As an example of this, consider the Stream.fromIterable() constructor returned from gridStateStream in lib/engine.dart. As simple as it may be, you can improve it further by specifying the start and end of the iterable and turn each numbers in between into events.

The RangeStream stream from RxDart fulfills this exact usecase. Open lib/player.dart and replace TODO: replace with staticPlayerStream and the line beneath it with the following code:

//TODO: replace with animatingPlayerStream
stream: _engine.staticPlayerStream(),

Next, open lib/engine.dart and take a look at the implementation for staticPlayerStream:

return RangeStream(0, effectiveHeight ~/ extent - 1).map((value) =>
        current: Piece.I, origin: Point(0, value * extent.toDouble())));

RangeStream emits numbers within its input range as events. Each event is “mapped” into a tetrimino object in the above code snippet.

Build and run the app and you will see that since RangeStream has completed emitting all its events, the block has come to rest at the bottom of the board which is represented by the final event emitted by RangeStream.
Current state of the app with stream schematic updated with new source

Along with RangeStream, RxDart also offers various other types of “pre-built” streams. You’ll see more of them as you continue with this tutorial.

Using RxDart Extensions

The map function at the end of the code above is an example of an extension function. Extension functions extend the operations you can apply on streams. You can use them when you need to manipulate an event before it reaches the sink.

For example, what if you only need odd events from a stream that emits integer values? Or, you need to create a gap between each event emitted in a stream.

That’s what map does in the previous code. It takes each integer and transforms it into a tetrimino. These functions have the power to transform the emitted events in any shape or form and can even create other streams from the events.

Note: A higher-order stream is a special stream that emits other streams.

Let’s make the previous example a bit more exciting using interval. The interval function emits events of the stream after the specified time interval. Open lib/player.dart and replace //TODO: replace with animatingPlayerStream and the code statement below it with the following:

//TODO: replace with animatingPlayerWithCompletionStream
stream: _engine.animatingPlayerStream(),

Check staticPlayerStream in lib/engine.dart to see how you used the interval extension.

return RangeStream(0, effectiveHeight ~/ extent - 1)
    .interval(const Duration(milliseconds: 500))
    .map((value) => Tetrimino(
        current: Piece.I, origin: Point(0, value * extent.toDouble())));

The only difference between this RangeStream and the previous stream is the addition of the interval extension that emits each event from its input stream in predefined intervals, 500 milliseconds in this case.

Build and run the app now. You will now see the block fall in intervals of 500 ms.

A stream schematic showing the effect of RxDart extensions

As you can see, you’re not limited to just one extension function between the source and a sink. You can chain multiple extension functions to manipulate the data between the source and the sink. Chaining various functions one after the other is an important part of reactive programming, used to achieve high degrees of data manipulation.