Last active
November 7, 2022 03:07
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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