Skip to content

Instantly share code, notes, and snippets.

@j05u3
Last active November 7, 2022 03:07
Show Gist options
  • Save j05u3/bee27ec0808f28949a989dbdbf4fe5ac to your computer and use it in GitHub Desktop.
Save j05u3/bee27ec0808f28949a989dbdbf4fe5ac to your computer and use it in GitHub Desktop.
Lifecycle aware stream builder (flutter). Find more details on https://medium.com/p/a2ae7244af32
import 'dart:async';
import 'package:flutter/widgets.dart';
abstract class StreamBuilderBase<T, S> extends StatefulWidget {
/// Creates a [StreamBuilderBase] connected to the specified [stream].
const StreamBuilderBase({ Key key, this.stream }) : super(key: key);
/// The asynchronous computation to which this builder is currently connected,
/// possibly null. When changed, the current summary is updated using
/// [afterDisconnected], if the previous stream was not null, followed by
/// [afterConnected], if the new stream is not null.
final Stream<T> stream;
/// Returns the initial summary of stream interaction, typically representing
/// the fact that no interaction has happened at all.
///
/// Sub-classes must override this method to provide the initial value for
/// the fold computation.
S initial();
/// Returns an updated version of the [current] summary reflecting that we
/// are now connected to a stream.
///
/// The default implementation returns [current] as is.
S afterConnected(S current) => current;
/// Returns an updated version of the [current] summary following a data event.
///
/// Sub-classes must override this method to specify how the current summary
/// is combined with the new data item in the fold computation.
S afterData(S current, T data);
/// Returns an updated version of the [current] summary following an error.
///
/// The default implementation returns [current] as is.
S afterError(S current, Object error) => current;
/// Returns an updated version of the [current] summary following stream
/// termination.
///
/// The default implementation returns [current] as is.
S afterDone(S current) => current;
/// Returns an updated version of the [current] summary reflecting that we
/// are no longer connected to a stream.
///
/// The default implementation returns [current] as is.
S afterDisconnected(S current) => current;
/// Returns a Widget based on the [currentSummary].
Widget build(BuildContext context, S currentSummary);
@override
State<StreamBuilderBase<T, S>> createState() => _StreamBuilderBaseState<T, S>();
}
/// State for [StreamBuilderBase].
class _StreamBuilderBaseState<T, S> extends State<StreamBuilderBase<T, S>>
with WidgetsBindingObserver {
StreamSubscription<T> _subscription;
S _summary;
@override
void initState() {
super.initState();
_summary = widget.initial();
_subscribe();
WidgetsBinding.instance.addObserver(this);
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (_subscription == null) return;
if (state == AppLifecycleState.paused) {
_subscription.pause();
} else if (state == AppLifecycleState.resumed) {
_subscription.resume();
}
}
@override
void didUpdateWidget(StreamBuilderBase<T, S> oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.stream != widget.stream) {
if (_subscription != null) {
_unsubscribe();
_summary = widget.afterDisconnected(_summary);
}
_subscribe();
}
}
@override
Widget build(BuildContext context) => widget.build(context, _summary);
@override
void dispose() {
_unsubscribe();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
void _subscribe() {
if (widget.stream != null) {
_subscription = widget.stream.listen((T data) {
setState(() {
_summary = widget.afterData(_summary, data);
});
}, onError: (Object error) {
setState(() {
_summary = widget.afterError(_summary, error);
});
}, onDone: () {
setState(() {
_summary = widget.afterDone(_summary);
});
});
_summary = widget.afterConnected(_summary);
}
}
void _unsubscribe() {
if (_subscription != null) {
_subscription.cancel();
_subscription = null;
}
}
}
class LifecycleAwareStreamBuilder<T> extends StreamBuilderBase<T, AsyncSnapshot<T>> {
/// Creates a new [LifecycleAwareStreamBuilder] that builds itself based on the latest
/// snapshot of interaction with the specified [stream] and whose build
/// strategy is given by [builder].
///
/// The [initialData] is used to create the initial snapshot.
///
/// The [builder] must not be null.
const LifecycleAwareStreamBuilder({
Key key,
this.initialData,
Stream<T> stream,
@required this.builder,
}) : assert(builder != null),
super(key: key, stream: stream);
/// The build strategy currently used by this builder.
final AsyncWidgetBuilder<T> builder;
/// The data that will be used to create the initial snapshot.
///
/// Providing this value (presumably obtained synchronously somehow when the
/// [Stream] was created) ensures that the first frame will show useful data.
/// Otherwise, the first frame will be built with the value null, regardless
/// of whether a value is available on the stream: since streams are
/// asynchronous, no events from the stream can be obtained before the initial
/// build.
final T initialData;
@override
AsyncSnapshot<T> initial() => AsyncSnapshot<T>.withData(ConnectionState.none, initialData);
@override
AsyncSnapshot<T> afterConnected(AsyncSnapshot<T> current) => current.inState(ConnectionState.waiting);
@override
AsyncSnapshot<T> afterData(AsyncSnapshot<T> current, T data) {
return AsyncSnapshot<T>.withData(ConnectionState.active, data);
}
@override
AsyncSnapshot<T> afterError(AsyncSnapshot<T> current, Object error) {
return AsyncSnapshot<T>.withError(ConnectionState.active, error);
}
@override
AsyncSnapshot<T> afterDone(AsyncSnapshot<T> current) => current.inState(ConnectionState.done);
@override
AsyncSnapshot<T> afterDisconnected(AsyncSnapshot<T> current) => current.inState(ConnectionState.none);
@override
Widget build(BuildContext context, AsyncSnapshot<T> currentSummary) => builder(context, currentSummary);
}
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:turuta/widgets/functional/lifecycle_aware_stream_builder.dart';
class StreamTestScreen extends StatefulWidget {
@override
State createState() => new _StreamTestScreenState();
}
class _StreamTestScreenState extends State<StreamTestScreen> {
@override
Widget build(BuildContext context) {
final stream = Stream.periodic(Duration(seconds: 5)).asyncMap((_) async {
// it was tested that the lifecycle aware stream made this occur only when the app is in foreground
print("API call done");
// you can call your server here
return "API results";
});
return SafeArea(
child: Stack(
children: <Widget>[
Container(
color: Colors.white,
),
LifecycleAwareStreamBuilder<String>(
stream: stream,
builder: (context, sn) {
if (sn.hasData) {
return Text(sn.data);
}
return Container();
},
),
],
)
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment