diff --git a/.github/workflows/flutter-example.yml b/.github/workflows/flutter-example.yml index 4fb458a0..00ea1f84 100644 --- a/.github/workflows/flutter-example.yml +++ b/.github/workflows/flutter-example.yml @@ -22,7 +22,7 @@ jobs: distribution: 'zulu' java-version: '17' - - uses: subosito/flutter-action@v2.4.0 + - uses: subosito/flutter-action@v2.18.0 with: channel: 'stable' diff --git a/.github/workflows/rxdart-test.yml b/.github/workflows/rxdart-test.yml index 328d7130..1dab92f6 100644 --- a/.github/workflows/rxdart-test.yml +++ b/.github/workflows/rxdart-test.yml @@ -18,31 +18,42 @@ jobs: strategy: matrix: - sdk: [ beta, stable, 2.17.0, 2.15.0, 2.12.0 ] + # sdk: [ beta, stable, 2.17.0, 2.15.0, 2.12.0 ] + flutter: [ beta, stable, 3.0.0, 2.8.0, 2.0.1 ] steps: - uses: actions/checkout@v4 - - name: Setup Dart - uses: dart-lang/setup-dart@v1.6.0 + - name: Setup Stable/Beta Flutter/Dart + if: ${{ matrix.flutter == 'stable' || matrix.flutter == 'beta' }} + uses: subosito/flutter-action@v2.18.0 with: - sdk: ${{ matrix.sdk }} + channel: ${{ matrix.flutter }} + + - name: Setup Older Flutter/Dart + if: ${{ matrix.flutter != 'stable' && matrix.flutter != 'beta' }} + uses: subosito/flutter-action@v2.18.0 + with: + flutter-version: ${{ matrix.flutter }} - name: Install melos run: dart pub global activate melos - - name: Print Dart version + - name: Print Dart SDK version run: dart --version + - name: Print Flutter SDK version + run: flutter --version + - name: Install dependencies run: melos run pub-get-no-private - name: Analyze - if: ${{ matrix.sdk == 'stable' }} + if: ${{ matrix.flutter == 'stable' }} run: melos run analyze-no-private - name: Format code - if: ${{ matrix.sdk == 'stable' }} + if: ${{ matrix.flutter == 'stable' }} run: melos run format-no-private - name: Active coverage @@ -52,4 +63,4 @@ jobs: run: melos run test-rxdart - uses: codecov/codecov-action@v3.1.6 - if: ${{ matrix.sdk == 'stable' }} + if: ${{ matrix.flutter == 'stable' }} diff --git a/examples/fibonacci/pubspec.lock b/examples/fibonacci/pubspec.lock index a74be322..753378cc 100644 --- a/examples/fibonacci/pubspec.lock +++ b/examples/fibonacci/pubspec.lock @@ -15,6 +15,6 @@ packages: path: "../../packages/rxdart" relative: true source: path - version: "0.28.0-dev.2" + version: "0.28.0" sdks: dart: ">=3.1.0 <4.0.0" diff --git a/examples/flutter/github_search/lib/search_screen.dart b/examples/flutter/github_search/lib/search_screen.dart index 4bf5b9aa..6e00f688 100644 --- a/examples/flutter/github_search/lib/search_screen.dart +++ b/examples/flutter/github_search/lib/search_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:rxdart_flutter/rxdart_flutter.dart'; import 'api/github_api.dart'; import 'bloc/search_bloc.dart'; @@ -44,11 +45,9 @@ class SearchScreenState extends State { @override Widget build(BuildContext context) { - return StreamBuilder( + return ValueStreamBuilder( stream: bloc.state, - initialData: bloc.state.value, - 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 6c90c08d..8c1d0881 100644 --- a/examples/flutter/github_search/pubspec.lock +++ b/examples/flutter/github_search/pubspec.lock @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -319,18 +319,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -367,18 +367,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" mime: dependency: transitive description: @@ -458,6 +458,13 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.0" + rxdart_flutter: + dependency: "direct main" + description: + path: "../../../packages/rxdart_flutter" + relative: true + source: path + version: "0.0.1" shelf: dependency: transitive description: @@ -494,7 +501,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: @@ -531,10 +538,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -555,10 +562,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" term_glyph: dependency: transitive description: @@ -571,26 +578,26 @@ packages: dependency: "direct dev" description: name: test - sha256: "7ee446762c2c50b3bd4ea96fe13ffac69919352bd3b4b17bac3f3465edc58073" + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" url: "https://pub.dev" source: hosted - version: "1.25.2" + version: "1.25.8" test_api: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.3" test_core: dependency: transitive description: name: test_core - sha256: "2bc4b4ecddd75309300d8096f781c0e3280ca1ef85beda558d33fcbedc2eead4" + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" url: "https://pub.dev" source: hosted - version: "0.6.0" + version: "0.6.5" timing: dependency: transitive description: @@ -619,10 +626,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.3.0" watcher: dependency: transitive description: diff --git a/examples/flutter/github_search/pubspec.yaml b/examples/flutter/github_search/pubspec.yaml index 5f87ab5b..1f870d63 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 rxdart_ext: ^0.3.0 diff --git a/examples/web/pubspec.lock b/examples/web/pubspec.lock index 15469723..2b0e82cc 100644 --- a/examples/web/pubspec.lock +++ b/examples/web/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: build_modules - sha256: "9987d67a29081872e730468295fc565e9a2b377ca3673337c1d4e41d57c6cd7c" + sha256: "0327cb2a9eefba197b63f71872c38bafe4c63b331797a43618d6c270516a6447" url: "https://pub.dev" source: hosted - version: "5.0.8" + version: "5.0.11" build_resolvers: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: "direct dev" description: name: build_web_compilers - sha256: "9071a94aa67787cebdd9e76837c9d2af61fb5242db541244f6a0b6249afafb46" + sha256: "01cfac85e835d9fe8e87928958586ddbd477d98cfac5af2442cb2df6bbb42dff" url: "https://pub.dev" source: hosted - version: "4.0.10" + version: "4.1.1" built_collection: dependency: transitive description: @@ -489,4 +489,4 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <3.6.0" + dart: ">=3.6.0 <3.8.0-z" diff --git a/packages/rxdart_flutter/.gitignore b/packages/rxdart_flutter/.gitignore new file mode 100644 index 00000000..96486fd9 --- /dev/null +++ b/packages/rxdart_flutter/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/rxdart_flutter/.metadata b/packages/rxdart_flutter/.metadata new file mode 100644 index 00000000..eea17bc4 --- /dev/null +++ b/packages/rxdart_flutter/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "2f708eb8396e362e280fac22cf171c2cb467343c" + channel: "stable" + +project_type: package diff --git a/packages/rxdart_flutter/CHANGELOG.md b/packages/rxdart_flutter/CHANGELOG.md new file mode 100644 index 00000000..41cc7d81 --- /dev/null +++ b/packages/rxdart_flutter/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/packages/rxdart_flutter/LICENSE b/packages/rxdart_flutter/LICENSE new file mode 100644 index 00000000..ba75c69f --- /dev/null +++ b/packages/rxdart_flutter/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/packages/rxdart_flutter/README.md b/packages/rxdart_flutter/README.md new file mode 100644 index 00000000..02fe8eca --- /dev/null +++ b/packages/rxdart_flutter/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/rxdart_flutter/analysis_options.yaml b/packages/rxdart_flutter/analysis_options.yaml new file mode 100644 index 00000000..e4c73f53 --- /dev/null +++ b/packages/rxdart_flutter/analysis_options.yaml @@ -0,0 +1,9 @@ +include: package:flutter_lints/flutter.yaml +linter: + rules: + - prefer_final_locals + - prefer_relative_imports + - always_declare_return_types # https://github.com/dart-lang/lints#migrating-from-packagepedantic + - prefer_single_quotes # https://github.com/dart-lang/lints#migrating-from-packagepedantic + - unawaited_futures # https://github.com/dart-lang/lints#migrating-from-packagepedantic + - unsafe_html # https://github.com/dart-lang/lints#migrating-from-packagepedantic \ No newline at end of file diff --git a/packages/rxdart_flutter/lib/rxdart_flutter.dart b/packages/rxdart_flutter/lib/rxdart_flutter.dart new file mode 100644 index 00000000..d41e62e4 --- /dev/null +++ b/packages/rxdart_flutter/lib/rxdart_flutter.dart @@ -0,0 +1,3 @@ +library rxdart_flutter; + +export 'src/value_stream_builder.dart'; diff --git a/packages/rxdart_flutter/lib/src/value_stream_builder.dart b/packages/rxdart_flutter/lib/src/value_stream_builder.dart new file mode 100644 index 00000000..03edbfb4 --- /dev/null +++ b/packages/rxdart_flutter/lib/src/value_stream_builder.dart @@ -0,0 +1,243 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:rxdart/rxdart.dart'; + +/// Signature for the `builder` function which takes the `BuildContext` and the current `data` +/// and is responsible for returning a widget which is to be rendered. +/// This is analogous to the `builder` function in [StreamBuilder]. +typedef ValueStreamWidgetBuilder = Widget Function( + BuildContext context, T data); + +/// 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 ValueStreamBuilderCondition = bool Function(S previous, S current); + +/// {@template value_stream_builder} +/// Similar to [StreamBuilder], but works with [ValueStream], +/// and with only-data events, not error events. +/// +/// [ValueStreamBuilder] handles building a widget in response to new `data`. +/// [ValueStreamBuilder] is analogous to [StreamBuilder] but has simplified API to +/// reduce the amount of boilerplate code needed as well as [ValueStream]-specific +/// performance improvements. +/// +/// ```dart +/// final valueStream = BehaviorSubject.seeded(0); +/// +/// ValueStreamBuilder( +/// stream: valueStream, +/// builder: (context, data) { +/// // return widget here based on data +/// }, +/// ); +/// ``` +/// {@endtemplate} +/// +/// {@template value_stream_builder_build_when} +/// An optional [buildWhen] can be implemented for more granular control over +/// how often [ValueStreamBuilder] rebuilds. +/// +/// - [buildWhen] should only be used for performance optimizations as it +/// provides no security about the data passed to the [builder] function. +/// - [buildWhen] will be invoked on each [ValueStream] `data` change. +/// - [buildWhen] takes the previous `data` and current `data` and must +/// return a [bool] which determines whether or not the [builder] function will +/// be invoked. +/// - The previous `data` will be initialized to the `data` of the [ValueStream] when +/// the [ValueStreamBuilder] is initialized. +/// +/// [buildWhen] is optional and if omitted, it will default to `true`. +/// +/// ```dart +/// ValueStreamBuilder( +/// buildWhen: (previous, current) { +/// // return true/false to determine whether or not +/// // to rebuild the widget with data +/// }, +/// builder: (context, data) { +/// // return widget here based on data +/// } +/// ) +/// ``` +/// {@endtemplate} +class ValueStreamBuilder extends StatefulWidget { + final ValueStreamWidgetBuilder _builder; + final ValueStream _stream; + final ValueStreamBuilderCondition? _buildWhen; + + /// {@macro value_stream_builder} + /// {@macro value_stream_builder_build_when} + const ValueStreamBuilder({ + Key? key, + required ValueStream stream, + required ValueStreamWidgetBuilder builder, + ValueStreamBuilderCondition? buildWhen, + }) : _builder = builder, + _stream = stream, + _buildWhen = buildWhen, + super(key: key); + + @override + State> createState() => _ValueStreamBuilderState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty>('stream', _stream)) + ..add(ObjectFlagProperty>.has( + 'builder', _builder)) + ..add( + ObjectFlagProperty?>.has( + 'buildWhen', + _buildWhen, + ), + ); + } +} + +class _ValueStreamBuilderState extends State> { + late T currentData; + StreamSubscription? subscription; + Object? error; + + @override + void initState() { + super.initState(); + subscribe(); + } + + @override + void didUpdateWidget(covariant ValueStreamBuilder oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget._stream != widget._stream) { + unsubscribe(); + subscribe(); + } + } + + @override + void dispose() { + unsubscribe(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (error != null) { + throw error!; + } + return widget._builder(context, currentData); + } + + @pragma('vm:notify-debugger-on-exception') + void subscribe() { + final stream = widget._stream; + + try { + currentData = stream.value; + error = null; + } on ValueStreamError catch (e, s) { + FlutterError.reportError( + FlutterErrorDetails( + exception: error = ValueStreamHasNoValueError(stream), + stack: s, + library: 'rxdart_flutter', + ), + ); + return; + } catch (e, s) { + FlutterError.reportError( + FlutterErrorDetails( + exception: error = e, + stack: s, + library: 'rxdart_flutter', + ), + ); + return; + } + + final buildWhen = widget._buildWhen; + + assert(subscription == null, 'Stream already subscribed'); + subscription = stream.listen( + (data) { + if (buildWhen?.call(currentData, data) ?? true) { + currentData = data; + setState(_emptyFn); + } + }, + onError: (Object e, StackTrace s) { + FlutterError.reportError( + FlutterErrorDetails( + exception: error = UnhandledStreamError(e), + stack: s, + library: 'rxdart_flutter', + ), + ); + setState(_emptyFn); + }, + ); + } + + 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)); + } + + static void _emptyFn() {} +} + +/// Error emitted from [Stream] when using [ValueStreamBuilder]. +class UnhandledStreamError extends Error { + /// Error emitted from [Stream]. + final Object error; + + /// Create an [UnhandledStreamError] from [error]. + UnhandledStreamError(this.error); + + @override + String toString() { + return '''Unhandled error from ValueStream: $error. +ValueStreamBuilder requires ValueStream never to emit error events. +You should use one of following methods to handle error before passing stream to ValueStreamBuilder: + * stream.handleError((e, s) { }) + * stream.onErrorReturn(value) + * stream.onErrorReturnWith((e) => value) + * stream.onErrorResumeNext(otherStream) + * stream.onErrorResume((e) => otherStream) + * stream.transform( + StreamTransformer.fromHandlers(handleError: (e, s, sink) {})) + ... +If none of these solutions work, please file a bug at: +https://github.com/ReactiveX/rxdart/issues/new +'''; + } +} + +/// Error is thrown when [ValueStream.hasValue] is `false`. +class ValueStreamHasNoValueError extends Error { + final ValueStream stream; + + ValueStreamHasNoValueError(this.stream); + + @override + String toString() { + return '''ValueStreamBuilder requires hasValue of $stream to be `true`. +You can use BehaviorSubject.seeded(value), or publishValueSeeded(value)/shareValueSeeded(value) to create a ValueStream with an initial value. +Otherwise, you should check stream.hasValue before using ValueStreamBuilder. +If none of these solutions work, please file a bug at: +https://github.com/ReactiveX/rxdart/issues/new + '''; + } +} diff --git a/packages/rxdart_flutter/pubspec.yaml b/packages/rxdart_flutter/pubspec.yaml new file mode 100644 index 00000000..922739ef --- /dev/null +++ b/packages/rxdart_flutter/pubspec.yaml @@ -0,0 +1,55 @@ +name: rxdart_flutter +description: RxDart Flutter +version: 0.0.1 +homepage: + +environment: + sdk: '>=2.12.0 <4.0.0' + flutter: '>=2.0.1' + +dependencies: + flutter: + sdk: flutter + rxdart: ^0.28.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.4 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # To add assets to your package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/pubspec.lock b/pubspec.lock index 6536bdf2..c41ba147 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -298,4 +298,4 @@ packages: source: hosted version: "2.2.1" sdks: - dart: ">=3.1.0 <4.0.0" + dart: ">=3.6.0 <4.0.0"