RxDart Tutorial for Flutter: Getting Started
Learn how to develop Flutter apps using the Reactive Programming paradigm with RxDart. By Ayush.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
RxDart Tutorial for Flutter: Getting Started
25 mins
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.
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 includesxOffset
,yOffset
andangle
. -
Piece
: An enum that represents the type of block from the available seven. -
Tetrimino
: represents the current block animating in the screen.
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:
Tap the Play button to enter play mode. You’ll see the screen below:
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:
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:
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:
//1
child: StreamBuilder<Tetrimino>(
//2
//TODO: replace with staticPlayerStream
stream: _engine.blankPlayerStream,
//3
builder: ((context, snapshot) {
if (snapshot.hasData) {
if (snapshot.data?.current == Piece.Empty)
return const SizedBox.shrink();
return ClipRect(
child: CustomPaint(
painter:
_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:
- You used Flutter
StreamBuilder
as a sink to accept events from a stream,_engine.blankPlayerStream
. -
_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. - The
snapshot
variable references stream events and processes them. In this case, you use each event in aText
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).
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) =>
Tetrimino(
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
.
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.
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.
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.