mainstream
A StreamBuilder
alternative that provides builder and event callbacks.
See the Futuristic package for a similar API for working with Futures
.
Problem
StreamBuilder
is one of the most powerful widgets in Flutter. Like AnimatedBuilder
and ValueListenableBuilder
, it can be used to selectively rebuild only parts of a widget, which is very efficient.
However StreamBuilder
does not provide a way to receive callbacks for a Stream's data
/error
/done
events. Often we may want to respond to those events by performing an action. For example, updating a Navigator
. Doing this from inside the builder
callback is unreliable because Flutter can call the build()
method many times. So we need an additional stream listener. However this requires the boilerplate of creating a StatefulWidget
and canceling our stream subscription. Also, if our stream is not a broadcast stream (meaning it supports multiple listeners), we might have to avoid using a StreamBuilder
altogether.
class Home extends StatefulWidget {
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
StreamSubscription<FirebaseUser> _subscription;
@override
void initState() {
super.initState();
_subscription = FirebaseAuth.instance.onAuthStateChanged.listen((event) {
Navigator.of(context).popUntil((route) => route.isFirst);
});
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return StreamBuilder<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
builder: (_context, snapshot) => snapshot.data.isAnonymous ? LoggedOut() : LoggedIn(),
);
}
}
Solution
The MainStream
widget uses the same underlying StreamBuilderBase
used by StreamBuilder
but also exposes additional onData
/onError
/onDone
callbacks. These callbacks will not be retriggered as the result of a widget rebuild. It also provides builder callbacks to build mutually exclusive busy/data/error
widget states.
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MainStream<FirebaseUser>(
stream: FirebaseAuth.instance.onAuthStateChanged,
onData: (_) => Navigator.of(context).popUntil((r) => r.isFirst),
dataBuilder: (_, data) => data.isAnonymous ? LoggedOut() : LoggedIn(),
);
}
}
- Can be used in a
StatelessWidget
- Works with single-subscription and broadcast streams
- Provides generic type safety for the
data
provided to callbacks. The type parameter<T>
can be omitted if it can be inferred by the compiler.
Usage
final myStream = Stream<int>.periodic(Duration(seconds: 1), (x) => x).take(5);
class Home extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MainStream<int>(
stream: myStream,
onData: (data) => print(data),
onError: (error) => showDialog(...),
onDone: () => print('Done!'),
busyBuilder: (_) => CircularProgressIndicator(),
dataBuilder: (_, data) => Text(data.toString()),
errorBuilder: (_, error) => Text(error.toString()),
);
}
The stream
parameter is required.
The optional busyBuilder
displays a widget when the Stream
is waiting for its first event. By default, it shows a centered CircularProgressIndicator
.
The optional dataBuilder
displays a widget when the Stream's last event it a succesful data payload. The resulting T
value of the Stream<T>
is provided as a parameter to the callback.
The optional errorBuilder
displays a widget when the Stream
last event is an error, typically an Error
or Exception
.
The optional onData
callback can be used to handle a successful data event by displaying an alert dialog or performing navigation, for example. This can be used in place of or together with the dataBuilder
. This will not be retriggered as a result of a widget rebuild.
The optional onError
callback can be used to handle an error event by displaying an alert dialog or sending to a logging provider, for example. It can be used in place of or together with the errorBuilder
. This will not be retriggered as a result of a widget rebuild.
The optional onDone
callback can be used to handle a Stream's done event. This will not be retriggered as a result of a widget rebuild.