Using StreamBuilder in Flutter

Recently, I have been playing around with Flutter and Dart. This is just one of the few things about Flutter framework, that looked interesting to me.

One of the interesting things about Flutter is, in contrast to Android JVM, it blurs the line between Activity. Fragment and Views. Any time, you would need to display something to user, you are to provide a Widget. Those widgets can be nested into other widgets (just like views). The widgets care about visible elements only, which works nicely with architectures like MVI/MVP. If we use reactive streams, and provide all UI updates, the widgets play nicely with them. In fact, the reactive streams, are even easier to use in Flutter than in Java/Kotlin.

What is Stream Builder?

StreamBuilder is a Widget that can convert a stream of user defined objects, to widgets. This takes two arguments

  • A stream
  • A builder, that can convert the elements of the stream to widgets

Suppose, you have a Stream, that updates if there is any UI update (may be from user interaction, or may be resulted from network updates). If your “main” widget, includes a StreamBuilder, which listens to the stream, it can act as the element in charge of translating your states to views.

Lets create the views first

As I mentioned before, there is a view for every screen. So, I have a Login screen, a count screen and a list screen. There is also a splash screen, which I would get to later.

Flutter doesn’t have XML layouts like android, the views are more or less self explanatory:

class LoginScreen extends StatelessWidget {

@override
Widget build(BuildContext context) {
return new Scaffold(
body: new Center(
child: new RaisedButton(
onPressed: () {
emulateLogin();
},
child: new Text('Log In'),
)
),
primary: true,
);
}

emulateLogin() {
new StateSubject().update(new Count());
}
}

(the login screen, with one button with text “Log In”)

Lets create the states now

As I mentioned earlier, the stream builder would convert the states into screens. So, there needs to be three states:

enum State {
login, count, list
}

abstract class UiState {
State state;

UiState(this.state);

@override
String toString() => "State: $state";
}

login, count and list each extends UiState to append data, specific to the view.

Lets create the stream

It’s worth noting that dart streams/async support is pretty decent. However, I had difficulty working with them, since the stream is not buffered. I tried wrapping the stream inside a async method, which returns the last saved value, but that didn’t work for me either.

I managed to have that working with BehaviorSubject from rxdart. I created a singleton to provide the streams, which updates/extracts stream of UiState.

class StateSubject {
static final StateSubject _instance = new StateSubject._();

BehaviorSubject<UiState> _subject;

update(UiState state) => this._subject.add(state);

factory StateSubject() => StateSubject._instance;

StateSubject._() {
this._subject = new BehaviorSubject();
}
...
}

StreamBuilder

The StreamBuilder is my home widget.

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
StreamBuilder<uiState.UiState> builder = new StreamBuilder(
stream: new uiState.StateSubject().screenEvent(),
builder: (context, asyncSnapshot) {
if (asyncSnapshot.hasError) {
return new Text("Error!");
} else if (asyncSnapshot.data == null) {
return new SplashScreen();
}else {
switch (asyncSnapshot.data.state) {
case uiState.State.login: return new LoginScreen();
case uiState.State.count: return new CountScreen();
case uiState.State.list: return new NumberListScreen();
default: return null;
}
}
});
return new MaterialApp(
title: 'Stream Builder',
home: builder);
}
}

You can see, depending on the state, it would create LoginScreen, CountScreen or NumberListScreen. There us one issue I faced here, that is, for some reason, first call always had asyncSnapshot.data == null. To avoid that, I created the SplashScreen.

import 'package:flutter/material.dart';
import 'state.dart' as uiState;

class SplashScreen extends StatelessWidget {

@override
Widget build(BuildContext context) {
new uiState.StateSubject().switchToLogin();
return new Scaffold(body: new Center(child: new Text("Splash Screen")),);
}
}

My full code can be found here: https://github.com/sidky/flutter_stream_builder