diff --git a/examples/flutter/github_search/lib/search_bloc.dart b/examples/flutter/github_search/lib/search_bloc.dart index 85748512..762574a8 100644 --- a/examples/flutter/github_search/lib/search_bloc.dart +++ b/examples/flutter/github_search/lib/search_bloc.dart @@ -7,7 +7,7 @@ import 'search_state.dart'; class SearchBloc { final Sink onTextChanged; - final Stream state; + final ValueStream state; factory SearchBloc(GithubApi api) { final onTextChanged = PublishSubject(); @@ -23,7 +23,9 @@ class SearchBloc { // to the View. .switchMap((String term) => _search(term, api)) // The initial state to deliver to the screen. - .startWith(SearchNoTerm()); + .startWith(SearchNoTerm()) + .publishValueSeeded(SearchNoTerm()) + ..connect(); return SearchBloc._(onTextChanged, state); } diff --git a/examples/flutter/github_search/lib/search_widget.dart b/examples/flutter/github_search/lib/search_widget.dart index 771b9d16..0114044b 100644 --- a/examples/flutter/github_search/lib/search_widget.dart +++ b/examples/flutter/github_search/lib/search_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:rxdart_flutter/rxdart_flutter.dart'; import 'empty_result_widget.dart'; import 'github_api.dart'; @@ -44,11 +45,9 @@ class SearchScreenState extends State { @override Widget build(BuildContext context) { - return StreamBuilder( + return RxStreamBuilder( stream: bloc.state, - initialData: SearchNoTerm(), - builder: (BuildContext context, AsyncSnapshot snapshot) { - final state = snapshot.requireData; + builder: (context, state) { return Scaffold( appBar: AppBar( title: const Text('RxDart Github Search'), diff --git a/examples/flutter/github_search/pubspec.lock b/examples/flutter/github_search/pubspec.lock index da633d69..cdfbecce 100644 --- a/examples/flutter/github_search/pubspec.lock +++ b/examples/flutter/github_search/pubspec.lock @@ -410,6 +410,13 @@ packages: relative: true source: path version: "0.28.0-dev.2" + rxdart_flutter: + dependency: "direct main" + description: + path: "../../../packages/rxdart_flutter" + relative: true + source: path + version: "0.0.1" shelf: dependency: transitive description: diff --git a/examples/flutter/github_search/pubspec.yaml b/examples/flutter/github_search/pubspec.yaml index 57343b7a..51fa756d 100644 --- a/examples/flutter/github_search/pubspec.yaml +++ b/examples/flutter/github_search/pubspec.yaml @@ -11,6 +11,8 @@ dependencies: sdk: flutter rxdart: path: ../../../packages/rxdart + rxdart_flutter: + path: ../../../packages/rxdart_flutter http: ^0.13.3 flutter_spinkit: ^5.1.0 diff --git a/packages/rxdart_flutter/lib/rxdart_flutter.dart b/packages/rxdart_flutter/lib/rxdart_flutter.dart index 82a1cfaf..287a1dcb 100644 --- a/packages/rxdart_flutter/lib/rxdart_flutter.dart +++ b/packages/rxdart_flutter/lib/rxdart_flutter.dart @@ -1,7 +1,3 @@ library rxdart_flutter; -/// A Calculator. -class Calculator { - /// Returns [value] plus 1. - int addOne(int value) => value + 1; -} +export 'src/rx_stream_builder.dart'; diff --git a/packages/rxdart_flutter/lib/src/rx_stream_builder.dart b/packages/rxdart_flutter/lib/src/rx_stream_builder.dart new file mode 100644 index 00000000..83e73140 --- /dev/null +++ b/packages/rxdart_flutter/lib/src/rx_stream_builder.dart @@ -0,0 +1,165 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:rxdart/rxdart.dart'; + +bool _defaultBuildWhen(Object? previous, Object? current) => + previous != current; + +/// Signature for strategies that build widgets based on asynchronous interaction. +typedef RxWidgetBuilder = Widget Function(BuildContext context, T data); + +/// Signature for the `initialData` function which takes no arguments and returns +/// the initial data in case the stream has no value. +typedef InitialData = T Function(); + +/// Signature for the `buildWhen` function which takes the previous `data` and +/// the current `data` and is responsible for returning a [bool] which +/// determines whether to rebuild [ValueStream] with the current `data`. +typedef RxStreamBuilderCondition = bool Function(S previous, S current); + +/// Rx stream builder that will pre-populate the streams initial data if the +/// given stream is an stream that holds the streams current value such +/// as a [ValueStream] or a [ReplayStream] +class RxStreamBuilder extends StatefulWidget { + final RxWidgetBuilder _builder; + final ValueStream _stream; + final InitialData? _initialData; + final RxStreamBuilderCondition? _buildWhen; + + /// Creates a new [RxStreamBuilder] 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. + /// See [StreamBuilder.initialData]. + /// + /// The [builder] must not be null. It must only return a widget and should not have any side + /// effects as it may be called multiple times. + const RxStreamBuilder({ + Key? key, + required ValueStream stream, + required RxWidgetBuilder builder, + InitialData? initialData, + RxStreamBuilderCondition? buildWhen, + }) : _builder = builder, + _stream = stream, + _initialData = initialData, + _buildWhen = buildWhen, + super(key: key); + + @override + State> createState() => _RxStreamBuilderState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty>('stream', _stream)) + ..add(ObjectFlagProperty>.has('builder', _builder)) + ..add( + ObjectFlagProperty?>.has( + 'buildWhen', + _buildWhen, + ), + ) + ..add( + ObjectFlagProperty?>.has( + 'initialData', + _initialData, + ), + ); + } + + /// Get latest value from stream or throw an [ArgumentError]. + @visibleForTesting + static T getInitialData( + ValueStream stream, InitialData? initialData) { + if (stream.hasValue) { + return stream.value; + } + if (initialData != null) { + return initialData(); + } + throw ArgumentError.value(stream, 'stream', 'has no value'); + } +} + +class _RxStreamBuilderState extends State> { + late T currentData; + StreamSubscription? subscription; + + @override + void initState() { + super.initState(); + subscribe(); + } + + @override + void didUpdateWidget(covariant RxStreamBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget._stream != widget._stream) { + unsubscribe(); + subscribe(); + } + } + + @override + void dispose() { + unsubscribe(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => widget._builder(context, currentData); + + void subscribe() { + final stream = widget._stream; + + try { + currentData = RxStreamBuilder.getInitialData(stream, widget._initialData); + } catch (e) { + FlutterError.reportError( + FlutterErrorDetails( + exception: e, + stack: StackTrace.current, + library: 'rxdart_flutter', + ), + ); + return; + } + + final buildWhen = widget._buildWhen ?? _defaultBuildWhen; + + assert(subscription == null, 'Stream already subscribed'); + subscription = stream.listen( + (data) { + if (buildWhen(currentData, data)) { + setState(() => currentData = data); + } + }, + onError: (Object e, StackTrace s) { + FlutterError.reportError( + FlutterErrorDetails( + exception: e, + stack: s, + library: 'rxdart_flutter', + ), + ); + }, + ); + } + + void unsubscribe() { + subscription?.cancel(); + subscription = null; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty.lazy('currentData', () => currentData)); + properties.add(DiagnosticsProperty('subscription', subscription)); + } +} diff --git a/packages/rxdart_flutter/pubspec.yaml b/packages/rxdart_flutter/pubspec.yaml index b4a7fbec..0f4f9fe1 100644 --- a/packages/rxdart_flutter/pubspec.yaml +++ b/packages/rxdart_flutter/pubspec.yaml @@ -4,12 +4,13 @@ version: 0.0.1 homepage: environment: - sdk: '>=3.1.3 <4.0.0' - flutter: ">=1.17.0" + sdk: '>=2.14.0 <3.0.0' + flutter: '>=2.5.0' dependencies: flutter: sdk: flutter + rxdart: ^0.28.0-dev.2 dev_dependencies: flutter_test: diff --git a/packages/rxdart_flutter/test/rxdart_flutter_test.dart b/packages/rxdart_flutter/test/rxdart_flutter_test.dart deleted file mode 100644 index 8d4c64fa..00000000 --- a/packages/rxdart_flutter/test/rxdart_flutter_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:rxdart_flutter/rxdart_flutter.dart'; - -void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); - }); -}