diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 83c3f00..f48e122 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,11 +15,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: subosito/flutter-action@v2 - - uses: bluefireteam/melos-action@v1 + - uses: bluefireteam/melos-action@v3 with: - melos-version: '3.1.0' + melos-version: '6.3.2' - name: Disable analytics run: flutter config --no-analytics diff --git a/CHANGELOG.md b/CHANGELOG.md index dc10ff2..d54df54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,73 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +## 2025-03-05 + +### Changes + +--- + +Packages with breaking changes: + + - [`value_state` - `v2.0.0`](#value_state---v200) + +Packages with other changes: + + - There are no other changes in this release. + +Packages graduated to a stable release (see pre-releases prior to the stable version for changelog entries): + + - `value_state` - `v2.0.0` + +--- + +#### `value_state` - `v2.0.0` + + +## 2025-02-13 + +### Changes + +--- + +Packages with breaking changes: + + - [`value_state` - `v2.0.0-beta.1`](#value_state---v200-beta1) + +Packages with other changes: + + - There are no other changes in this release. + +--- + +#### `value_state` - `v2.0.0-beta.1` + + - **DOCS**: simplify README.md. + - **BREAKING** **REFACTOR**: class ValueState is replaced by Value. + - **BREAKING** **REFACTOR**: class ValueState is replaced by Value. + + +## 2025-02-07 + +### Changes + +--- + +Packages with breaking changes: + + - [`value_state` - `v2.0.0-beta.0`](#value_state---v200-beta0) + +Packages with other changes: + + - There are no other changes in this release. + +--- + +#### `value_state` - `v2.0.0-beta.0` + + - **BREAKING** **REFACTOR**: class ValueState is replaced by Value. + + ## 2023-06-01 ### Changes diff --git a/README.md b/README.md index 0d725e8..c8490a5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -A dart package that helps to implement basic states for [Flutter](https://flutter.dev/) and/or [BLoC library](https://pub.dev/packages/bloc) to perform, load and fetch data. +A dart package that helps to implement basic states for [Flutter](https://flutter.dev/) to load and fetch data. [![Test](https://github.com/devobs/value_state/actions/workflows/test.yml/badge.svg)](https://github.com/devobs/value_state/actions/workflows/test.yml) @@ -11,8 +11,6 @@ A dart package that helps to implement basic states for [Flutter](https://flutte | Package | Pub | | ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- | | [value_state](https://github.com/devobs/value_state/tree/main/packages/value_state) | [![pub package](https://img.shields.io/pub/v/value_state.svg)](https://pub.dev/packages/value_state) | -| [value_cubit](https://github.com/devobs/value_state/tree/main/packages/value_cubit) | [![pub package](https://img.shields.io/pub/v/value_cubit.svg)](https://pub.dev/packages/value_cubit) | -| [flutter_value_state](https://github.com/devobs/value_state/tree/main/packages/flutter_value_state) | [![pub package](https://img.shields.io/pub/v/flutter_value_state.svg)](https://pub.dev/packages/flutter_value_state) | diff --git a/packages/analysis_options.yaml b/packages/analysis_options.yaml deleted file mode 100644 index 17ccefe..0000000 --- a/packages/analysis_options.yaml +++ /dev/null @@ -1,7 +0,0 @@ -include: package:lints/recommended.yaml - -linter: - rules: - - prefer_const_constructors - - prefer_const_declarations - - prefer_single_quotes diff --git a/packages/flutter_value_state/.gitignore b/packages/flutter_value_state/.gitignore deleted file mode 100644 index e420cab..0000000 --- a/packages/flutter_value_state/.gitignore +++ /dev/null @@ -1,30 +0,0 @@ -# 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/flutter_value_state/.metadata b/packages/flutter_value_state/.metadata deleted file mode 100644 index e7011f6..0000000 --- a/packages/flutter_value_state/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# 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: f1875d570e39de09040c8f79aa13cc56baab8db1 - channel: stable - -project_type: package diff --git a/packages/flutter_value_state/CHANGELOG.md b/packages/flutter_value_state/CHANGELOG.md deleted file mode 100644 index 29d76ec..0000000 --- a/packages/flutter_value_state/CHANGELOG.md +++ /dev/null @@ -1,39 +0,0 @@ -## 1.3.6 - - - **FIX**: buildWidget with child class as value. - -## 1.3.5 - - - Update a dependency to the latest release. - -## 1.3.4 - - - **FIX**: onDefault not triggered. - -## 1.3.3 - - - **FIX**: ValueStateConfiguration merge broken. - -## 1.3.2 - - - Update a dependency to the latest release. - -## 1.3.1 - - - Update a dependency to the latest release. - -## 1.3.0 - - - **FEAT**: added onValue and wrapper parameters. - -## 1.2.2 - - - **FIX**: deprecation messages. - -## 1.2.1 - - - Update a dependency to the latest release. - -## 1.2.0 - -Initial version of the library. diff --git a/packages/flutter_value_state/LICENSE b/packages/flutter_value_state/LICENSE deleted file mode 100644 index d3f2fb0..0000000 --- a/packages/flutter_value_state/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) -Copyright (c) 2022 Emmanuel LEFEBVRE - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/flutter_value_state/README.md b/packages/flutter_value_state/README.md deleted file mode 100644 index 2f88bd6..0000000 --- a/packages/flutter_value_state/README.md +++ /dev/null @@ -1,128 +0,0 @@ -A dart package that helps to implement basic states for [BLoC library](https://pub.dev/packages/bloc) to perform, load and fetch data. - - -[![pub package](https://img.shields.io/pub/v/flutter_value_state.svg)](https://pub.dev/packages/flutter_value_state) -[![Test](https://github.com/devobs/value_state/actions/workflows/test.yml/badge.svg)](https://github.com/devobs/value_state/actions/workflows/test.yml) -[![codecov](https://codecov.io/gh/devobs/value_state/branch/main/graph/badge.svg)](https://app.codecov.io/gh/devobs/value_state/tree/main/packages/flutter_value_state) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -## Features - -* Provides all necessary states for data : init, waiting, value/no value and error states (from [value_state](https://pub.dev/packages/value_state)), -* Provides `BaseState.buildWidget` that build a widget depending on its state. The `WithValueState` case is mandatory (first ordered parameter). Other states that are not passed as parameter are handled by `ValueStateConfiguration`. If no `ValueStateConfiguration` is in ascendant tree, a `SizedBox`is returned, -* `ValueStateConfiguration` provides a default behavior for null parameters in `BaseState.buildWidget`. - -## Usage - -This example show in the Flutter app, how pattern matching is used to handles the different states. - -```dart -class CounterCubit extends ValueCubit { - var _value = 0; - - // Put your WS call that can be refreshed - Future _getCounterValueFromWebService() async => _value++; - - Future increment() => perform(() async { - // [perform] generate intermediate or final states such as PendingState, - // concrete subclass of ReadyState with right [ReadyState.refreshing] value - // or ErrorState if an error is raised. - final result = await _getCounterValueFromWebService(); - - emit(ValueState(result)); - }); - - void clear() { - _value = 0; - emit(const PendingState()); - } -} - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => CounterCubit(), - child: MaterialApp( - title: 'Value Cubit Demo', - builder: (context, child) => child == null - ? const SizedBox.shrink() - : ValueStateConfiguration( - configuration: ValueStateConfigurationData( - builderWaiting: (context, state) => - const Center(child: CircularProgressIndicator()), - builderError: (context, state) => Center( - child: Text('Expected error.', - style: - TextStyle(color: Theme.of(context).colorScheme.error)), - ), - builderNoValue: (context, state) => - const Center(child: Text('No value.')), - wrapper: (context, state, child) => AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: child), - ), - child: child, - ), - home: const MyHomePage(), - )); - } -} - -class MyHomePage extends StatelessWidget { - const MyHomePage({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return BlocBuilder>(builder: (context, state) { - return Scaffold( - appBar: AppBar( - title: const Text('Flutter Demo Home Page'), - ), - body: DefaultTextStyle( - style: const TextStyle(fontSize: 24), - textAlign: TextAlign.center, - child: state.buildWidget( - (context, state, error) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (state.refreshing) const LinearProgressIndicator(), - const Spacer(), - if (error != null) error, - const Text('Counter value :'), - Text( - state.value.toString(), - style: theme.textTheme.headlineMedium, - ), - const Spacer(), - ]), - valueMixedWithError: true), - ), - floatingActionButton: state is! ReadyState - ? null - : FloatingActionButton( - onPressed: state.refreshing - ? null - : context.read().increment, - tooltip: 'Increment', - child: state.refreshing - ? SizedBox.square( - dimension: 20, - child: CircularProgressIndicator( - color: theme.colorScheme.onPrimary)) - : const Icon(Icons.refresh)), - ); - }); - } -} -``` - -The whole code of this example is available in [example](example). - -## Feedback - -Please file any issues, bugs or feature requests as an issue on the [Github page](https://github.com/devobs/value_state/issues). diff --git a/packages/flutter_value_state/analysis_options.yaml b/packages/flutter_value_state/analysis_options.yaml deleted file mode 100644 index 0caccc8..0000000 --- a/packages/flutter_value_state/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../analysis_options.yaml \ No newline at end of file diff --git a/packages/flutter_value_state/example/analysis_options.yaml b/packages/flutter_value_state/example/analysis_options.yaml deleted file mode 100644 index 0caccc8..0000000 --- a/packages/flutter_value_state/example/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../analysis_options.yaml \ No newline at end of file diff --git a/packages/flutter_value_state/example/lib/cubit.dart b/packages/flutter_value_state/example/lib/cubit.dart deleted file mode 100644 index 593d101..0000000 --- a/packages/flutter_value_state/example/lib/cubit.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:value_cubit/value_cubit.dart'; - -class CounterCubit extends ValueCubit { - var _value = 0; - Future _getMyValueFromRepository() async => _value++; - - CounterCubit() { - increment(); - } - - Future increment() => perform(() async { - await Future.delayed(const Duration(seconds: 1)); - - final result = await _getMyValueFromRepository(); - - if (result == 2) { - throw 'Error'; - } else if (result > 4) { - emit(const NoValueState()); - } else { - emit(ValueState(result)); - } - }); -} diff --git a/packages/flutter_value_state/example/lib/main.dart b/packages/flutter_value_state/example/lib/main.dart deleted file mode 100644 index cbbabea..0000000 --- a/packages/flutter_value_state/example/lib/main.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_value_state/flutter_value_state.dart'; - -import 'cubit.dart'; - -// coverage:ignore-start -void main() { - runApp(const MyApp()); -} -// coverage:ignore-end - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => CounterCubit(), - child: MaterialApp( - title: 'Value Cubit Demo', - builder: (context, child) => child == null - ? const SizedBox.shrink() - : ValueStateConfiguration( - configuration: ValueStateConfigurationData( - builderWaiting: (context, state) => - const Center(child: CircularProgressIndicator()), - builderError: (context, state) => Center( - child: Text('Expected error.', - style: TextStyle( - color: Theme.of(context).colorScheme.error)), - ), - builderNoValue: (context, state) => - const Center(child: Text('No value.')), - wrapper: (context, state, child) => AnimatedSwitcher( - duration: const Duration(milliseconds: 300), - child: child), - ), - child: child, - ), - home: const MyHomePage(), - )); - } -} - -class MyHomePage extends StatelessWidget { - const MyHomePage({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return BlocBuilder>(builder: (context, state) { - return Scaffold( - appBar: AppBar( - title: const Text('Flutter Demo Home Page'), - ), - body: DefaultTextStyle( - style: const TextStyle(fontSize: 24), - textAlign: TextAlign.center, - child: state.buildWidget( - onValue: (context, state, error) => Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (state.refreshing) const LinearProgressIndicator(), - const Spacer(), - if (error != null) error, - const Text('Counter value :'), - Text( - state.value.toString(), - style: theme.textTheme.headlineMedium, - ), - const Spacer(), - ]), - valueMixedWithError: true), - ), - floatingActionButton: state is! ReadyState - ? null - : FloatingActionButton( - onPressed: state.refreshing - ? null - : context.read().increment, - tooltip: 'Increment', - child: state.refreshing - ? SizedBox.square( - dimension: 20, - child: CircularProgressIndicator( - color: theme.colorScheme.onPrimary)) - : const Icon(Icons.refresh)), - ); - }); - } -} diff --git a/packages/flutter_value_state/example/pubspec.yaml b/packages/flutter_value_state/example/pubspec.yaml deleted file mode 100644 index 8bf775a..0000000 --- a/packages/flutter_value_state/example/pubspec.yaml +++ /dev/null @@ -1,27 +0,0 @@ -name: flutter_state_value_example -description: A sample with a basic example - -publish_to: 'none' - -version: 1.0.0+1 - -environment: - sdk: ">=2.17.1 <3.0.0" - -dependencies: - flutter: - sdk: flutter - - flutter_bloc: ^8.0.1 - - value_state: 1.5.1 - flutter_value_state: 1.3.6 - value_cubit: 1.3.3 -dev_dependencies: - flutter_test: - sdk: flutter - - flutter_lints: ^2.0.0 - -flutter: - uses-material-design: true diff --git a/packages/flutter_value_state/example/test/widget_test.dart b/packages/flutter_value_state/example/test/widget_test.dart deleted file mode 100644 index 027da59..0000000 --- a/packages/flutter_value_state/example/test/widget_test.dart +++ /dev/null @@ -1,97 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_state_value_example/main.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - testWidgets( - 'Counter increments test', - (WidgetTester tester) async { - // Build our app and trigger a frame. - runZonedGuarded( - () async { - await tester.pumpWidget(const MyApp()); - await tester.pumpAndSettle(); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - expect(find.byType(LinearProgressIndicator), findsNothing); - - // Tap the refresh icon. - await tester.tap(find.byIcon(Icons.refresh)); - await tester.pump(); - - expect(find.byType(LinearProgressIndicator), findsOneWidget); - - await tester.pumpAndSettle(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - expect(find.text('Expected error.'), findsNothing); - - // Tap the refresh icon. - await tester.tap(find.byIcon(Icons.refresh)); - await tester.pump(); - - expect(find.byType(LinearProgressIndicator), findsOneWidget); - - await tester.pumpAndSettle(); - - // Verify that our counter has incremented. - expect(find.text('Expected error.'), findsOneWidget); - expect(find.text('1'), findsOneWidget); - - // Tap the refresh icon. - await tester.tap(find.byIcon(Icons.refresh)); - await tester.pump(); - - expect(find.byType(LinearProgressIndicator), findsOneWidget); - - await tester.pumpAndSettle(); - - // Verify that our counter has incremented. - expect(find.text('Expected error.'), findsNothing); - expect(find.text('3'), findsOneWidget); - - await tester.tap(find.byIcon(Icons.refresh)); - await tester.pump(); - - expect(find.byType(LinearProgressIndicator), findsOneWidget); - - await tester.pumpAndSettle(); - - // Verify that our counter has incremented. - expect(find.text('Expected error.'), findsNothing); - expect(find.text('4'), findsOneWidget); - - await tester.tap(find.byIcon(Icons.refresh)); - await tester.pump(); - - expect(find.byType(LinearProgressIndicator), findsOneWidget); - - await tester.pumpAndSettle(); - - // Verify that our counter has incremented. - expect(find.text('Expected error.'), findsNothing); - expect(find.text('5'), findsNothing); - }, - (error, stack) { - if (error != 'Error') { - // 'Error' expected - throw error; - } - }, - ); - }, - ); -} diff --git a/packages/flutter_value_state/lib/flutter_value_state.dart b/packages/flutter_value_state/lib/flutter_value_state.dart deleted file mode 100644 index de34753..0000000 --- a/packages/flutter_value_state/lib/flutter_value_state.dart +++ /dev/null @@ -1,6 +0,0 @@ -library flutter_value_state; - -export 'package:value_state/value_state.dart'; - -export 'src/configuration.dart'; -export 'src/widgets.dart'; diff --git a/packages/flutter_value_state/lib/src/configuration.dart b/packages/flutter_value_state/lib/src/configuration.dart deleted file mode 100644 index 449c5fd..0000000 --- a/packages/flutter_value_state/lib/src/configuration.dart +++ /dev/null @@ -1,162 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:value_state/value_state.dart'; - -Widget _defaultBuilder(BuildContext context, BaseState state) => - const SizedBox.shrink(); - -Widget _defaultWrapper( - BuildContext context, BaseState state, Widget child) => - child; - -typedef OnValueStateWaiting = Widget Function( - BuildContext context, WaitingState state); - -typedef OnValueStateWithValue = Widget Function( - BuildContext context, WithValueState state, Widget? error); -typedef OnValueStateNoValue = Widget Function( - BuildContext context, NoValueState state); -typedef OnValueStateError = Widget Function( - BuildContext context, ErrorState state); -typedef OnValueStateDefault = Widget Function( - BuildContext context, BaseState state); -typedef OnValueStateWrapper = Widget Function( - BuildContext context, BaseState state, Widget child); - -/// Define default behavior for the states [WaitingState], [NoValueState], [ErrorState]. -/// [builderDefault] can be used when none of this callback is mentionned. -class ValueStateConfigurationData { - const ValueStateConfigurationData({ - OnValueStateWrapper? wrapper, - OnValueStateWaiting? builderWaiting, - OnValueStateNoValue? builderNoValue, - OnValueStateError? builderError, - OnValueStateDefault? builderDefault, - }) : _wrapper = wrapper, - _builderWaiting = builderWaiting, - _builderNoValue = builderNoValue, - _builderError = builderError, - _builderDefault = builderDefault; - - /// Builder for all states that will be wrapped by this builder. - OnValueStateWrapper get wrapper => _wrapper ?? _defaultWrapper; - final OnValueStateWrapper? _wrapper; - - /// Builder for [WaitingState]. - OnValueStateWaiting get builderWaiting => _builderWaiting ?? builderDefault; - final OnValueStateWaiting? _builderWaiting; - - /// Builder for [NoValueState]. - OnValueStateNoValue get builderNoValue => _builderNoValue ?? builderDefault; - final OnValueStateNoValue? _builderNoValue; - - /// Builder for [ErrorState]. - OnValueStateError get builderError => _builderError ?? builderDefault; - final OnValueStateError? _builderError; - - /// Fallback builder when one of the state builder is empty. - OnValueStateDefault get builderDefault => _builderDefault ?? _defaultBuilder; - final OnValueStateDefault? _builderDefault; - - /// Creates a copy of this [ValueStateConfigurationData] but with the given - /// fields replaced with the new values. - ValueStateConfigurationData copyWith({ - OnValueStateWrapper? wrapper, - OnValueStateWaiting? builderWaiting, - OnValueStateNoValue? builderNoValue, - OnValueStateError? builderError, - OnValueStateDefault? builderDefault, - }) => - ValueStateConfigurationData( - wrapper: wrapper ?? this.wrapper, - builderWaiting: builderWaiting ?? this.builderWaiting, - builderNoValue: builderNoValue ?? this.builderNoValue, - builderError: builderError ?? this.builderError, - builderDefault: builderDefault ?? this.builderDefault, - ); - - /// Creates a new [ValueStateConfigurationData] where each parameter - /// from this object has been merged with the matching attribute. - ValueStateConfigurationData merge( - ValueStateConfigurationData? configuration) { - final baseConfiguration = - configuration ?? const ValueStateConfigurationData(); - - return baseConfiguration.copyWith( - wrapper: _wrapper, - builderWaiting: _builderWaiting, - builderNoValue: _builderNoValue, - builderError: _builderError, - builderDefault: _builderDefault, - ); - } - - @override - bool operator ==(other) => - identical(this, other) || - runtimeType == other.runtimeType && - other is ValueStateConfigurationData && - wrapper == other.wrapper && - builderWaiting == other.builderWaiting && - builderNoValue == other.builderNoValue && - builderError == other.builderError && - builderDefault == other.builderDefault; - - @override - int get hashCode => Object.hash( - wrapper, - builderNoValue, - builderWaiting, - builderError, - builderDefault, - ); -} - -/// Provide a [ValueStateConfigurationData] for all inherited widget to define -/// default behavior for any state of [BaseState] except [ValueState]. -/// -/// If this configuration is in a subtree of another [ValueStateConfiguration], -/// the configuration will be merged with the parent one. -class ValueStateConfiguration extends StatelessWidget { - const ValueStateConfiguration({ - super.key, - required this.configuration, - required this.child, - }); - - /// The default to configuration. - final ValueStateConfigurationData configuration; - final Widget child; - - @override - Widget build(BuildContext context) { - final inheritedConfiguration = maybeOf(context); - - return _ValueStateConfiguration( - configuration: configuration.merge(inheritedConfiguration), - child: child); - } - - static ValueStateConfigurationData? maybeOf(BuildContext context) => context - .dependOnInheritedWidgetOfExactType<_ValueStateConfiguration>() - ?.configuration; - - static ValueStateConfigurationData of(BuildContext context) { - final ValueStateConfigurationData? configuration = maybeOf(context); - - assert( - configuration != null, 'No $ValueStateConfiguration found in context'); - - return configuration!; - } -} - -class _ValueStateConfiguration extends InheritedWidget { - const _ValueStateConfiguration( - {required this.configuration, required super.child}); - - final ValueStateConfigurationData configuration; - - @override - bool updateShouldNotify(covariant _ValueStateConfiguration oldWidget) => - configuration != oldWidget.configuration; -} diff --git a/packages/flutter_value_state/lib/src/widgets.dart b/packages/flutter_value_state/lib/src/widgets.dart deleted file mode 100644 index 6200e0c..0000000 --- a/packages/flutter_value_state/lib/src/widgets.dart +++ /dev/null @@ -1,149 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:value_state/value_state.dart'; - -import 'configuration.dart'; - -extension StateConfigurationExtensions on BuildContext { - ValueStateConfigurationData get stateConfiguration => - ValueStateConfiguration.maybeOf(this) ?? - const ValueStateConfigurationData(); -} - -extension ValueStateBuilderExtension on BaseState { - Widget buildWidget({ - Key? key, - OnValueStateWithValue? onValue, - OnValueStateWaiting? onWaiting, - OnValueStateNoValue? onNoValue, - OnValueStateError? onError, - OnValueStateDefault? onDefault, - OnValueStateWrapper? wrapper, - bool wrapped = true, - bool valueMixedWithError = false, - }) => - ValueStateWidget( - state: this, - onDefault: onDefault, - onError: onError, - onWithValue: onValue, - onNoValue: onNoValue, - onWaiting: onWaiting, - valueMixedWithError: valueMixedWithError, - wrapped: wrapped, - wrapper: wrapper, - ); -} - -class ValueStateWidget extends StatelessWidget { - const ValueStateWidget({ - required this.state, - this.onWithValue, - this.onWaiting, - this.onNoValue, - this.onError, - this.onDefault, - this.wrapper, - this.wrapped = true, - this.valueMixedWithError = false, - }); - - final BaseState state; - - final OnValueStateWithValue? onWithValue; - - final OnValueStateWaiting? onWaiting; - - final OnValueStateNoValue? onNoValue; - final OnValueStateError? onError; - final OnValueStateDefault? onDefault; - final OnValueStateWrapper? wrapper; - - final bool wrapped; - final bool valueMixedWithError; - - @override - Widget build(BuildContext context) { - final state = this.state; - if (state is WaitingState) { - return _buildWaitingState(context, state); - } else if (state is NoValueState) { - return _buildNoValueState(context, state); - } else if (state is ValueState) { - return _buildWithValueState(context, state); - } else if (state is ErrorState) { - return _buildErrorState(context, state); - } - - // coverage:ignore-start - throw UnimplementedError(); - // coverage:ignore-end - } - - Widget _builder( - BuildContext context, - BaseState state, - Widget Function( - BuildContext context, - ValueStateConfigurationData valueStateConfiguration, - OnValueStateDefault? onDefault, - ) builder, - ) { - final valueStateConfiguration = context.stateConfiguration; - Widget child = builder(context, valueStateConfiguration, onDefault); - - if (wrapper != null) { - child = wrapper!(context, state, child); - } - - return wrapped - ? valueStateConfiguration.wrapper(context, state, child) - : child; - } - - Widget _buildWaitingState(BuildContext context, WaitingState state) => - _builder(context, state, (context, valueStateConfiguration, onDefault) { - final onWaiting = this.onWaiting ?? - onDefault ?? - valueStateConfiguration.builderWaiting; - - return onWaiting(context, state); - }); - - Widget _buildNoValueState(BuildContext context, NoValueState state) => - _builder(context, state, (context, valueStateConfiguration, onDefault) { - final onNoValue = this.onNoValue ?? - onDefault ?? - valueStateConfiguration.builderNoValue; - - return onNoValue(context, state); - }); - - Widget _buildWithValueState(BuildContext context, WithValueState state) => - _builder(context, state, (context, valueStateConfiguration, onDefault) { - final onError = - this.onError ?? onDefault ?? valueStateConfiguration.builderError; - Widget? error; - - if (state is ErrorWithPreviousValue) { - error = onError(context, state); - } - - return onWithValue?.call(context, state, error) ?? - onDefault?.call(context, state) ?? - valueStateConfiguration.builderDefault(context, state); - }); - - Widget _buildErrorState(BuildContext context, ErrorState state) { - if (valueMixedWithError && state is ErrorWithPreviousValue) { - return _buildWithValueState(context, state); - } - - return _builder(context, state, - (context, valueStateConfiguration, onDefault) { - final onError = - this.onError ?? onDefault ?? valueStateConfiguration.builderError; - - return onError(context, state); - }); - } -} diff --git a/packages/flutter_value_state/pubspec.yaml b/packages/flutter_value_state/pubspec.yaml deleted file mode 100644 index 22f7889..0000000 --- a/packages/flutter_value_state/pubspec.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: flutter_value_state -description: A dart package that helps implements basic states for BLoC library -repository: https://github.com/devobs/value_state -homepage: https://github.com/devobs -version: 1.3.6 - -environment: - sdk: ">=2.17.5 <3.0.0" - flutter: ">=3.0.0" - -dependencies: - flutter: - sdk: flutter - - value_state: ^1.5.1 -dev_dependencies: - flutter_test: - sdk: flutter - - flutter_lints: ^2.0.0 - test: ^1.20.1 - stream_transform: ^2.0.0 diff --git a/packages/flutter_value_state/test/flutter_value_state_test.dart b/packages/flutter_value_state/test/flutter_value_state_test.dart deleted file mode 100644 index 2adad88..0000000 --- a/packages/flutter_value_state/test/flutter_value_state_test.dart +++ /dev/null @@ -1,401 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_value_state/flutter_value_state.dart'; - -const _buildWidgetKey = ValueKey('buildWidget'); -const _defaultWidgetKey = ValueKey('defaultWidget'); -const _errorWidgetKey = ValueKey('errorWidget'); -const _noValueWidgetKey = ValueKey('noValueWidget'); -const _waitingWidgetKey = ValueKey('waitingWidget'); -const _wrapperWidgetKey = ValueKey('wrapperWidget'); -const _defaultWidgetType = SizedBox; - -late ValueStateConfigurationData _valueStateConfigurationData; - -class _TestWidget> extends StatelessWidget { - const _TestWidget({ - required this.state, - this.valueMixedWithError = false, - this.child, - this.onWaiting, - this.onNoValue, - this.onError, - this.onDefault, - this.wrapper, - this.wrapped = true, - this.onValueEnabled = true, - }); - - final T state; - final bool valueMixedWithError; - - final Widget? child; - - final OnValueStateWaiting? onWaiting; - final OnValueStateNoValue? onNoValue; - final OnValueStateError? onError; - final OnValueStateDefault? onDefault; - final OnValueStateWrapper? wrapper; - - final bool wrapped; - final bool onValueEnabled; - - @override - Widget build(BuildContext context) { - return state.buildWidget( - onValue: onValueEnabled - ? (context, state, error) { - return Column( - children: [ - if (error != null) error, - child ?? const SizedBox.shrink(key: _buildWidgetKey), - ], - ); - } - : null, - valueMixedWithError: valueMixedWithError, - onDefault: onDefault, - onError: onError, - onNoValue: onNoValue, - onWaiting: onWaiting, - wrapper: wrapper, - wrapped: wrapped, - ); - } -} - -class _TestConfigurationWidget> - extends StatefulWidget { - const _TestConfigurationWidget({ - super.key, - required this.state, - this.valueMixedWithError = false, - this.child, - this.onWaiting, - this.onNoValue, - this.onError, - this.onDefault, - this.wrapper, - this.wrapped = true, - this.onValueEnabled = true, - }); - - final T state; - final bool valueMixedWithError; - - final Widget? child; - - final OnValueStateWaiting? onWaiting; - final OnValueStateNoValue? onNoValue; - final OnValueStateError? onError; - final OnValueStateDefault? onDefault; - final OnValueStateWrapper? wrapper; - - final bool wrapped; - final bool onValueEnabled; - - @override - State<_TestConfigurationWidget> createState() => - _TestConfigurationWidgetState(); -} - -class _TestConfigurationWidgetState> - extends State<_TestConfigurationWidget> { - OnValueStateError _onError = - (context, state) => const SizedBox.shrink(key: _errorWidgetKey); - - void updateOnError(OnValueStateError onError) { - setState(() { - _onError = onError; - }); - } - - @override - Widget build(BuildContext context) { - _valueStateConfigurationData = ValueStateConfigurationData( - builderDefault: (context, state) => - const SizedBox.shrink(key: _defaultWidgetKey), - builderError: _onError, - builderNoValue: (context, state) => - const SizedBox.shrink(key: _noValueWidgetKey), - builderWaiting: (context, state) => - const SizedBox.shrink(key: _waitingWidgetKey), - wrapper: (context, state, child) => - KeyedSubtree(key: _wrapperWidgetKey, child: child), - ); - - return ValueStateConfiguration( - configuration: _valueStateConfigurationData, - child: _TestWidget( - state: widget.state, - valueMixedWithError: widget.valueMixedWithError, - child: widget.child, - onDefault: widget.onDefault, - onError: widget.onError, - onNoValue: widget.onNoValue, - onWaiting: widget.onWaiting, - wrapper: widget.wrapper, - wrapped: widget.wrapped, - onValueEnabled: widget.onValueEnabled, - )); - } -} - -void main() { - test('$ValueStateConfiguration.copyWith without parameter', () { - const configuration = ValueStateConfigurationData(); - - expect(configuration.copyWith(), configuration); - }); - - group('without configuration', () { - testWidgets('buildWidget with ${ValueState}', (tester) async { - await tester.pumpWidget(const _TestWidget(state: ValueState(1))); - - expect(find.byKey(_buildWidgetKey), findsOneWidget); - expect(find.byType(_defaultWidgetType), findsOneWidget); - }); - - testWidgets('buildWidget without parameter with ${ValueState}', - (tester) async { - await tester.pumpWidget(const ValueState(1).buildWidget()); - - expect(find.byKey(_buildWidgetKey), findsNothing); - expect(find.byType(_defaultWidgetType), findsOneWidget); - }); - - for (final state in >[ - const InitState(), - const PendingState(), - // const ValueState(1), - const NoValueState(), - ErrorState( - previousState: const InitState(), - error: 'Error', - refreshing: false) - ]) { - testWidgets('defaultBuilder with ${state.runtimeType}', (tester) async { - await tester.pumpWidget(_TestWidget(state: state)); - - expect(find.byKey(_buildWidgetKey), findsNothing); - expect(find.byType(_defaultWidgetType), findsOneWidget); - }); - - testWidgets( - 'defaultBuilder with empty configuration ${state.runtimeType}', - (tester) async { - await tester.pumpWidget( - ValueStateConfiguration( - configuration: const ValueStateConfigurationData(), - child: _TestWidget(state: state)), - ); - - expect(find.byKey(_buildWidgetKey), findsNothing); - expect(find.byType(_defaultWidgetType), findsOneWidget); - }); - } - - for (final state in >[ - const InitState(), - const PendingState(), - // const ValueState(1), - const NoValueState(), - ErrorState( - previousState: const InitState(), - error: 'Error', - refreshing: false) - ]) { - testWidgets('defaultBuilder with ${state.runtimeType}', (tester) async { - await tester.pumpWidget(_TestWidget(state: state)); - - expect(find.byKey(_buildWidgetKey), findsNothing); - expect(find.byType(_defaultWidgetType), findsOneWidget); - }); - } - }); - - group('with configuration', () { - testWidgets('should get ValueStateConfigurationData', (tester) async { - late ValueStateConfigurationData valueStateConfigurationData; - await tester.pumpWidget(_TestConfigurationWidget( - state: const ValueState(1), - child: Builder(builder: (context) { - valueStateConfigurationData = ValueStateConfiguration.of(context); - return const SizedBox.shrink(); - }))); - - expect(valueStateConfigurationData, _valueStateConfigurationData); - expect(valueStateConfigurationData.hashCode, - _valueStateConfigurationData.hashCode); - }); - - testWidgets('buildWidget with ${ValueState}', (tester) async { - final testKey = GlobalKey<_TestConfigurationWidgetState>(); - await tester.pumpWidget(_TestConfigurationWidget( - key: testKey, - state: const ValueState(1), - )); - - expect(find.byKey(_buildWidgetKey), findsOneWidget); - expect(find.byType(_defaultWidgetType), findsOneWidget); - - testKey.currentState!.updateOnError((context, state) { - return Container(key: _errorWidgetKey); - }); - - expect(find.byKey(_buildWidgetKey), findsOneWidget); - expect(find.byType(_defaultWidgetType), findsOneWidget); - }); - - for (final state in , Key>{ - const InitState(): _waitingWidgetKey, - const PendingState(): _waitingWidgetKey, - const NoValueState(): _noValueWidgetKey, - ErrorState( - previousState: const InitState(), - error: 'Error', - refreshing: false): _errorWidgetKey, - ErrorState( - previousState: 0.toState(), - error: 'Error', - refreshing: false): _errorWidgetKey, - }.entries) { - testWidgets('build with ${state.key.runtimeType}', (tester) async { - await tester.pumpWidget(_TestConfigurationWidget(state: state.key)); - - expect(find.byKey(state.value), findsOneWidget); - expect(find.byType(_defaultWidgetType), findsOneWidget); - }); - - testWidgets('build with ${state.key.runtimeType} and onDefault', - (tester) async { - await tester.pumpWidget(_TestConfigurationWidget( - state: state.key, - onDefault: (context, state) => Container(key: _defaultWidgetKey), - valueMixedWithError: true, - onValueEnabled: false, - )); - - expect(find.byKey(state.value), findsNothing); - expect(find.byKey(_defaultWidgetKey), findsOneWidget); - }); - } - - for (final state in , Key>{ - const InitState(): _waitingWidgetKey, - const PendingState(): _waitingWidgetKey, - const NoValueState(): _noValueWidgetKey, - ErrorState( - previousState: const InitState(), - error: 'Error', - refreshing: false): _errorWidgetKey, - }.entries) { - const wrapperKey = Key('innerWrapperWidget'); - testWidgets( - 'build with ${state.key.runtimeType} and callbacks and wrapper', - (tester) async { - await tester.pumpWidget(_TestConfigurationWidget( - state: state.key, - onDefault: (context, state) => Container(key: _defaultWidgetKey), - onError: (context, state) => Container(key: _errorWidgetKey), - onNoValue: (context, state) => Container(key: _noValueWidgetKey), - onWaiting: (context, state) => Container(key: _waitingWidgetKey), - wrapper: (context, state, child) => - Center(key: wrapperKey, child: child), - )); - - expect(find.byKey(state.value), findsOneWidget); - expect(find.byKey(wrapperKey), findsOneWidget); - expect(find.byKey(_defaultWidgetKey), findsNothing); - expect(find.byType(_defaultWidgetType), findsNothing); - expect(find.byType(Container), findsOneWidget); - expect(find.byType(Center), findsOneWidget); - }); - - testWidgets( - 'build with ${state.key.runtimeType} and callbacks and wrapper disabled', - (tester) async { - await tester.pumpWidget(_TestConfigurationWidget( - state: state.key, - onDefault: (context, state) => Container(key: _defaultWidgetKey), - onError: (context, state) => Container(key: _errorWidgetKey), - onNoValue: (context, state) => Container(key: _noValueWidgetKey), - onWaiting: (context, state) => Container(key: _waitingWidgetKey), - wrapper: (context, state, child) => - Center(key: wrapperKey, child: child), - wrapped: false, - )); - - expect(find.byKey(state.value), findsOneWidget); - expect(find.byKey(wrapperKey), findsOneWidget); - expect(find.byKey(_defaultWidgetKey), findsNothing); - expect(find.byType(_defaultWidgetType), findsNothing); - expect(find.byType(Container), findsOneWidget); - expect(find.byType(Center), findsOneWidget); - }); - } - - testWidgets( - 'build with ${ErrorWithoutPreviousValue} with and valueMixedWithError', - (tester) async { - final testKey = GlobalKey<_TestConfigurationWidgetState>(); - await tester.pumpWidget(_TestConfigurationWidget( - key: testKey, - state: ErrorState( - previousState: const InitState(), - error: 'Error', - refreshing: false, - ), - valueMixedWithError: true, - )); - - expect(find.byKey(_buildWidgetKey), findsNothing); - expect(find.byKey(_errorWidgetKey), findsOneWidget); - expect(find.byType(_defaultWidgetType), findsOneWidget); - }); - - testWidgets('build with ${ErrorWithPreviousValue}', (tester) async { - final testKey = GlobalKey<_TestConfigurationWidgetState>(); - await tester.pumpWidget(_TestConfigurationWidget( - key: testKey, - state: ErrorState( - previousState: const ValueState(1), - error: 'Error', - refreshing: false, - ), - )); - - expect(find.byKey(_errorWidgetKey), findsOneWidget); - expect(find.byType(_defaultWidgetType), findsOneWidget); - - testKey.currentState!.updateOnError((context, state) { - return Container(key: _errorWidgetKey); - }); - - await tester.pumpAndSettle(); - - expect(find.byKey(_errorWidgetKey), findsOneWidget); - expect(find.byType(Container), findsOneWidget); - expect(find.byType(_defaultWidgetType), findsNothing); - }); - - testWidgets( - 'build with ${ErrorWithPreviousValue} with and valueMixedWithError', - (tester) async { - final testKey = GlobalKey<_TestConfigurationWidgetState>(); - await tester.pumpWidget(_TestConfigurationWidget( - key: testKey, - state: ErrorState( - previousState: const ValueState(1), - error: 'Error', - refreshing: false, - ), - valueMixedWithError: true, - )); - - expect(find.byKey(_buildWidgetKey), findsOneWidget); - expect(find.byKey(_errorWidgetKey), findsOneWidget); - expect(find.byType(_defaultWidgetType), findsNWidgets(2)); - }); - }); -} diff --git a/packages/value_cubit/.gitignore b/packages/value_cubit/.gitignore deleted file mode 100644 index c3e6113..0000000 --- a/packages/value_cubit/.gitignore +++ /dev/null @@ -1,29 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# 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/value_cubit/CHANGELOG.md b/packages/value_cubit/CHANGELOG.md deleted file mode 100644 index 98ef2ae..0000000 --- a/packages/value_cubit/CHANGELOG.md +++ /dev/null @@ -1,80 +0,0 @@ -## 1.3.3 - - - **FIX**: no emit on closed bloc. - -## 1.3.2 - - - Update a dependency to the latest release. - -## 1.3.1 - - - **DOCS**: fix performOnState reference. - -## 1.3.0 - - - **FEAT**: added ValueCubitMixin.waitReady. - -## 1.2.5 - - - Update a dependency to the latest release. - -## 1.2.4 - - - Update a dependency to the latest release. - -## 1.2.3 - - - **FIX**: deprecation messages. - -## 1.2.2 - - - Update a dependency to the latest release. - -## 1.2.0 - -* Added pattern matching - -## 1.1.6 - -* Update README exemple - -## 1.1.5 - -* Simplification of the example - -## 1.1.4 - -* Fix diagram images - -## 1.1.2 - -* Simplify BaseState -* More understable example messages -* States modelization - -## 1.1.1 - -* Sanitize emitMappedState parameters - -## 1.1.0 - -* Upgrade depedencies to Flutter 3.0.1 -* `emitMappedState` on ValueState - -## 1.0.1 - -* Update documentation - -## 1.0.0 - -* New cubit class `RefreshValueCubit` -* New states `WithValueState`, `PendingState`, `ReadyState` and `ErrorWithPreviousValue` -* New helpers `StreamInputCubitMixin` (mixin on `ValueCubit`) -* New extension `behaviorSubject` (current state with stream) on `StateAndStream` -* Add examples : basic and flutter - -## 0.9.0 - -Initial version of the library. -* Includes states -* ValueCubit and mixins diff --git a/packages/value_cubit/LICENSE b/packages/value_cubit/LICENSE deleted file mode 100644 index d3f2fb0..0000000 --- a/packages/value_cubit/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) -Copyright (c) 2022 Emmanuel LEFEBVRE - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without restriction, -including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, -and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/packages/value_cubit/README.md b/packages/value_cubit/README.md deleted file mode 100644 index cc59f03..0000000 --- a/packages/value_cubit/README.md +++ /dev/null @@ -1,127 +0,0 @@ -A dart package that helps to implement basic states for [BLoC library](https://pub.dev/packages/bloc) to perform, load and fetch data. - - -[![pub package](https://img.shields.io/pub/v/value_cubit.svg)](https://pub.dev/packages/value_cubit) -[![Test](https://github.com/devobs/value_state/actions/workflows/test.yml/badge.svg)](https://github.com/devobs/value_state/actions/workflows/test.yml) -[![codecov](https://codecov.io/gh/devobs/value_state/branch/main/graph/badge.svg)](https://app.codecov.io/gh/devobs/value_state/tree/main/packages/value_cubit) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -## Features - -* Provides all necessary states for data : init, waiting, value/no value and error states (from [value_state](https://pub.dev/packages/value_state)), -* A `ValueCubit` class to simplify `Cubit` subclassing, -* A `RefreshValueCubit` class like `ValueCubit` with refreshing capabilities, -* Some helpers `perform` (an extension on `Cubit`) to emit intermediate states while an action is intended to update state : the same state is reemitted with attribute `refreshing` at `true`. - -## Usage - -This example shows how different value states from this library help developpers to show load step data widgets. - - -```dart -class CounterCubit extends ValueCubit { - var _value = 0; - - // Put your WS call that can be refreshed - Future _getCounterValueFromWebService() async => _value++; - - Future increment() => perform(() async { - // [perform] generate intermediate or final states such as PendingState, - // concrete subclass of ReadyState with right [ReadyState.refreshing] value - // or ErrorState if an error is raised. - final result = await _getCounterValueFromWebService(); - - emit(ValueState(result)); - }); - - void clear() { - _value = 0; - emit(const PendingState()); - } -} - -class MyHomePage extends StatelessWidget { - const MyHomePage({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return BlocBuilder>(builder: (context, state) { - return Scaffold( - appBar: AppBar( - title: const Text('Flutter Demo Home Page'), - ), - body: DefaultTextStyle( - style: const TextStyle(fontSize: 24), - textAlign: TextAlign.center, - child: state is ReadyState - ? Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (state.refreshing) const LinearProgressIndicator(), - const Spacer(), - if (state.hasError) - Text('Expected error.', - style: TextStyle(color: theme.colorScheme.error)), - if (state is WithValueState) ...[ - if (state.hasError) - const Text('Previous counter value :') - else - const Text('Actual counter value :'), - Text( - state.value.toString(), - style: theme.textTheme.headlineMedium, - ), - ], - if (state is NoValueState) const Text('No Value'), - const Spacer(), - ], - ) - : const Center(child: CircularProgressIndicator()), - ), - floatingActionButton: state is! ReadyState - ? null - : FloatingActionButton( - onPressed: state.refreshing - ? null - : context.read().increment, - tooltip: 'Increment', - child: state.refreshing - ? SizedBox.square( - dimension: 20, - child: CircularProgressIndicator( - color: theme.colorScheme.onPrimary)) - : const Icon(Icons.refresh)), - ); - }); - } -} -``` - -The whole code of this example is available in [example](example). - - -If your cubit is only a getter with the need to refresh your cubit state, you can simplify the implementation `ValueCubit` with `RefreshValueCubit`. - -```dart -class CounterCubit extends RefreshValueCubit { - var _value = 0; - - // Put your WS call that can be refreshed - Future _getCounterValueFromWebService() async => _value++; - - @override - Future emitValues() async { - final result = await _getCounterValueFromWebService(); - - emit(ValueState(result)); - } -} -``` - -Update your value (increment in our example) by calling `myCubit.refresh()`. - -## Feedback - -Please file any issues, bugs or feature requests as an issue on the [Github page](https://github.com/devobs/value_state/issues). diff --git a/packages/value_cubit/analysis_options.yaml b/packages/value_cubit/analysis_options.yaml deleted file mode 100644 index 0caccc8..0000000 --- a/packages/value_cubit/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../analysis_options.yaml \ No newline at end of file diff --git a/packages/value_cubit/example/.gitignore b/packages/value_cubit/example/.gitignore deleted file mode 100644 index 0fa6b67..0000000 --- a/packages/value_cubit/example/.gitignore +++ /dev/null @@ -1,46 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.buildlog/ -.history -.svn/ - -# 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 -**/doc/api/ -**/ios/Flutter/.last_build_id -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -/build/ - -# Web related -lib/generated_plugin_registrant.dart - -# Symbolication related -app.*.symbols - -# Obfuscation related -app.*.map.json - -# Android Studio will place build artifacts here -/android/app/debug -/android/app/profile -/android/app/release diff --git a/packages/value_cubit/example/.metadata b/packages/value_cubit/example/.metadata deleted file mode 100644 index 166a998..0000000 --- a/packages/value_cubit/example/.metadata +++ /dev/null @@ -1,10 +0,0 @@ -# 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: c860cba910319332564e1e9d470a17074c1f2dfd - channel: stable - -project_type: app diff --git a/packages/value_cubit/example/README.md b/packages/value_cubit/example/README.md deleted file mode 100644 index b96eb8d..0000000 --- a/packages/value_cubit/example/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# flutter_basic - -A basic example of value cubit usage. - -## Getting Started - -Just type to launch in navigator : -```bash -flutter run -``` \ No newline at end of file diff --git a/packages/value_cubit/example/analysis_options.yaml b/packages/value_cubit/example/analysis_options.yaml deleted file mode 100644 index 0caccc8..0000000 --- a/packages/value_cubit/example/analysis_options.yaml +++ /dev/null @@ -1 +0,0 @@ -include: ../analysis_options.yaml \ No newline at end of file diff --git a/packages/value_cubit/example/lib/cubit.dart b/packages/value_cubit/example/lib/cubit.dart deleted file mode 100644 index 593d101..0000000 --- a/packages/value_cubit/example/lib/cubit.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:value_cubit/value_cubit.dart'; - -class CounterCubit extends ValueCubit { - var _value = 0; - Future _getMyValueFromRepository() async => _value++; - - CounterCubit() { - increment(); - } - - Future increment() => perform(() async { - await Future.delayed(const Duration(seconds: 1)); - - final result = await _getMyValueFromRepository(); - - if (result == 2) { - throw 'Error'; - } else if (result > 4) { - emit(const NoValueState()); - } else { - emit(ValueState(result)); - } - }); -} diff --git a/packages/value_cubit/example/lib/main.dart b/packages/value_cubit/example/lib/main.dart deleted file mode 100644 index 7b94733..0000000 --- a/packages/value_cubit/example/lib/main.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:value_state/value_state.dart'; - -import 'cubit.dart'; - -// coverage:ignore-start -void main() { - runApp(const MyApp()); -} -// coverage:ignore-end - -class MyApp extends StatelessWidget { - const MyApp({super.key}); - - // This widget is the root of your application. - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (_) => CounterCubit(), - child: const MaterialApp( - title: 'Value Cubit Demo', - home: MyHomePage(), - )); - } -} - -class MyHomePage extends StatelessWidget { - const MyHomePage({super.key}); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return BlocBuilder>(builder: (context, state) { - return Scaffold( - appBar: AppBar( - title: const Text('Flutter Demo Home Page'), - ), - body: DefaultTextStyle( - style: const TextStyle(fontSize: 24), - textAlign: TextAlign.center, - child: state is ReadyState - ? Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (state.refreshing) const LinearProgressIndicator(), - const Spacer(), - if (state.hasError) - Text('Expected error.', - style: TextStyle(color: theme.colorScheme.error)), - if (state is WithValueState) ...[ - if (state.hasError) - const Text('Previous counter value :') - else - const Text('Actual counter value :'), - Text( - state.value.toString(), - style: theme.textTheme.headlineMedium, - ), - ], - const Spacer(), - ], - ) - : const Center(child: CircularProgressIndicator()), - ), - floatingActionButton: state is! ReadyState - ? null - : FloatingActionButton( - onPressed: state.refreshing - ? null - : context.read().increment, - tooltip: 'Increment', - child: state.refreshing - ? SizedBox.square( - dimension: 20, - child: CircularProgressIndicator( - color: theme.colorScheme.onPrimary)) - : const Icon(Icons.refresh)), - ); - }); - } -} diff --git a/packages/value_cubit/example/pubspec.yaml b/packages/value_cubit/example/pubspec.yaml deleted file mode 100644 index 2da97da..0000000 --- a/packages/value_cubit/example/pubspec.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: flutter_basic -description: A sample with a basic example - -publish_to: 'none' - -version: 1.0.0+1 - -environment: - sdk: ">=2.17.1 <3.0.0" - -dependencies: - flutter: - sdk: flutter - - flutter_bloc: ^8.0.1 - - value_state: 1.5.1 - value_cubit: 1.3.3 -dev_dependencies: - flutter_test: - sdk: flutter - - flutter_lints: ^2.0.0 - -flutter: - uses-material-design: true diff --git a/packages/value_cubit/example/web/favicon.png b/packages/value_cubit/example/web/favicon.png deleted file mode 100644 index 8aaa46a..0000000 Binary files a/packages/value_cubit/example/web/favicon.png and /dev/null differ diff --git a/packages/value_cubit/example/web/icons/Icon-192.png b/packages/value_cubit/example/web/icons/Icon-192.png deleted file mode 100644 index b749bfe..0000000 Binary files a/packages/value_cubit/example/web/icons/Icon-192.png and /dev/null differ diff --git a/packages/value_cubit/example/web/icons/Icon-512.png b/packages/value_cubit/example/web/icons/Icon-512.png deleted file mode 100644 index 88cfd48..0000000 Binary files a/packages/value_cubit/example/web/icons/Icon-512.png and /dev/null differ diff --git a/packages/value_cubit/example/web/icons/Icon-maskable-192.png b/packages/value_cubit/example/web/icons/Icon-maskable-192.png deleted file mode 100644 index eb9b4d7..0000000 Binary files a/packages/value_cubit/example/web/icons/Icon-maskable-192.png and /dev/null differ diff --git a/packages/value_cubit/example/web/icons/Icon-maskable-512.png b/packages/value_cubit/example/web/icons/Icon-maskable-512.png deleted file mode 100644 index d69c566..0000000 Binary files a/packages/value_cubit/example/web/icons/Icon-maskable-512.png and /dev/null differ diff --git a/packages/value_cubit/example/web/index.html b/packages/value_cubit/example/web/index.html deleted file mode 100644 index febe6a3..0000000 --- a/packages/value_cubit/example/web/index.html +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - flutter_basic - - - - - - - diff --git a/packages/value_cubit/example/web/manifest.json b/packages/value_cubit/example/web/manifest.json deleted file mode 100644 index b8dd7fc..0000000 --- a/packages/value_cubit/example/web/manifest.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "flutter_basic", - "short_name": "flutter_basic", - "start_url": ".", - "display": "standalone", - "background_color": "#0175C2", - "theme_color": "#0175C2", - "description": "A new Flutter project.", - "orientation": "portrait-primary", - "prefer_related_applications": false, - "icons": [ - { - "src": "icons/Icon-192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "icons/Icon-512.png", - "sizes": "512x512", - "type": "image/png" - }, - { - "src": "icons/Icon-maskable-192.png", - "sizes": "192x192", - "type": "image/png", - "purpose": "maskable" - }, - { - "src": "icons/Icon-maskable-512.png", - "sizes": "512x512", - "type": "image/png", - "purpose": "maskable" - } - ] -} diff --git a/packages/value_cubit/lib/src/cubit.dart b/packages/value_cubit/lib/src/cubit.dart deleted file mode 100644 index 445338b..0000000 --- a/packages/value_cubit/lib/src/cubit.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:meta/meta.dart'; -import 'package:synchronized/synchronized.dart'; -import 'package:value_state/value_state.dart'; - -/// Shortbut to user [BaseState] with [Cubit] -abstract class ValueCubit extends Cubit> with ValueCubitMixin { - ValueCubit([BaseState? initState]) : super(initState ?? InitState()); -} - -/// Shared implementation to handle refresh capability on cubit -abstract class RefreshValueCubit extends ValueCubit - with RefreshValueCubitMixin { - RefreshValueCubit([BaseState? initState]) - : super(initState ?? InitState()); -} - -/// Shared implementation to handle refresh capability on cubit -mixin RefreshValueCubitMixin on ValueCubitMixin { - /// Refresh the cubit state. - Future refresh() async { - await perform(emitValues); - } - - /// Init the state of cubit. - void clear() { - emit(PendingState()); - } - - /// Get the value here and emit a [ValueState] if success. - @protected - Future emitValues(); -} - -@Deprecated( - 'CubitValueStateMixin will be dropped in 2.0, use ValueCubitMixin instead.') -typedef CubitValueStateMixin = ValueCubitMixin; - -/// Shared implementation of [perform]. -mixin ValueCubitMixin on BlocBase> { - /// Ensure that [perform] executions are sequential. - final _performValueCubitLock = Lock(reentrant: true); - - /// Handle states (waiting, refreshing, error...) while an [action] is - /// processed. - /// If [errorAsState] is `true` and [action] raise an exception then an - /// [ErrorState] is emitted. if `false`, nothing is emitted. The exception - /// is always rethrown by [perform] to be handled by the caller. - @protected - Future perform(FutureOr Function() action, - {bool errorAsState = true}) => - _performValueCubitLock.synchronized( - () => performOnState( - state: () => state, - emitter: (state) { - if (!isClosed) { - emit(state); - } - }, - action: (state, emitter) => action()), - ); - - /// Return `true` when a [ReadyState] is emitted. - /// Return `false` if this bloc is closed before a [ReadyState] is emitted. - Future waitReady() async { - if (state is! ReadyState) { - final result = await stream.firstWhere((state) => state is ReadyState, - orElse: () => PendingState()); - - return result is ReadyState; - } - - return true; - } -} - -/// Execute [ValueCubitMixin.perform] on each cubit of a list. -/// Useful for cubits that are suscribed to others. -@Deprecated('This feature will be dropped in 2.0.') -Future performOnIterable( - Iterable cubits, FutureOr Function() action, - {bool errorAsState = true}) async { - if (cubits.isEmpty) { - return await action(); - } - - return performOnIterable(cubits.skip(1), - () => cubits.first.perform(action, errorAsState: errorAsState), - errorAsState: errorAsState); -} diff --git a/packages/value_cubit/lib/src/extensions.dart b/packages/value_cubit/lib/src/extensions.dart deleted file mode 100644 index e186c82..0000000 --- a/packages/value_cubit/lib/src/extensions.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'dart:async'; - -import 'package:bloc/bloc.dart'; -import 'package:meta/meta.dart'; -import 'package:stream_transform/stream_transform.dart'; -import 'package:value_state/value_state.dart'; - -import 'cubit.dart'; - -/// Extensions for cubit to -extension StateAndStream on Cubit { - /// Get a new stream with current state as first value and the following - /// values - Stream get behaviorSubject => Stream.value(state).followedBy(stream); -} - -/// This mixin help to listen a stream an then update the current cubit -mixin StreamInputCubitMixin on ValueCubit { - late StreamSubscription _refreshStreamSubscription; - - /// Listen the [stream] and call [emitValuesFromStream] for every event. - @protected - void listenRefreshStream(Stream stream, - Future Function(EVENT event) emitValuesFromStream) { - _refreshStreamSubscription = stream.listen(emitValuesFromStream); - } - - @override - Future close() async { - await _refreshStreamSubscription.cancel(); - return super.close(); - } - - /// Helper to map from a state to other state. Useful to map "default" states - /// from original stream. - /// The [map] argument contains a function that map the origin event from the - /// stream to the value. If `null` is returned, then a [NoValueState] is - /// emitted. Else a [ValueState] is emitted with the value returned inside. - /// [fromState] is the origin state to map. - /// If the optional parameter [refreshingWithCurrentState] is `true` (default - /// value), then the cubit emit the current state refreshing if original - /// stream emit a refreshing state. Else, the refreshing is mapped from - /// original stream. - /// [mapInit], [mapPending], [mapNoValue] and [mapError] override the default - /// behavior of the mapper. - void emitMappedState( - T? Function(F from) map, - BaseState fromState, { - bool refreshingWithCurrentState = true, - WaitingMapperType? mapInit, - WaitingMapperType? mapPending, - RefreshingyMapperType? mapNoValue, - ErrorMapperType? mapError, - }) { - emit(mapState( - map, - fromState, - currentState: refreshingWithCurrentState ? state : null, - mapInit: mapInit, - mapPending: mapPending, - mapNoValue: mapNoValue, - mapError: mapError, - )); - } -} diff --git a/packages/value_cubit/lib/value_cubit.dart b/packages/value_cubit/lib/value_cubit.dart deleted file mode 100644 index a6d3d14..0000000 --- a/packages/value_cubit/lib/value_cubit.dart +++ /dev/null @@ -1,6 +0,0 @@ -library value_cubit; - -export 'package:value_state/value_state.dart'; - -export 'src/cubit.dart'; -export 'src/extensions.dart'; diff --git a/packages/value_cubit/pubspec.yaml b/packages/value_cubit/pubspec.yaml deleted file mode 100644 index f6b7f77..0000000 --- a/packages/value_cubit/pubspec.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: value_cubit -description: A dart package that helps implements basic states for BLoC library -repository: https://github.com/devobs/value_state -homepage: https://github.com/devobs -version: 1.3.3 - -environment: - sdk: ">=2.17.1 <3.0.0" - -dependencies: - bloc: ^8.1.0 - - meta: ^1.7.0 - stream_transform: ^2.0.0 - synchronized: ^3.0.0 - - value_state: ^1.5.1 -dev_dependencies: - lints: ^2.0.0 - test: ^1.20.1 diff --git a/packages/value_cubit/test/cubit.dart b/packages/value_cubit/test/cubit.dart deleted file mode 100644 index b83328a..0000000 --- a/packages/value_cubit/test/cubit.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:test/expect.dart'; -import 'package:value_cubit/value_cubit.dart'; - -class CounterCubit extends RefreshValueCubit { - var _value = 0; - Future _getMyValueFromRepository() async => _value++; - - @override - Future emitValues() => incrementValue(); - - Future incrementValue() async { - final result = await _getMyValueFromRepository(); - - switch (result) { - case 2: - emit(const NoValueState()); - break; - case 3: - case 4: - case 6: - fail('Error'); - default: - emit(ValueState(result)); - } - } -} - -void cubitStandardActions(CounterCubit counterCubit) { - counterCubit.refresh(); - counterCubit.refresh(); - counterCubit.refresh(); - counterCubit.refresh().onError((error, stackTrace) { - // Ignore error - }); - counterCubit.refresh().onError((error, stackTrace) { - // Ignore error - }); - counterCubit.refresh(); - counterCubit.refresh().onError((error, stackTrace) { - // Ignore error - }); - counterCubit.refresh(); - counterCubit.refresh().then((_) { - counterCubit.clear(); - }); -} diff --git a/packages/value_cubit/test/helpers_cubit_test.dart b/packages/value_cubit/test/helpers_cubit_test.dart deleted file mode 100644 index 083b284..0000000 --- a/packages/value_cubit/test/helpers_cubit_test.dart +++ /dev/null @@ -1,275 +0,0 @@ -import 'package:test/test.dart'; -import 'package:value_cubit/value_cubit.dart'; - -import 'cubit.dart'; - -class CounterCubitListener extends ValueCubit - with StreamInputCubitMixin> { - CounterCubitListener( - {required CounterCubit counterCubit, required bool variant}) { - var ignoreMapError = false; - listenRefreshStream(counterCubit.behaviorSubject, (state) async { - if (!ignoreMapError && state is WithValueState && state.value > 4) { - ignoreMapError = true; - } - - emitMappedState( - (from) => variant && from == 1 ? null : from + 1, state, - refreshingWithCurrentState: !variant, - mapError: variant && !ignoreMapError - ? (errorState) { - return NoValueState(refreshing: errorState.refreshing); - } - : null, - mapNoValue: variant - ? (refreshing) { - return ValueState(-1, refreshing: refreshing); - } - : null); - }); - } -} - -void main() { - late CounterCubit counterCubit; - late CounterCubitListener counterCubitListener; - late CounterCubitListener counterCubitListener2; - - setUp(() { - counterCubit = CounterCubit(); - counterCubitListener = - CounterCubitListener(counterCubit: counterCubit, variant: false); - counterCubitListener2 = - CounterCubitListener(counterCubit: counterCubit, variant: true); - }); - - tearDown(() async { - await counterCubitListener2.close(); - await counterCubitListener.close(); - await counterCubit.close(); - }); - - test('with values incremented', () { - expect(counterCubitListener.state, isA>()); - cubitStandardActions(counterCubit); - - expect( - counterCubitListener.stream, - emitsInOrder([ - isA>() - .having((state) => state.refreshing, 'first value not refreshing', - false) - .having((state) => state.value, 'first value', 1), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', true) - .having((state) => state.value, 'second value', 1), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', false) - .having((state) => state.value, 'second value', 2), - // refresh with no value after value - isA>() - .having( - (state) => state.refreshing, 'second value refreshing', true) - .having((state) => state.value, 'second value', 2), - isA>() - .having((state) => state.refreshing, 'no value', false), - isA>() - .having((state) => state.refreshing, 'no value refreshing', true), - isA>() - .having((state) => state.refreshing, - 'error for third value not refreshing', false) - .having( - (state) => state.stateBeforeError, - 'no value before erreur', - isA>() - .having((state) => state.refreshing, 'no value', false), - ) - .having((state) => state.hasValue, 'second value before erreur', - false), - // refresh with error after error - isA>().having( - (state) => state.refreshing, - 'error for third value refreshing', - true), - isA>() - .having((state) => state.refreshing, - 'error for fourth value not refreshing', false) - .having( - (state) => state.stateBeforeError, - 'no value before erreur', - isA>() - .having((state) => state.refreshing, 'no value', false), - ) - .having((state) => state.hasValue, 'second value before erreur', - false), - // refresh after arror - isA>().having( - (state) => state.refreshing, - 'error for fourth value refreshing', - true), - isA>() - .having((state) => state.refreshing, 'fifth value not refreshing', - false) - .having((state) => state.value, 'fifth value ', 6), - isA>() - .having( - (state) => state.refreshing, 'fifth value refreshing', true) - .having((state) => state.value, 'fifth value', 6), - isA>() - .having((state) => state.refreshing, - 'error for sixth value refreshing', false) - .having( - (state) => state.hasValue, 'error for sixth has value', true) - .having((state) => state.value, - 'error for sixth value refreshing', 6), - isA>().having((state) => state.refreshing, - 'error for sixth value refreshing', true), - isA>() - .having((state) => state.refreshing, - 'seventh value not refreshing', false) - .having((state) => state.value, 'seventh value', 8), - isA>() - .having( - (state) => state.refreshing, 'seventh value refreshing', true) - .having((state) => state.value, 'seventh value', 8), - isA>() - .having((state) => state.refreshing, - 'eighth value not refreshing', false) - .having((state) => state.value, 'eighth value', 9), - // after _myRefresh.clear() triggered - isA>(), - ])); - }); - - test('with values incremented and variant', () { - expect(counterCubitListener2.state, isA>()); - cubitStandardActions(counterCubit); - - expect( - counterCubitListener2.stream, - emitsInOrder([ - isA>() - .having((state) => state.refreshing, 'first value not refreshing', - false) - .having((state) => state.value, 'first value', 1), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', true) - .having((state) => state.value, 'second value', 1), - isA>().having((state) => state.refreshing, - 'error for fourth value not refreshing', false), - isA>().having((state) => state.refreshing, - 'error for fourth value refreshing', true), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', false) - .having((state) => state.value, 'second value', -1), - isA>() - .having( - (state) => state.refreshing, 'second value refreshing', true) - .having((state) => state.value, 'second value', -1), - // refresh with error after error - isA>().having((state) => state.refreshing, - 'error for fourth value not refreshing', false), - isA>().having((state) => state.refreshing, - 'error for fourth value refreshing', true), - isA>().having((state) => state.refreshing, - 'error for fourth value not refreshing', false), - isA>().having((state) => state.refreshing, - 'error for fourth value refreshing', true), - // refresh after arror - isA>() - .having((state) => state.refreshing, 'fifth value not refreshing', - false) - .having((state) => state.value, 'fifth value ', 6), - isA>() - .having( - (state) => state.refreshing, 'fifth value refreshing', true) - .having((state) => state.value, 'fifth value', 6), - isA>() - .having((state) => state.refreshing, - 'error for sixth value refreshing', false) - .having( - (state) => state.hasValue, 'error for sixth has value', true) - .having((state) => state.value, - 'error for sixth value refreshing', 6), - isA>().having((state) => state.refreshing, - 'error for sixth value refreshing', true), - isA>() - .having((state) => state.refreshing, - 'seventh value not refreshing', false) - .having((state) => state.value, 'seventh value', 8), - isA>() - .having( - (state) => state.refreshing, 'seventh value refreshing', true) - .having((state) => state.value, 'seventh value', 8), - isA>() - .having((state) => state.refreshing, - 'eighth value not refreshing', false) - .having((state) => state.value, 'eighth value', 9), - // after _myRefresh.clear() triggered - isA>(), - ])); - }); - - test('performIterable', () { - final counterCubit2 = CounterCubit(); - - expect(counterCubit2.state, isA>()); - - expect( - // ignore: deprecated_member_use_from_same_package - performOnIterable([counterCubit, counterCubit2], () async { - await counterCubit.refresh(); - await counterCubit2.refresh(); - - return 'Success'; - }).then((res) { - // ignore: deprecated_member_use_from_same_package - return performOnIterable([counterCubit, counterCubit2], () async { - await counterCubit.refresh(); - - return res; - }); - }), - completion('Success'), - ); - - expect( - counterCubit.stream, - emitsInOrder([ - const PendingState(), - isA>() - .having((state) => state.refreshing, 'first value not refreshing', - false) - .having((state) => state.value, 'first value', 0), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', true) - .having((state) => state.value, 'second value', 0), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', false) - .having((state) => state.value, 'second value', 1), - ])); - - expect( - counterCubit2.stream, - emitsInOrder([ - isA>() - .having((state) => state.refreshing, 'first value not refreshing', - false) - .having((state) => state.value, 'first value', 0), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', true) - .having((state) => state.value, 'second value', 0), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', false) - .having((state) => state.value, 'second value not changed', 0), - ])); - }); -} diff --git a/packages/value_cubit/test/value_cubit_test.dart b/packages/value_cubit/test/value_cubit_test.dart deleted file mode 100644 index 28c4242..0000000 --- a/packages/value_cubit/test/value_cubit_test.dart +++ /dev/null @@ -1,211 +0,0 @@ -import 'package:test/test.dart'; -import 'package:value_state/value_state.dart'; - -import 'cubit.dart'; - -void main() { - late CounterCubit counterCubit; - - setUp(() { - counterCubit = CounterCubit(); - }); - - tearDown(() async { - await counterCubit.close(); - }); - - test('with values incremented', () { - expect(counterCubit.state, isA>()); - - cubitStandardActions(counterCubit); - - // Ensure the current state is [WaitingState] instead of [InitState] - expect(counterCubit.state, isNot(isA>())); - expect(counterCubit.state, isA>()); - - expect( - counterCubit.stream, - emitsInOrder([ - isA>() - .having((state) => state.refreshing, 'first value not refreshing', - false) - .having((state) => state.value, 'first value', 0), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', true) - .having((state) => state.value, 'second value', 0), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', false) - .having((state) => state.value, 'second value', 1), - // refresh with no value after value - isA>() - .having( - (state) => state.refreshing, 'second value refreshing', true) - .having((state) => state.value, 'second value', 1), - isA>() - .having((state) => state.refreshing, 'no value', false), - isA>() - .having((state) => state.refreshing, 'no value refreshing', true), - isA>() - .having((state) => state.refreshing, - 'error for third value not refreshing', false) - .having( - (state) => state.stateBeforeError, - 'no value before erreur', - isA>() - .having((state) => state.refreshing, 'no value', false), - ) - .having((state) => state.hasValue, 'second value before erreur', - false), - // refresh with error after error - isA>().having( - (state) => state.refreshing, - 'error for third value refreshing', - true), - isA>() - .having((state) => state.refreshing, - 'error for fourth value not refreshing', false) - .having( - (state) => state.stateBeforeError, - 'no value before erreur', - isA>() - .having((state) => state.refreshing, 'no value', false), - ) - .having((state) => state.hasValue, 'second value before erreur', - false), - // refresh after arror - isA>().having( - (state) => state.refreshing, - 'error for fourth value refreshing', - true), - isA>() - .having((state) => state.refreshing, 'fifth value not refreshing', - false) - .having((state) => state.value, 'fifth value ', 5), - isA>() - .having( - (state) => state.refreshing, 'fifth value refreshing', true) - .having((state) => state.value, 'fifth value', 5), - isA>() - .having((state) => state.refreshing, - 'error for sixth value refreshing', false) - .having( - (state) => state.hasValue, 'error for sixth has value', true) - .having((state) => state.value, - 'error for sixth value refreshing', 5), - isA>().having((state) => state.refreshing, - 'error for sixth value refreshing', true), - isA>() - .having((state) => state.refreshing, - 'seventh value not refreshing', false) - .having((state) => state.value, 'seventh value', 7), - isA>() - .having( - (state) => state.refreshing, 'seventh value refreshing', true) - .having((state) => state.value, 'seventh value', 7), - isA>() - .having((state) => state.refreshing, - 'eighth value not refreshing', false) - .having((state) => state.value, 'eighth value', 8), - // after _myRefresh.clear() triggered - isA>(), - ])); - }); - - test('equalities and hash', () { - // Dont create object with [const] to avoid [identical] return true - const initState1 = InitState(), initState2 = InitState(); - - expect(initState1, initState2); - expect(initState1.hashCode, initState2.hashCode); - - const waitingState1 = PendingState(), - waitingState2 = PendingState(); - - expect(waitingState1, waitingState2); - expect(waitingState1.hashCode, waitingState2.hashCode); - - expect(waitingState1.mayRefreshing(), waitingState1); - expect(waitingState1.mayNotRefreshing(), waitingState2); - - const noValueState1 = NoValueState(), - noValueState2 = NoValueState(); - - expect(noValueState1, noValueState2); - expect(noValueState1.hashCode, noValueState2.hashCode); - - const valueState1 = ValueState(0), valueState2 = ValueState(0); - - expect(valueState1, valueState2); - expect(valueState1.hashCode, valueState2.hashCode); - - final errorState1 = - ErrorState(previousState: const InitState(), error: 'Error'), - errorState2 = - ErrorState(previousState: const InitState(), error: 'Error'); - - expect(errorState1, errorState2); - expect(errorState1.hashCode, errorState2.hashCode); - - final errorStateWithValue1 = - ErrorState(previousState: const ValueState(1), error: 'Error'), - errorStateWithValue2 = - ErrorState(previousState: const ValueState(1), error: 'Error'); - - expect(errorStateWithValue1, errorStateWithValue2); - expect(errorStateWithValue1.hashCode, errorStateWithValue2.hashCode); - }); - - test('waitReady', () async { - expect(counterCubit.state, isA>()); - - counterCubit.refresh(); - - final result = await counterCubit.waitReady(); - - expect(result, isTrue); - expect(counterCubit.state, isA>()); - }); - - test('waitReady', () async { - expect(counterCubit.state, isA>()); - - counterCubit.close(); - - final result = await counterCubit.waitReady(); - - expect(result, isFalse); - expect(counterCubit.state, isNot(isA>())); - }); - - test('visitor', () { - const visitor = _TestStateVisitor(); - - expect(const InitState().accept(visitor), 1); - expect(const PendingState().accept(visitor), 4); - expect(const NoValueState().accept(visitor), 2); - expect(const ValueState(0).accept(visitor), 3); - expect( - ErrorState(previousState: const InitState(), error: 'Error') - .accept(visitor), - 0); - }); -} - -class _TestStateVisitor extends StateVisitor { - const _TestStateVisitor(); - - @override - visitInitState(InitState state) => 1; - @override - visitPendingState(PendingState state) => 4; - - @override - visitValueState(ValueState state) => 3; - @override - visitNoValueState(NoValueState state) => 2; - - @override - visitErrorState(ErrorState state) => 0; -} diff --git a/packages/value_state/CHANGELOG.md b/packages/value_state/CHANGELOG.md index 8981332..a70fda1 100644 --- a/packages/value_state/CHANGELOG.md +++ b/packages/value_state/CHANGELOG.md @@ -1,3 +1,21 @@ +## 2.0.0 + + - Graduate package to a stable release. See pre-releases prior to this version for changelog entries. + - **DOCS**: added topics. + + +## 2.0.0-beta.1 + +> Note: This release has breaking changes. + + - **DOCS**: simplify README.md. + +## 2.0.0-beta.0 + +> Note: This release has breaking changes. + + - **BREAKING** **REFACTOR**: class ValueState is replaced by Value. + ## 1.5.1 - **FIX**: close stream in perform. diff --git a/packages/value_state/README.md b/packages/value_state/README.md index 8ce0edc..7b0d31d 100644 --- a/packages/value_state/README.md +++ b/packages/value_state/README.md @@ -1,99 +1,98 @@ -A dart package that helps to implement basic states for [BLoC library](https://pub.dev/packages/bloc) to perform, load and fetch data. +## value_state +[![pub package](https://img.shields.io/pub/v/value_state.svg)](https://pub.dev/packages/value_state) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -[![pub package](https://img.shields.io/pub/v/value_state.svg)](https://pub.dev/packages/value_state) -[![Test](https://github.com/devobs/value_state/actions/workflows/test.yml/badge.svg)](https://github.com/devobs/value_state/actions/workflows/test.yml) -[![codecov](https://codecov.io/gh/devobs/value_state/branch/main/graph/badge.svg)](https://app.codecov.io/gh/devobs/value_state/tree/main/packages/value_state) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Test](https://github.com/devobs/value_state/actions/workflows/test.yml/badge.svg)](https://github.com/devobs/value_state/actions/workflows/test.yml) [![codecov](https://codecov.io/gh/devobs/value_state/branch/main/graph/badge.svg)](https://app.codecov.io/gh/devobs/value_state/tree/main/packages/value_state) -## Features +A dart package that helps to implement basic states such as initial, success and error. -* Provides all necessary states for data : init, waiting, value/no value and error states, -* Some helpers `performOnState` to emit intermediate states while an action is intended to update state : the same state is reemitted with attribute `refreshing` at `true`. +### 🔥 Features -## Usage +This package helps you manage the different states your data can have in your app (like loading, success, or error). It makes your code cleaner and easier to understand, especially when dealing with things like network requests, storage loading or complex operations. + +It provides a way to represent a value that can be in one of three states: + * initial + * success + * failure + + +### 🚀 Quick start + +```dart +final valueInitial = Value.initial(); +final state = valueInitial.state; // ValueState.initial +final isInitial = valueInitial.isInitial; // true + +final valueSuccess = Value.success(1); +final isSuccess = valueSuccess.isSuccess; // false +final isFailure = valueError.isFailure; // false +print('Data of value : ${valueSuccess.data}'); // Data of value : 1 +``` + +#### When the value is in a specific state ```dart -class CounterBehaviorSubject { - var _value = 0; - Future _getCounterValueFromRepository() async => _value++; - - Future refresh() => performOnState( - state: () => state, - emitter: _streamController.add, - action: (state, emitter) async { - final result = await _getCounterValueFromRepository(); - - if (result == 2) { - throw 'Error'; - } else if (result > 4) { - emitter(const NoValueState()); - } else { - emitter(ValueState(result)); - } - }); - - final BaseState _state = const InitState(); - BaseState get state => _state; - - final _streamController = StreamController>(); - late StreamSubscription> _streamSubscription; - - Stream> get stream => - Stream.value(state).followedBy(_streamController.stream); - - Future close() async { - await _streamSubscription.cancel(); - await _streamController.close(); - } -} - -main() async { - final counterCubit = CounterBehaviorSubject(); - - final timer = Timer.periodic(const Duration(milliseconds: 500), (_) async { - try { - await counterCubit.refresh(); - } catch (error) { - // Prevent stop execution for example - } - }); - - await for (final state in counterCubit.stream) { - if (state is ReadyState) { - print('State is refreshing: ${state.refreshing}'); - - if (state.hasError) { - print('Error'); - } - - if (state is WithValueState) { - print('Value : ${state.value}'); - } - - if (state is NoValueState) { - timer.cancel(); - print('No value'); - } - } else { - print('Waiting for value - $state'); - } - } -} +value.when( + initial: () => print('initial'), + success: (data) => print('success: $data'), + failure: (error) => print('failure: $error'), + orElse: () => print('orElse'), +); ``` -The whole code of this example is available in [example](example). +#### Map the value to a different type -## Models +```dart +valueInitial.map( + initial: () => 'initial', + success: (data) => 'success: $data', + failure: (error) => 'failure: $error', + orElse: () => 'orElse', +); +``` + +#### Merge two values with different types + +```dart +value1.merge(value2, mapData: (value) => value.length); +``` -### State diagram +#### Value error + +Map a Value to `failure` with actual data if any. There is no `Value.failure` constructor to prevent developers from forgetting to retain the data from a previous state of the Value. +```dart +final valueError = Value.initial().toFailure(Exception('error')); + +print('Data of value : ${valueError.data}'); // Data of value : null +print('Error of value : "${valueError.error}"'); // Error of value : "Exception: error" +``` + +The new value from call `toFailure` on `valueSuccess` keep previous `data`. It provides a simple way to display both error and previous data (for example a refresh failure). +```dart +final valueErrorWithPreviousData = valueSuccess.toFailure(Exception('error')); + +print('Data of value : ${valueErrorWithPreviousData.data}'); // Data of value : 1 +print('Error of value : "${valueErrorWithPreviousData.error}"'); // Error of value : "Exception: error" +``` + +#### Handle states (isFetching, success, error...) while an action is processed + +```dart +const value = Value.initial(); +print(value); +value.fetchFrom(() async => "result").forEach(print); +// Result : +// Value(state: ValueState.initial, isFetching: false) +// Value(state: ValueState.initial, isFetching: true) +// Value(state: ValueState.success, isFetching: false, data: result) +``` -![State diagram](https://github.com/devobs/value_state/blob/main/packages/value_state/doc/state_diagram.png?raw=true) +### License -### Class diagram +MIT License -![Class diagram](https://github.com/devobs/value_state/blob/main/packages/value_state/doc/class_diagram.png?raw=true) +See the [LICENSE](https://www.google.com/url?sa=E&source=gmail&q=https://www.google.com/url?sa=E%26source=gmail%26q=LICENSE) file for details. -## Feedback +### Feedback -Please file any issues, bugs or feature requests as an issue on the [Github page](https://github.com/devobs/value_state/issues). +Please file any issues, bugs or feature requests as an issue on the [Github page](https://github.com/devobs/value_state/issues). \ No newline at end of file diff --git a/packages/value_state/analysis_options.yaml b/packages/value_state/analysis_options.yaml index 0caccc8..17ccefe 100644 --- a/packages/value_state/analysis_options.yaml +++ b/packages/value_state/analysis_options.yaml @@ -1 +1,7 @@ -include: ../analysis_options.yaml \ No newline at end of file +include: package:lints/recommended.yaml + +linter: + rules: + - prefer_const_constructors + - prefer_const_declarations + - prefer_single_quotes diff --git a/packages/value_state/doc/class_diagram.mermaid b/packages/value_state/doc/class_diagram.mermaid deleted file mode 100644 index a257fc9..0000000 --- a/packages/value_state/doc/class_diagram.mermaid +++ /dev/null @@ -1,52 +0,0 @@ -classDiagram - class BaseState { - <> - } - class WaitingState { - <> - } - BaseState <|-- WaitingState - class InitState - WaitingState <|-- InitState - WaitingState <|-- PendingState - class ReadyState { - <> - refreshing: bool - hasError: bool - hasValue: bool - } - BaseState <|-- ReadyState - class NoValueState { - hasError = false - hasValue = false - } - class ValueState { - value: T - hasError = false - hasValue = true - } - class ErrorState { - <> - error: Object - stackTrace: StackTrace? - hasError = true - } - ReadyState <|-- NoValueState - ReadyState <|-- ValueState - ReadyState <|-- ErrorState - ErrorState "*" --> "1" BaseState : stateBeforeError - class ErrorWithValueState { - value: T - hasValue = true - } - class ErrorWithoutValueState { - hasValue = false - } - ErrorState <|-- ErrorWithValueState - ErrorState <|-- ErrorWithoutValueState - class WithValueState { - <> - value: T - } - WithValueState <|.. ValueState - WithValueState <|.. ErrorWithValueState \ No newline at end of file diff --git a/packages/value_state/doc/class_diagram.png b/packages/value_state/doc/class_diagram.png deleted file mode 100644 index 2d326f6..0000000 Binary files a/packages/value_state/doc/class_diagram.png and /dev/null differ diff --git a/packages/value_state/doc/state_diagram.mermaid b/packages/value_state/doc/state_diagram.mermaid deleted file mode 100644 index 87e62a9..0000000 --- a/packages/value_state/doc/state_diagram.mermaid +++ /dev/null @@ -1,15 +0,0 @@ -stateDiagram-v2 - [*] --> WaitingState - state WaitingState { - InitState --> PendingState - } - WaitingState --> ReadyState - ReadyState --> PendingState - state ReadyState { - ValueState --> NoValueState - ValueState --> ErrorState - ErrorState --> NoValueState - ErrorState --> ValueState - NoValueState --> ValueState - NoValueState --> ErrorState - } \ No newline at end of file diff --git a/packages/value_state/doc/state_diagram.png b/packages/value_state/doc/state_diagram.png deleted file mode 100644 index da9e188..0000000 Binary files a/packages/value_state/doc/state_diagram.png and /dev/null differ diff --git a/packages/flutter_value_state/example/.gitignore b/packages/value_state/example/.gitignore similarity index 100% rename from packages/flutter_value_state/example/.gitignore rename to packages/value_state/example/.gitignore diff --git a/packages/flutter_value_state/example/.metadata b/packages/value_state/example/.metadata similarity index 100% rename from packages/flutter_value_state/example/.metadata rename to packages/value_state/example/.metadata diff --git a/packages/flutter_value_state/example/README.md b/packages/value_state/example/README.md similarity index 64% rename from packages/flutter_value_state/example/README.md rename to packages/value_state/example/README.md index 5891114..ca32683 100644 --- a/packages/flutter_value_state/example/README.md +++ b/packages/value_state/example/README.md @@ -1,6 +1,6 @@ # flutter_state_value_examples -A basic example of flutter value state usage. +A basic example of value state usage with flutter and blocs. ## Getting Started diff --git a/packages/value_state/example/analysis_options.yaml b/packages/value_state/example/analysis_options.yaml new file mode 100644 index 0000000..57644e4 --- /dev/null +++ b/packages/value_state/example/analysis_options.yaml @@ -0,0 +1,15 @@ +include: package:flutter_lints/flutter.yaml + +analyzer: + plugins: + - custom_lint + +custom_lint: + rules: + - missing_provider_scope: false + +linter: + rules: + - prefer_const_constructors + - prefer_const_declarations + - prefer_single_quotes diff --git a/packages/value_state/example/lib/logic/counter_cubit.dart b/packages/value_state/example/lib/logic/counter_cubit.dart new file mode 100644 index 0000000..4bc31cb --- /dev/null +++ b/packages/value_state/example/lib/logic/counter_cubit.dart @@ -0,0 +1,13 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:value_state/value_state.dart'; + +import 'repository.dart'; + +class CounterCubit extends Cubit> { + CounterCubit() : super(const Value.initial()); + + final _myRepository = MyRepository(); + + Future increment() => + state.fetchFrom(_myRepository.getValue).forEach(emit); +} diff --git a/packages/value_state/example/lib/logic/counter_notifier.dart b/packages/value_state/example/lib/logic/counter_notifier.dart new file mode 100644 index 0000000..cd4cbd8 --- /dev/null +++ b/packages/value_state/example/lib/logic/counter_notifier.dart @@ -0,0 +1,31 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:value_state/value_state.dart'; + +import 'repository.dart'; + +part 'counter_notifier.g.dart'; + +@riverpod +MyRepository myRepository(Ref ref) => MyRepository(); + +@riverpod +Future counter(Ref ref) => ref.watch(myRepositoryProvider).getValue(); + +/// Simple extension to map [AsyncValue] to [Value] that you can include in +/// your project. +extension AsyncValueX on AsyncValue { + Value mapToValue() => map( + data: (data) => Value.success(data.value, isFetching: isLoading), + error: (error) => switch (error) { + AsyncError(:final value?) => Value.success(value) + .toFailure(error, stackTrace: stackTrace, isFetching: isLoading), + _ => Value.initial().toFailure( + error, + stackTrace: stackTrace, + isFetching: isLoading, + ), + }, + loading: (loading) => Value.initial(isFetching: isLoading), + ); +} diff --git a/packages/value_state/example/lib/logic/counter_notifier.g.dart b/packages/value_state/example/lib/logic/counter_notifier.g.dart new file mode 100644 index 0000000..ed7ebf1 --- /dev/null +++ b/packages/value_state/example/lib/logic/counter_notifier.g.dart @@ -0,0 +1,42 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'counter_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$myRepositoryHash() => r'f60f56f53e1042b2719118d50e02a2134f095fd5'; + +/// See also [myRepository]. +@ProviderFor(myRepository) +final myRepositoryProvider = AutoDisposeProvider.internal( + myRepository, + name: r'myRepositoryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$myRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef MyRepositoryRef = AutoDisposeProviderRef; +String _$counterHash() => r'f915d09ac88f54b0ee6665ded639a2958c7461b4'; + +/// See also [counter]. +@ProviderFor(counter) +final counterProvider = AutoDisposeFutureProvider.internal( + counter, + name: r'counterProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$counterHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef CounterRef = AutoDisposeFutureProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/packages/value_state/example/lib/logic/counter_value_notifier.dart b/packages/value_state/example/lib/logic/counter_value_notifier.dart new file mode 100644 index 0000000..fd2f4cc --- /dev/null +++ b/packages/value_state/example/lib/logic/counter_value_notifier.dart @@ -0,0 +1,21 @@ +import 'package:flutter/widgets.dart'; +import 'package:value_state/value_state.dart'; + +import 'repository.dart'; + +class CounterValueNotifier extends ValueNotifier> { + CounterValueNotifier() : super(const Value.initial()); + + final _myRepository = MyRepository(); + + Future increment() => + value.fetchFrom(_myRepository.getValue).forEach(setNotifierValue); +} + +/// Add this extension on your Flutter project to make it easier to use. +extension ValueNotifierExtensions on ValueNotifier { + @protected + void setNotifierValue(Value newValue) { + value = newValue; + } +} diff --git a/packages/value_state/example/lib/logic/repository.dart b/packages/value_state/example/lib/logic/repository.dart new file mode 100644 index 0000000..d3cd9cb --- /dev/null +++ b/packages/value_state/example/lib/logic/repository.dart @@ -0,0 +1,15 @@ +class MyRepository { + var _value = 0; + + Future getValue() async { + // Emulate a network request delay + await Future.delayed(const Duration(milliseconds: 500)); + + final value = _value++; + + if (value == 2) { + throw 'Error'; + } + return value; + } +} diff --git a/packages/value_state/example/lib/main.dart b/packages/value_state/example/lib/main.dart new file mode 100644 index 0000000..ac2071d --- /dev/null +++ b/packages/value_state/example/lib/main.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:value_state/value_state.dart'; + +import 'widgets/action_button.dart'; +import 'widgets/app_root.dart'; +import 'widgets/counter_notifier.dart'; +import 'widgets/default_error.dart'; +import 'widgets/formatted_column.dart'; +import 'widgets/loader.dart'; + +// coverage:ignore-start +void main() { + runApp(const MyApp()); +} +// coverage:ignore-end + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) => + CounterNotifier(child: const AppRoot(child: MyHomePage())); +} + +class MyHomePage extends StatelessWidget { + const MyHomePage({super.key}); + + @override + Widget build(BuildContext context) => + // This example, show how to handle different states with refetching + // problematic. In this case, when an error is raised after a value has + // been successfully fetched, we can see the error and the last value + // fetched both displayed. + ValueListenableBuilder( + valueListenable: CounterNotifier.of(context), + builder: (context, state, _) { + if (state.isInitial) return const Loader(); + + return FormattedColumn(children: [ + RefreshLoader(isLoading: state.isRefetching), + if (state case Value(:final error?)) DefaultError(error: error), + if (state case Value(:final data?)) Text('Counter value : $data'), + ActionButton( + onPressed: state.isRefetching + ? null + : CounterNotifier.of(context).increment, + ), + ]); + }, + ); +} diff --git a/packages/value_state/example/lib/main_cubit.dart b/packages/value_state/example/lib/main_cubit.dart new file mode 100644 index 0000000..d7e17f5 --- /dev/null +++ b/packages/value_state/example/lib/main_cubit.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:value_state/value_state.dart'; + +import 'logic/counter_cubit.dart'; +import 'widgets/action_button.dart'; +import 'widgets/app_root.dart'; +import 'widgets/default_error.dart'; +import 'widgets/formatted_column.dart'; +import 'widgets/loader.dart'; + +// coverage:ignore-start +void main() { + runApp(const MyCubitApp()); +} +// coverage:ignore-end + +class MyCubitApp extends StatelessWidget { + const MyCubitApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => CounterCubit()..increment(), + child: const AppRoot(child: MyHomePage()), + ); + } +} + +class MyHomePage extends StatelessWidget { + const MyHomePage({super.key}); + + @override + Widget build(BuildContext context) => + // This example, show how to handle different states with refetching + // problematic. In this case, when an error is raised after a value has + // been successfully fetched, we can see the error and the last value + // fetched both displayed. + BlocBuilder>( + builder: (context, state) { + if (state.isInitial) return const Loader(); + + return FormattedColumn(children: [ + RefreshLoader(isLoading: state.isRefetching), + if (state case Value(:final error?)) DefaultError(error: error), + if (state case Value(:final data?)) Text('Counter value : $data'), + ActionButton( + onPressed: state.isRefetching + ? null + : context.read().increment, + ), + ]); + }, + ); +} diff --git a/packages/value_state/example/lib/main_riverpod.dart b/packages/value_state/example/lib/main_riverpod.dart new file mode 100644 index 0000000..318b893 --- /dev/null +++ b/packages/value_state/example/lib/main_riverpod.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:value_state/value_state.dart'; + +import 'logic/counter_notifier.dart'; +import 'widgets/action_button.dart'; +import 'widgets/app_root.dart'; +import 'widgets/default_error.dart'; +import 'widgets/formatted_column.dart'; +import 'widgets/loader.dart'; + +// coverage:ignore-start +void main() { + runApp( + const ProviderScope(child: MyRiverpodApp()), + ); +} +// coverage:ignore-end + +class MyRiverpodApp extends StatelessWidget { + const MyRiverpodApp({super.key}); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return const AppRoot(child: MyHomePage()); + } +} + +class MyHomePage extends StatelessWidget { + const MyHomePage({super.key}); + + @override + Widget build(BuildContext context) => + // This example, show how to handle different states with refetching + // problematic. In this case, when an error is raised after a value has + // been successfully fetched, we can see the error and the last value + // fetched both displayed. + // All fetch/refresh logic is handled by riverpod, this example show how + // to standardize the UI with the use of Value. + Consumer( + builder: (context, ref, _) { + final state = ref.watch(counterProvider).mapToValue(); + + if (state.isInitial) return const Loader(); + + return FormattedColumn(children: [ + RefreshLoader(isLoading: state.isRefetching), + if (state case Value(:final error?)) DefaultError(error: error), + if (state case Value(:final data?)) Text('Counter value : $data'), + ActionButton( + onPressed: state.isRefetching + ? null + : () => ref.invalidate(counterProvider), + ), + ]); + }, + ); +} diff --git a/packages/value_state/example/lib/widgets/action_button.dart b/packages/value_state/example/lib/widgets/action_button.dart new file mode 100644 index 0000000..d1da08c --- /dev/null +++ b/packages/value_state/example/lib/widgets/action_button.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class ActionButton extends StatelessWidget { + const ActionButton({ + super.key, + required this.onPressed, + }); + + final VoidCallback? onPressed; + + @override + Widget build(BuildContext context) => Center( + child: ElevatedButton( + onPressed: onPressed, + child: const Text('Increment'), + ), + ); +} diff --git a/packages/value_state/example/lib/widgets/app_root.dart b/packages/value_state/example/lib/widgets/app_root.dart new file mode 100644 index 0000000..7f45b43 --- /dev/null +++ b/packages/value_state/example/lib/widgets/app_root.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; + +class AppRoot extends StatelessWidget { + const AppRoot({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Value State Demo', + home: Scaffold( + appBar: AppBar(title: const Text('Flutter Demo Home Page')), + body: DefaultTextStyle( + style: const TextStyle(fontSize: 24), + textAlign: TextAlign.center, + child: child, + ), + ), + ); + } +} diff --git a/packages/value_state/example/lib/widgets/counter_notifier.dart b/packages/value_state/example/lib/widgets/counter_notifier.dart new file mode 100644 index 0000000..a7eef33 --- /dev/null +++ b/packages/value_state/example/lib/widgets/counter_notifier.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart'; + +import '../logic/counter_value_notifier.dart'; + +class CounterNotifier extends InheritedNotifier { + CounterNotifier({super.key, required super.child}) + : super(notifier: CounterValueNotifier()..increment()); + + static CounterValueNotifier of(BuildContext context) => + context.dependOnInheritedWidgetOfExactType()!.notifier!; +} diff --git a/packages/value_state/example/lib/widgets/default_error.dart b/packages/value_state/example/lib/widgets/default_error.dart new file mode 100644 index 0000000..e9d9536 --- /dev/null +++ b/packages/value_state/example/lib/widgets/default_error.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; + +class DefaultError extends StatelessWidget { + const DefaultError({super.key, required this.error}); + + final Object error; + + @override + Widget build(BuildContext context) { + return Center( + child: Text( + 'Expected error.', + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ); + } +} diff --git a/packages/value_state/example/lib/widgets/formatted_column.dart b/packages/value_state/example/lib/widgets/formatted_column.dart new file mode 100644 index 0000000..eb0e15e --- /dev/null +++ b/packages/value_state/example/lib/widgets/formatted_column.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class FormattedColumn extends StatelessWidget { + const FormattedColumn({super.key, required this.children}); + + final List children; + + @override + Widget build(BuildContext context) => FractionallySizedBox( + alignment: Alignment.topCenter, + heightFactor: 0.75, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: children, + ), + ); +} diff --git a/packages/value_state/example/lib/widgets/loader.dart b/packages/value_state/example/lib/widgets/loader.dart new file mode 100644 index 0000000..34f38c8 --- /dev/null +++ b/packages/value_state/example/lib/widgets/loader.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +class Loader extends StatelessWidget { + const Loader({super.key}); + + @override + Widget build(BuildContext context) => + const Center(child: CircularProgressIndicator()); +} + +class RefreshLoader extends StatelessWidget { + const RefreshLoader({super.key, required this.isLoading}); + + final bool isLoading; + + @override + Widget build(BuildContext context) { + return Visibility( + visible: isLoading, + child: const Align( + heightFactor: 0, + alignment: Alignment.topCenter, + child: LinearProgressIndicator(), + ), + ); + } +} diff --git a/packages/value_state/example/main.dart b/packages/value_state/example/main.dart deleted file mode 100644 index 6e00581..0000000 --- a/packages/value_state/example/main.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'dart:async'; - -import 'package:stream_transform/stream_transform.dart'; -import 'package:value_state/value_state.dart'; - -class CounterBehaviorSubject { - var _value = 0; - Future _getCounterValueFromRepository() async => _value++; - - Future refresh() => performOnState( - state: () => state, - emitter: _streamController.add, - action: (state, emitter) async { - final result = await _getCounterValueFromRepository(); - - if (result == 2) { - throw 'Error'; - } else if (result > 4) { - emitter(const NoValueState()); - } else { - emitter(ValueState(result)); - } - }); - - final BaseState _state = const InitState(); - BaseState get state => _state; - - final _streamController = StreamController>(); - late StreamSubscription> _streamSubscription; - - Stream> get stream => - Stream.value(state).followedBy(_streamController.stream); - - Future close() async { - await _streamSubscription.cancel(); - await _streamController.close(); - } -} - -main() async { - final counterCubit = CounterBehaviorSubject(); - - final timer = Timer.periodic(const Duration(milliseconds: 500), (_) async { - try { - await counterCubit.refresh(); - } catch (error) { - // Prevent stop execution for example - } - }); - - await for (final state in counterCubit.stream) { - if (state is ReadyState) { - print('State is refreshing: ${state.refreshing}'); - - if (state.hasError) { - print('Error'); - } - - if (state is WithValueState) { - print('Value : ${state.value}'); - } - - if (state is NoValueState) { - timer.cancel(); - print('No value'); - } - } else { - print('Waiting for value - $state'); - } - } -} diff --git a/packages/value_state/example/pubspec.yaml b/packages/value_state/example/pubspec.yaml new file mode 100644 index 0000000..da03265 --- /dev/null +++ b/packages/value_state/example/pubspec.yaml @@ -0,0 +1,35 @@ +name: value_state_example +description: A sample with a basic example + +publish_to: 'none' + +version: 1.0.0+1 + +environment: + sdk: ">=3.3.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + + flutter_bloc: ^8.1.6 + flutter_riverpod: ^2.6.1 + riverpod_annotation: ^2.6.1 + + value_state: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + flutter_lints: ^4.0.0 + riverpod_generator: ^2.6.3 + build_runner: ^2.4.13 + custom_lint: ^0.7.0 + riverpod_lint: ^2.6.3 + + meta: ^1.7.0 + +flutter: + uses-material-design: true diff --git a/packages/value_cubit/example/test/widget_test.dart b/packages/value_state/example/test/widget_test.dart similarity index 53% rename from packages/value_cubit/example/test/widget_test.dart rename to packages/value_state/example/test/widget_test.dart index b826e3c..9c8f155 100644 --- a/packages/value_cubit/example/test/widget_test.dart +++ b/packages/value_state/example/test/widget_test.dart @@ -8,26 +8,50 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_basic/main.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:meta/meta.dart'; +import 'package:value_state_example/main.dart'; +import 'package:value_state_example/main_cubit.dart'; +import 'package:value_state_example/main_riverpod.dart'; void main() { + testScenario( + 'Counter increments standard test', + const MyApp(), + ); + testScenario( + 'Counter increments cubit test', + const MyCubitApp(), + ); + testScenario( + 'Counter increments riverpod test', + const ProviderScope(child: MyRiverpodApp()), + ); +} + +@isTest +void testScenario(String name, Widget widget) { + const incrementTextButton = 'Increment'; + const expectedError = 'Expected error.'; + Finder findCounter(int count) => find.text('Counter value : $count'); + testWidgets( - 'Counter increments test', + name, (WidgetTester tester) async { // Build our app and trigger a frame. runZonedGuarded( () async { - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(widget); await tester.pumpAndSettle(); // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); + expect(findCounter(0), findsOneWidget); + expect(findCounter(1), findsNothing); expect(find.byType(LinearProgressIndicator), findsNothing); // Tap the refresh icon. - await tester.tap(find.byIcon(Icons.refresh)); + await tester.tap(find.text(incrementTextButton)); await tester.pump(); expect(find.byType(LinearProgressIndicator), findsOneWidget); @@ -35,12 +59,12 @@ void main() { await tester.pumpAndSettle(); // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - expect(find.text('Expected error.'), findsNothing); + expect(findCounter(0), findsNothing); + expect(findCounter(1), findsOneWidget); + expect(find.text(expectedError), findsNothing); // Tap the refresh icon. - await tester.tap(find.byIcon(Icons.refresh)); + await tester.tap(find.text(incrementTextButton)); await tester.pump(); expect(find.byType(LinearProgressIndicator), findsOneWidget); @@ -48,11 +72,11 @@ void main() { await tester.pumpAndSettle(); // Verify that our counter has incremented. - expect(find.text('Expected error.'), findsOneWidget); - expect(find.text('1'), findsOneWidget); + expect(find.text(expectedError), findsOneWidget); + expect(findCounter(1), findsOneWidget); // Tap the refresh icon. - await tester.tap(find.byIcon(Icons.refresh)); + await tester.tap(find.text(incrementTextButton)); await tester.pump(); expect(find.byType(LinearProgressIndicator), findsOneWidget); @@ -60,10 +84,10 @@ void main() { await tester.pumpAndSettle(); // Verify that our counter has incremented. - expect(find.text('Expected error.'), findsNothing); - expect(find.text('3'), findsOneWidget); + expect(find.text(expectedError), findsNothing); + expect(findCounter(3), findsOneWidget); - await tester.tap(find.byIcon(Icons.refresh)); + await tester.tap(find.text(incrementTextButton)); await tester.pump(); expect(find.byType(LinearProgressIndicator), findsOneWidget); @@ -71,10 +95,10 @@ void main() { await tester.pumpAndSettle(); // Verify that our counter has incremented. - expect(find.text('Expected error.'), findsNothing); - expect(find.text('4'), findsOneWidget); + expect(find.text(expectedError), findsNothing); + expect(findCounter(4), findsOneWidget); - await tester.tap(find.byIcon(Icons.refresh)); + await tester.tap(find.text(incrementTextButton)); await tester.pump(); expect(find.byType(LinearProgressIndicator), findsOneWidget); @@ -82,8 +106,8 @@ void main() { await tester.pumpAndSettle(); // Verify that our counter has incremented. - expect(find.text('Expected error.'), findsNothing); - expect(find.text('5'), findsNothing); + expect(find.text(expectedError), findsNothing); + expect(findCounter(5), findsOneWidget); }, (error, stack) { if (error != 'Error') { diff --git a/packages/flutter_value_state/example/web/favicon.png b/packages/value_state/example/web/favicon.png similarity index 100% rename from packages/flutter_value_state/example/web/favicon.png rename to packages/value_state/example/web/favicon.png diff --git a/packages/flutter_value_state/example/web/icons/Icon-192.png b/packages/value_state/example/web/icons/Icon-192.png similarity index 100% rename from packages/flutter_value_state/example/web/icons/Icon-192.png rename to packages/value_state/example/web/icons/Icon-192.png diff --git a/packages/flutter_value_state/example/web/icons/Icon-512.png b/packages/value_state/example/web/icons/Icon-512.png similarity index 100% rename from packages/flutter_value_state/example/web/icons/Icon-512.png rename to packages/value_state/example/web/icons/Icon-512.png diff --git a/packages/flutter_value_state/example/web/icons/Icon-maskable-192.png b/packages/value_state/example/web/icons/Icon-maskable-192.png similarity index 100% rename from packages/flutter_value_state/example/web/icons/Icon-maskable-192.png rename to packages/value_state/example/web/icons/Icon-maskable-192.png diff --git a/packages/flutter_value_state/example/web/icons/Icon-maskable-512.png b/packages/value_state/example/web/icons/Icon-maskable-512.png similarity index 100% rename from packages/flutter_value_state/example/web/icons/Icon-maskable-512.png rename to packages/value_state/example/web/icons/Icon-maskable-512.png diff --git a/packages/flutter_value_state/example/web/index.html b/packages/value_state/example/web/index.html similarity index 100% rename from packages/flutter_value_state/example/web/index.html rename to packages/value_state/example/web/index.html diff --git a/packages/flutter_value_state/example/web/manifest.json b/packages/value_state/example/web/manifest.json similarity index 100% rename from packages/flutter_value_state/example/web/manifest.json rename to packages/value_state/example/web/manifest.json diff --git a/packages/value_state/lib/src/extensions.dart b/packages/value_state/lib/src/extensions.dart index 3720f11..94cefee 100644 --- a/packages/value_state/lib/src/extensions.dart +++ b/packages/value_state/lib/src/extensions.dart @@ -1,76 +1,61 @@ -import 'perform.dart'; -import 'states.dart'; - -extension ObjectWithValueExtensions on BaseState { - /// Shortcut on [BaseState] to easily handle [WithValueState] state. It can be used in different case : - /// * To return a value - /// ```dart - /// print('Phone number : ${personState.withValue((person) => person.phone) ?? 'unknown'}'); - /// ``` - /// * To perform some action - /// ```dart - /// personState.withValue((person) => print('Phone number : ${person.phone}')); - /// ``` - /// - /// If [onlyValueState] is true, then [withValue] is trigerred only on [ValueState] state. - R? withValue(R Function(T value) onValue, {bool onlyValueState = false}) { - final state = this; - - if (state is WithValueState) { - if (!onlyValueState || !state.hasError) { - return onValue(state.value); - } - } - - return null; +import 'value.dart'; + +extension ValueExtensions on Value { + /// Provides a concise way to map a value depending on the [Value.state] of + /// this value. + /// * [initial] is called if this value is [Value.isInitial], + /// * [success] is called if this value is [Value.isSuccess], [Value.data] is + /// then available, + /// * [data] is called if this value has [Value.data] available (can occur + /// in both [ValueState.success] and [ValueState.failure] states), + /// * [failure] is called if this value is [Value.isFailure], and + /// [Value.error] is then available as parameter, + /// * [orElse] is called if none of the above match or not specified. This + /// parameter is required. + R map({ + R Function()? initial, + R Function(T data)? success, + R Function(T data)? data, + R Function(Object error)? failure, + required R Function() orElse, + }) { + final dataAvailable = data; // to make code more readable. + + return switch (this) { + Value(isInitial: true) when initial != null => initial(), + Value(:final dataOnSuccess?) when success != null => + success(dataOnSuccess), + Value(:final data?) when dataAvailable != null => dataAvailable(data), + Value(:final error?) when failure != null => failure(error), + _ => orElse(), + }; } - /// Shorcut to [withValue] with its parameter `onlyValueState` set to `true`. It is equivalent to handle only - /// [ValueState] state. - R? whenValue(R Function(T value) onValue) => - withValue(onValue, onlyValueState: true); - - /// Shorcut to [withValue] which return the value if avaible. [onlyValueState] is the same as [withValue]. - T? toValue({bool onlyValueState = false}) => - withValue((value) => value, onlyValueState: onlyValueState); -} - -extension OrExtensions on R? { - /// Helpers to execute/return non null result on a null object. + /// Provides a concise way to execute an action or map a value depending on + /// the [Value.state] of this value. + /// * [initial] is called if this value is [Value.isInitial], + /// * [success] is called if this value is [Value.isSuccess], [Value.data] is + /// then available as parameter, + /// * [data] is called if this value has [Value.data] available (can occur + /// in both [ValueState.success] and [ValueState.failure] states), + /// * [failure] is called if this value is [Value.isFailure], and + /// [Value.error] is then available as parameter, + /// * [orElse] is called if none of the above match or not specified. /// - /// Example : - /// ```dart - /// personState.whenValue((person) { - /// print('Phone number : ${person.phone}'); - /// }).orElse(() { - /// print('Phone number unknown'); - /// }); - /// ``` - R orElse(R Function() elseAction) => this ?? elseAction(); -} - -extension ToReadyStateExtensions on T? { - /// Shorcut to transform to a [ReadyState] with following rules : - /// * if `this`is non null, it returns a [ValueState] - /// * else it returns a [NoValueState] - ReadyState toState({bool refreshing = false}) { - final state = this; - return state == null - ? NoValueState(refreshing: refreshing) - : ValueState(state, refreshing: refreshing); - } -} - -extension FutureValueStateExtension on Future { - /// Map a [Future] to [ReadyState] : [NoValueState] or [ValueState]. - Future> toFutureState({bool refreshing = false}) async { - final result = await this; - - if (result == null) return NoValueState(refreshing: refreshing); - return ValueState(result, refreshing: refreshing); - } - - /// Generate a stream of [BaseState] during a processing [Future]. - Stream> toStates() => - InitState().perform((_) => toFutureState()); + /// If none of those parameters are specified or does not match, then this + /// method returns `null`. + R? when({ + R Function()? initial, + R Function(T data)? success, + R Function(T data)? data, + R Function(Object error)? failure, + R Function()? orElse, + }) => + map( + initial: initial, + success: success, + data: data, + failure: failure, + orElse: orElse ?? () => null, + ); } diff --git a/packages/value_state/lib/src/fetch.dart b/packages/value_state/lib/src/fetch.dart new file mode 100644 index 0000000..5481c85 --- /dev/null +++ b/packages/value_state/lib/src/fetch.dart @@ -0,0 +1,54 @@ +import 'dart:async'; + +import 'fetch_on_value.dart'; +import 'value.dart'; + +extension ValueFetch on Value { + /// Fetch a value from a [computation] function and return a stream of values. + Stream> fetchFrom(Future Function() computation) => + fetchFromStream(() => computation().asStream().map(Value.success)); + + /// Handle values (isFetching, success, error...) before and after the + /// [streamComputation] is processed : + /// * Before the [streamComputation] is processed, the value is emitted with + /// [Value.isFetching] set to true. + /// * After the [streamComputation] is processed, the last value is emitted + /// with [Value.isFetching] set to false. + /// * If an exception is raised, an error is emitted based on the + /// [keepLastOnError] setting : + /// * If [keepLastOnError] is true, the most recent value emitted is used + /// to construct the error. + /// * If [keepLastOnError] is false, the value present before the stream + /// processing begins is used instead. + Stream> fetchFromStream( + Stream> Function() streamComputation, { + bool keepLastOnError = false, + }) { + final controller = StreamController>(); + var lastValue = this; + + fetchOnValue( + value: () => lastValue, + emitter: (value) { + lastValue = value; + controller.add(value); + }, + action: (value, emit) => streamComputation().forEach(emit), + lastValueOnError: keepLastOnError, + ).onError((error, stackTrace) { + if (error != null) { + _errorsController.add(AsyncError(error, stackTrace)); + } + }).whenComplete(() { + controller.close(); + }); + + return controller.stream; + } + + /// Stream of errors emitted by [fetchFromStream] and [fetchFrom]. + static Stream get errors => _errorsController.stream; + + static final StreamController _errorsController = + StreamController.broadcast(); +} diff --git a/packages/value_state/lib/src/fetch_on_value.dart b/packages/value_state/lib/src/fetch_on_value.dart new file mode 100644 index 0000000..7ddc573 --- /dev/null +++ b/packages/value_state/lib/src/fetch_on_value.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:value_state/value_state.dart'; + +typedef FetchOnValueEmitter = FutureOr Function( + Value value); +typedef FetchOnValueAction = FutureOr Function( + Value value, + FetchOnValueEmitter emitter, +); + +Future fetchOnValue({ + required Value Function() value, + required FetchOnValueEmitter emitter, + required FetchOnValueAction action, + required bool lastValueOnError, +}) async { + final valueBeforeFetch = value(); + + try { + final currentValue = valueBeforeFetch; + final valueFetching = currentValue.copyWithFetching(true); + + if (currentValue != valueFetching) await emitter(valueFetching); + + return await action(value(), emitter); + } catch (error, stackTrace) { + final currentValue = lastValueOnError ? value() : valueBeforeFetch; + + await emitter(currentValue.toFailure( + error, + stackTrace: stackTrace, + isFetching: false, + )); + + rethrow; + } finally { + final currentValue = value(); + final valueFetchingEnd = currentValue.copyWithFetching(false); + + if (currentValue != valueFetchingEnd) await emitter(valueFetchingEnd); + } +} diff --git a/packages/value_state/lib/src/helpers.dart b/packages/value_state/lib/src/helpers.dart deleted file mode 100644 index 1c2ffc2..0000000 --- a/packages/value_state/lib/src/helpers.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'states.dart'; - -typedef WaitingMapperType = BaseState? Function(); -typedef RefreshingyMapperType = BaseState? Function(bool refreshing); -typedef ErrorMapperType = BaseState? Function( - ErrorState errorState); - -/// Helper to map from a state to other state. Useful to map "default" states -/// from original stream. -/// The [map] argument contains a function that map the origin event from the -/// stream to the value. If `null` is returned, then a [NoValueState] is -/// emitted. Else a [ValueState] is emitted with the value returned inside. -/// [fromState] is the origin state to map. -/// If the optional parameter [currentState] is not `null` (default -/// value), then the cubit emit the current state refreshing if original -/// stream emit a refreshing state. Else, the refreshing is mapped from -/// original stream. -/// [mapInit], [mapPending], [mapNoValue] and [mapError] override the default -/// behavior of the mapper. -BaseState mapState( - T? Function(F from) map, - BaseState fromState, { - BaseState? currentState, - WaitingMapperType? mapInit, - WaitingMapperType? mapPending, - RefreshingyMapperType? mapNoValue, - ErrorMapperType? mapError, -}) { - return fromState.accept>(_MappedStateVisitor( - map, - currentState: currentState, - mapInit: mapInit, - mapPending: mapPending, - mapNoValue: mapNoValue, - mapError: mapError, - )); -} - -/// A visitor to map a state to other state. -class _MappedStateVisitor implements StateVisitor, F> { - /// Function that map the origin event from the stream to the value - /// If `null` is returned, then a [NoValueState] is emitted. Else a - /// [ValueState] is emitted with the value returned inside. - final T? Function(F from) mapValue; - - final BaseState? currentState; - - final WaitingMapperType? mapInit; - final WaitingMapperType? mapPending; - final RefreshingyMapperType? mapNoValue; - final ErrorMapperType? mapError; - - _MappedStateVisitor( - this.mapValue, { - required this.currentState, - required this.mapInit, - required this.mapPending, - required this.mapNoValue, - required this.mapError, - }); - - @override - BaseState visitInitState(InitState state) => - _applyMap((_) => mapInit?.call(), state) ?? InitState(); - - @override - BaseState visitPendingState(PendingState state) => - _applyMap((_) => mapPending?.call(), state) ?? PendingState(); - - @override - BaseState visitValueState(ValueState state) { - final currentStateRefreshing = _returnCurrentStateRefreshing(state); - if (currentStateRefreshing != null) { - return currentStateRefreshing; - } - - final mapped = mapValue(state.value); - - if (mapped == null) { - return NoValueState(refreshing: state.refreshing); - } - - return ValueState(mapped, refreshing: state.refreshing); - } - - @override - BaseState visitNoValueState(NoValueState state) => - _applyMap(mapNoValue, state) ?? - NoValueState(refreshing: state.refreshing); - - @override - BaseState visitErrorState(ErrorState errorState) { - if (mapError != null) { - final result = mapError!(errorState); - - if (result != null) return result; - } - - return _returnCurrentStateRefreshing(errorState) ?? - ErrorState( - error: errorState.error, - stackTrace: errorState.stackTrace, - refreshing: errorState.refreshing, - previousState: currentState ?? - errorState.stateBeforeError.accept( - _MappedStateVisitor(mapValue, - currentState: currentState, - mapInit: mapInit, - mapPending: mapPending, - mapNoValue: mapNoValue, - mapError: mapError), - ), - ); - } - - BaseState? _returnCurrentStateRefreshing(BaseState state) => - state is ReadyState && state.refreshing && currentState != null - ? - // Prefer using the current state refreshing before display the new - // value - currentState!.mayRefreshing() - : null; - - BaseState? _applyMap( - RefreshingyMapperType? mapper, BaseState state) { - if (mapper != null) { - return mapper(state is ReadyState && state.refreshing); - } - return _returnCurrentStateRefreshing(state); - } -} diff --git a/packages/value_state/lib/src/perform.dart b/packages/value_state/lib/src/perform.dart deleted file mode 100644 index f1bd7a4..0000000 --- a/packages/value_state/lib/src/perform.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'dart:async'; - -import 'states.dart'; - -typedef PerfomOnStateEmitter = FutureOr Function(BaseState state); -typedef PerfomOnStateAction = FutureOr Function( - BaseState state, - PerfomOnStateEmitter emitter, -); - -/// Handle states (waiting, refreshing, error...) while an [action] is -/// processed. -/// [state] must return the state updated. -/// If [errorAsState] is `true` and [action] raise an exception then an -/// [ErrorState] is emitted. if `false`, nothing is emitted. The exception -/// is always rethrown by [performOnState] to be handled by the caller. -Future performOnState( - {required BaseState Function() state, - required PerfomOnStateEmitter emitter, - required PerfomOnStateAction action, - bool errorAsState = true}) async { - try { - final currentState = state(); - final stateRefreshing = currentState.mayRefreshing(); - - if (currentState != stateRefreshing) await emitter(stateRefreshing); - - return await action(state(), emitter); - } catch (error, stackTrace) { - if (errorAsState) { - await emitter(ErrorState( - previousState: state().mayNotRefreshing(), - error: error, - stackTrace: stackTrace)); - } - rethrow; - } finally { - final currentState = state(); - final stateRefreshingEnd = currentState.mayNotRefreshing(); - - if (currentState != stateRefreshingEnd) await emitter(stateRefreshingEnd); - } -} - -extension ValueStatePerformExtensions on BaseState { - Stream> perform( - Future> Function(BaseState state) action) { - final controller = StreamController>(); - var lastState = this; - - performOnState( - state: () => lastState, - emitter: (state) { - lastState = state; - controller.add(state); - }, - action: (state, emit) async { - return emit(await action(state)); - }, - ).onError((error, stackTrace) { - // Will be raised in stream as [ErrorState] - }).whenComplete(() { - controller.close(); - }); - - return controller.stream; - } - - Stream> performStream( - Stream> Function(BaseState state) action) { - final controller = StreamController>(); - var lastState = this; - - performOnState( - state: () => lastState, - emitter: (state) { - lastState = state; - controller.add(state); - }, - action: (state, emit) async { - final stream = action(this); - - await stream.forEach(emit); - }, - ).onError((error, stackTrace) { - // Will be raised in stream as [ErrorState] - }).whenComplete(() { - controller.close(); - }); - - return controller.stream; - } -} diff --git a/packages/value_state/lib/src/states.dart b/packages/value_state/lib/src/states.dart deleted file mode 100644 index 0a6b1c2..0000000 --- a/packages/value_state/lib/src/states.dart +++ /dev/null @@ -1,336 +0,0 @@ -/// Base class for handling value states. -abstract class BaseState { - const BaseState(); - - /// The action is processing to get a new value or refresh it. - bool get fetching; - - /// Copy the actual object and according to the state can enable refreshing - BaseState mayRefreshing(); - - /// Copy the actual object and according to the state can disable refreshing - BaseState mayNotRefreshing(); - - /// Visitor pattern to safely enhance class capabilities - R accept(StateVisitor visitor); - - Map get _diagnosticableAttributes => {'fetching': fetching}; - - @override - String toString() { - return '$runtimeType${_prettyPrint(_diagnosticableAttributes)}'; - } - - static String _prettyPrint(Map attributes) => - '(${attributes.entries.map((entry) => '${entry.key}: ${entry.value}').join(', ')})'; -} - -/// State for waiting value and there was no [ReadyState] before. -/// Useful to handle waiting page before first value is displayed or when -/// a user is disconnected. -abstract class WaitingState extends BaseState { - const WaitingState(); - - @override - WaitingState mayRefreshing(); - - @override - WaitingState mayNotRefreshing(); - - @override - bool get fetching => true; -} - -/// Initial state before any processing. If all has been intialized and -/// the action to get the value is started, then emit a [WaitingState] -class InitState extends WaitingState { - const InitState(); - - @override - WaitingState mayRefreshing() => PendingState(); - - @override - WaitingState mayNotRefreshing() => PendingState(); - - @override - R accept(StateVisitor visitor) => visitor.visitInitState(this); - - @override - bool operator ==(other) => - identical(this, other) || runtimeType == other.runtimeType; - - @override - int get hashCode => runtimeType.hashCode; -} - -/// Initial state before any processing. If all has been intialized and -/// the action to get the value is started, then emit a [WaitingState] -class PendingState extends WaitingState { - const PendingState(); - - @override - WaitingState mayRefreshing() => this; - - @override - WaitingState mayNotRefreshing() => this; - - @override - R accept(StateVisitor visitor) => visitor.visitPendingState(this); - - @override - bool operator ==(other) => - identical(this, other) || runtimeType == other.runtimeType; - @override - int get hashCode => runtimeType.hashCode; -} - -/// Abstract class for all states that a value, no value or error has been -/// received. -abstract class ReadyState extends BaseState { - const ReadyState(); - - /// This property indicate an action is processing from a [ReadyState] to get a new state. - /// [ValueState], [NoValueState] or [ErrorState] will be emitted. - bool get refreshing; - - @override - bool get fetching => refreshing; - - /// Current state carry a value ? - bool get hasValue; - - /// Current state is an error ? - bool get hasError; - - @override - ReadyState mayRefreshing(); - - @override - ReadyState mayNotRefreshing(); -} - -/// State with no value (support null safety). -class NoValueState extends ReadyState { - const NoValueState({this.refreshing = false}); - - @override - final hasValue = false; - - @override - final hasError = false; - - @override - final bool refreshing; - - @override - ReadyState mayRefreshing() => NoValueState(refreshing: true); - - @override - ReadyState mayNotRefreshing() => NoValueState(refreshing: false); - - @override - R accept(StateVisitor visitor) => visitor.visitNoValueState(this); - - @override - bool operator ==(other) => - identical(this, other) || - runtimeType == other.runtimeType && - other is NoValueState && - refreshing == other.refreshing; - @override - int get hashCode => refreshing.hashCode; -} - -/// Abstraction of a state with a value [ValueState] or [ErrorState] -abstract class WithValueState extends ReadyState { - /// Value associated with state - T get value; -} - -/// State that provide the value. -class ValueState extends ReadyState implements WithValueState { - const ValueState(this.value, {this.refreshing = false}); - - @override - final T value; - - @override - final hasValue = true; - - @override - final hasError = false; - - @override - final bool refreshing; - - @override - ReadyState mayRefreshing() => ValueState(value, refreshing: true); - - @override - ReadyState mayNotRefreshing() => ValueState(value, refreshing: false); - - @override - R accept(StateVisitor visitor) => visitor.visitValueState(this); - - @override - bool operator ==(other) => - identical(this, other) || - runtimeType == other.runtimeType && - other is ValueState && - refreshing == other.refreshing && - value == other.value; - @override - int get hashCode => Object.hash(refreshing, value); - - @override - Map get _diagnosticableAttributes => { - ...super._diagnosticableAttributes, - 'value': value, - }; -} - -/// State for error (may be linked with a [ValueState] or not) -abstract class ErrorState extends ReadyState { - factory ErrorState({ - required BaseState previousState, - required Object error, - StackTrace? stackTrace, - bool refreshing = false, - }) { - final stateBeforeError = _consumePreviousErrors(previousState); - - if (stateBeforeError is ValueState) { - return ErrorWithPreviousValue._( - stateBeforeError: stateBeforeError, - error: error, - stackTrace: stackTrace, - refreshing: refreshing); - } - - return ErrorWithoutPreviousValue._( - stateBeforeError: stateBeforeError, - error: error, - stackTrace: stackTrace, - refreshing: refreshing); - } - - const ErrorState._({ - required this.error, - required this.stackTrace, - required this.stateBeforeError, - required this.refreshing, - }); - - /// Previous state that is not [ErrorState]. If several errors are - /// triggered, they are also ignored. - final BaseState stateBeforeError; - - /// The error object. - final Object error; - - @override - final hasError = true; - - /// The error stack trace. - final StackTrace? stackTrace; - - /// Current error has previous value - @override - bool get hasValue => stateBeforeError is ValueState; - - static BaseState _consumePreviousErrors(BaseState state) => - state is ErrorState - ? _consumePreviousErrors(state.stateBeforeError) - : state.mayNotRefreshing(); - - @override - final bool refreshing; - - @override - ReadyState mayRefreshing() => ErrorState( - previousState: stateBeforeError, - refreshing: true, - error: error, - stackTrace: stackTrace, - ); - - @override - ReadyState mayNotRefreshing() => ErrorState( - previousState: stateBeforeError, - refreshing: false, - error: error, - stackTrace: stackTrace, - ); - - @override - R accept(StateVisitor visitor) => visitor.visitErrorState(this); - - @override - bool operator ==(other) => - identical(this, other) || - runtimeType == other.runtimeType && - other is ErrorState && - refreshing == other.refreshing && - error == other.error && - stackTrace == other.stackTrace && - stateBeforeError == other.stateBeforeError; - @override - int get hashCode => - Object.hash(refreshing, error, stackTrace, stateBeforeError); - - @override - Map get _diagnosticableAttributes => { - ...super._diagnosticableAttributes, - 'error': error, - if (stackTrace != null) 'stackTrace': stackTrace, - 'stateBeforeError': stateBeforeError, - }; -} - -/// An error with a [ValueState] as previous state -class ErrorWithoutPreviousValue extends ErrorState { - const ErrorWithoutPreviousValue._({ - required Object error, - required StackTrace? stackTrace, - required BaseState stateBeforeError, - required bool refreshing, - }) : super._( - error: error, - stackTrace: stackTrace, - stateBeforeError: stateBeforeError, - refreshing: refreshing); -} - -/// An error with a [ValueState] as previous state -class ErrorWithPreviousValue extends ErrorState - implements WithValueState { - const ErrorWithPreviousValue._({ - required Object error, - required StackTrace? stackTrace, - required ValueState stateBeforeError, - required bool refreshing, - }) : super._( - error: error, - stackTrace: stackTrace, - stateBeforeError: stateBeforeError, - refreshing: refreshing); - - @override - ValueState get stateBeforeError => super.stateBeforeError as ValueState; - - @override - T get value => stateBeforeError.value; -} - -/// Visitor base class to enhance states capabilities -abstract class StateVisitor { - const StateVisitor(); - - R visitInitState(InitState state); - R visitPendingState(PendingState state); - - R visitValueState(ValueState state); - R visitNoValueState(NoValueState state); - - R visitErrorState(ErrorState state); -} diff --git a/packages/value_state/lib/src/value.dart b/packages/value_state/lib/src/value.dart new file mode 100644 index 0000000..8cb27d2 --- /dev/null +++ b/packages/value_state/lib/src/value.dart @@ -0,0 +1,209 @@ +import 'package:meta/meta.dart'; + +/// A class that represents a value that can be in one of three states: +/// * [ValueState.initial] - the initial state of the value. +/// * [ValueState.success] - the state when the value is successfully fetched. +/// * [ValueState.failure] - the state when the value has failed to fetch. +enum ValueState { + initial, + success, + failure, +} + +/// A convenient class to handle different states of a value. +/// The three states are enumerated in [ValueState]. +/// +/// [T] cannot be `null` or `void` : +/// * if you need a nullable data, use an `Optional` class pattern as type. +/// * if you want to follow excution flow with `void`, create or use a `Unit` +/// class. +final class Value { + /// Create a value in the initial state. + const Value.initial({bool isFetching = false}) + : this._( + data: null, + failure: null, + isFetching: isFetching, + ); + + /// Create a value in the success state with [data]. + const Value.success(T data, {bool isFetching = false}) + : this._( + data: data, + failure: null, + isFetching: isFetching, + ); + + /// Map a [Value] to `failure` with actual [data] if any. + /// + /// There is no [Value.failure] constructor to prevent developers from + /// forgetting to retain the [data] from a previous [Value]. + Value toFailure( + Object error, { + StackTrace? stackTrace, + bool isFetching = false, + }) => + Value._( + data: data, + failure: _Failure(error, stackTrace: stackTrace), + isFetching: isFetching, + ); + + /// Create a [Value] in the [ValueState.failure] state. + /// This is only for tests purpose. + @visibleForTesting + Value.failure( + Object error, { + StackTrace? stackTrace, + bool isFetching = false, + }) : this._( + data: null, + failure: _Failure(error, stackTrace: stackTrace), + isFetching: isFetching, + ); + + const Value._({ + required this.isFetching, + required this.data, + required _Failure? failure, + }) : _failure = failure; + + /// A new value state will be available. It can start from + /// [ValueState.initial] or a previous [ValueState.success] or + /// [ValueState.failure]. + final bool isFetching; + + /// Get data if available, otherwise return `null`. + /// [Value] can have [data] in this [state] : + /// * [ValueState.success] - when the value is successfully fetched, + /// * [ValueState.failure] - when the value has failed to `fetch` and the + /// previous [Value] has [data]. + final T? data; + + final _Failure? _failure; + + /// Get error if available, otherwise return `null`. + Object? get error => _failure?.error; + + /// Get stackTrace if available, otherwise return `null`. + StackTrace? get stackTrace => _failure?.stackTrace; + + /// Get state of the value. + ValueState get state => switch (this) { + Value(hasError: true) => ValueState.failure, + Value(hasData: true) => ValueState.success, + _ => ValueState.initial, + }; + + /// Check if the value is in the initial state. + bool get isInitial => state == ValueState.initial; + + /// Check if the value is in the success state. + /// If the generic type T is nullable, [isSuccess] will return true if the + /// data is `null`. + bool get isSuccess => state == ValueState.success; + + /// Check if the value is in the failure state. + bool get isFailure => state == ValueState.failure; + + /// Get data if the value is in the [ValueState.success] state, otherwise + /// return `null`. + T? get dataOnSuccess => isSuccess ? data : null; + + /// Get data if the value is in the [ValueState.failure] state, otherwise + /// return `null`. A [Value] in the [ValueState.failure] state can have [data] + /// if previous value before `fetch` has [data]. + T? get previousDataOnFailure => isFailure ? data : null; + + /// Check if the value has data. It is a bit different of [isSuccess] because + /// [ValueState.failure] can have data (from previous state). + bool get hasData => data != null; + + /// Check if the value has error (available only in + /// [ValueState.failure]). + bool get hasError => _failure != null; + + /// Check if the value has stack trace (available only in + /// [ValueState.failure]). + bool get hasStackTrace => _failure?.stackTrace != null; + + /// Check if the value is fecthing again : the current state is fetching with + /// a previous fetched state ([ValueState.success] or [ValueState.failure]). + bool get isRefetching => !isInitial && isFetching; + + /// Copy the actual object with fetching as [isFetching]. + Value copyWithFetching(bool isFetching) => this.isFetching != isFetching + ? Value._( + data: data, + failure: _failure, + isFetching: isFetching, + ) + : this; + + /// Merge two values with different type. It is intendend to facilitate + /// data mapping from a value to another without handling [Value.isFetching] + /// and [Value.error]/[Value.stackTrace] attributes. + Value merge( + Value from, { + Value Function(F from)? mapData, + }) => + mapData != null && from.data != null + ? mapData(from.data!).copyWithFetching(from.isFetching) + : Value._( + data: this.data, + failure: from._failure, + isFetching: from.isFetching, + ); + + @override + bool operator ==(Object other) => + identical(this, other) || + runtimeType == other.runtimeType && + other is Value && + isFetching == other.isFetching && + data == other.data && + _failure == other._failure; + + @override + int get hashCode => Object.hash(data, _failure, isFetching); + + @override + String toString() { + final prettyPrint = _attributes.entries + .where((entry) => entry.value?.toString().isNotEmpty ?? false) + .map((entry) => '${entry.key}: ${entry.value}') + .join(', '); + + return '$runtimeType($prettyPrint)'; + } + + Map get _attributes => { + 'state': state, + 'isFetching': isFetching, + if (!isInitial) 'data': data, + ...?_failure?._attributes, + }; +} + +final class _Failure { + const _Failure(this.error, {this.stackTrace}); + + final Object error; + final StackTrace? stackTrace; + + @override + bool operator ==(Object other) => + identical(this, other) || + runtimeType == other.runtimeType && + other is _Failure && + error == other.error && + stackTrace == other.stackTrace; + + @override + int get hashCode => Object.hash(error, stackTrace); + + Map get _attributes => { + 'error': error, + 'stackTrace': stackTrace, + }; +} diff --git a/packages/value_state/lib/value_state.dart b/packages/value_state/lib/value_state.dart index d62aa34..f73faca 100644 --- a/packages/value_state/lib/value_state.dart +++ b/packages/value_state/lib/value_state.dart @@ -1,6 +1,3 @@ -library value_state; - export 'src/extensions.dart'; -export 'src/helpers.dart'; -export 'src/perform.dart'; -export 'src/states.dart'; +export 'src/fetch.dart'; +export 'src/value.dart'; diff --git a/packages/value_state/pubspec.yaml b/packages/value_state/pubspec.yaml index ba10a5f..55034e0 100644 --- a/packages/value_state/pubspec.yaml +++ b/packages/value_state/pubspec.yaml @@ -1,17 +1,17 @@ name: value_state -description: A dart package that helps implements basic states for BLoC library +description: Dart package for handling loading, error, and data states. +version: 2.0.0 repository: https://github.com/devobs/value_state homepage: https://github.com/devobs -version: 1.5.1 +topics: [error, fetch, init, state, success] environment: - sdk: ">=2.17.1 <3.0.0" + sdk: ^3.4.0 dependencies: meta: ^1.7.0 stream_transform: ^2.0.0 - synchronized: ^3.0.0 dev_dependencies: - lints: ^2.0.0 + lints: ^5.0.0 test: ^1.20.1 diff --git a/packages/value_state/test/extensions_test.dart b/packages/value_state/test/extensions_test.dart index 578aee2..2cedef3 100644 --- a/packages/value_state/test/extensions_test.dart +++ b/packages/value_state/test/extensions_test.dart @@ -2,168 +2,58 @@ import 'package:test/test.dart'; import 'package:value_state/value_state.dart'; void main() { + const myStrInitial = 'Initial'; const myStr = 'My String'; - const myStrOrElse = 'My String orElse'; + const myError = 'Test Error'; + const myOrElse = 'Or Else'; - group('toState()', () { - test('on a non null String', () { - expect(myStr.toState(), const ValueState(myStr)); - expect(myStr.toState(refreshing: true), - const ValueState(myStr, refreshing: true)); + group('when', () { + test('on initial', () { + expect(const Value.initial().when(initial: () => myStrInitial), + myStrInitial); }); - test('on null', () { - const String? nullStr = null; - expect(nullStr.toState(), const NoValueState()); - expect(nullStr.toState(refreshing: true), - const NoValueState(refreshing: true)); + test('on success', () { + expect(const Value.success(myStr).when(success: (data) => data), myStr); }); - }); - - String? modifier(String value) => '$value modified'; - - group('withValue', () { - test('on a $ValueState', () { - final result = myStr.toState().withValue(modifier); - - expect(result, modifier(myStr)); - }); - - test('on a $ValueState with onlyValueState to true', () { - final result = myStr.toState().withValue(modifier, onlyValueState: true); - - expect(result, modifier(myStr)); - }); - - test('on a $InitState', () { - final result = const InitState().withValue(modifier); - - expect(result, isNull); - }); - - test('on a $InitState with onlyValueState to true', () { - final result = - const InitState().withValue(modifier, onlyValueState: true); - - expect(result, isNull); - }); - - test('on a $ErrorState', () { - final result = - ErrorState(error: 'Error', previousState: myStr.toState()) - .withValue(modifier); - expect(result, modifier(myStr)); + test('on data', () { + expect(const Value.success(myStr).when(data: (data) => data), myStr); }); - test('on a $ErrorState with onlyValueState to true', () { - final result = - ErrorState(error: 'Error', previousState: myStr.toState()) - .withValue(modifier, onlyValueState: true); - - expect(result, isNull); + test('on data with error', () { + expect( + const Value.success(myStr).toFailure(myError).when( + data: (data) => data, + failure: (error) => myError, + ), + myStr); }); - test('orElse on a $ValueState', () { - final result = - myStr.toState().withValue(modifier).orElse(() => myStrOrElse); - - expect(result, modifier(myStr)); + test('on failure', () { + expect( + const Value.initial().toFailure(myError).when( + data: (data) => data, + failure: (error) => error, + ), + myError); }); - test('orElse on a $InitState', () { - final result = const InitState() - .withValue(modifier) - .orElse(() => myStrOrElse); - - expect(result, myStrOrElse); + test('on orElse', () { + expect( + const Value.initial().when( + data: (data) => myStr, + orElse: () => myOrElse, + ), + myOrElse); }); - test('expression on a $ValueState', () { - String? result; - myStr.toState().withValue((value) { - result = modifier(value); - }); - - expect(result, modifier(myStr)); - }); - }); - - group('whenValue', () { - test('on a $ValueState', () { - final result = myStr.toState().whenValue(modifier); - - expect(result, modifier(myStr)); - }); - - test('on a $ErrorState', () { - final result = - ErrorState(error: 'Error', previousState: myStr.toState()) - .whenValue(modifier); - - expect(result, isNull); + test('on fallback', () { + expect( + const Value.initial().when( + data: (data) => myStr, + ), + isNull); }); }); - - group('toValue', () { - test('on a $ValueState', () { - final result = myStr.toState().toValue(); - - expect(result, myStr); - }); - - test('on a $ValueState with onlyValueState to true', () { - final result = myStr.toState().toValue(onlyValueState: true); - - expect(result, myStr); - }); - - test('on a $InitState', () { - final result = const InitState().toValue(); - - expect(result, isNull); - }); - - test('on a $InitState with onlyValueState to true', () { - final result = const InitState().toValue(onlyValueState: true); - - expect(result, isNull); - }); - - test('on a $ErrorState', () { - final result = - ErrorState(error: 'Error', previousState: myStr.toState()) - .toValue(); - - expect(result, myStr); - }); - - test('on a $ErrorState with onlyValueState to true', () { - final result = - ErrorState(error: 'Error', previousState: myStr.toState()) - .toValue(onlyValueState: true); - - expect(result, isNull); - }); - }); - - test('toFutureState', () { - const value = 'Result'; - - expect(Future.value(value).toFutureState(), completion(value.toState())); - expect(Future.value(null).toFutureState(), - completion(const NoValueState())); - }); - - test('toStates', () { - const value = 'Result'; - - expect( - Future.value(value).toStates(), - emitsInOrder([ - const PendingState(), - const ValueState(value), - emitsDone, - ])); - }); } diff --git a/packages/value_state/test/fetch_test.dart b/packages/value_state/test/fetch_test.dart new file mode 100644 index 0000000..4a94ec7 --- /dev/null +++ b/packages/value_state/test/fetch_test.dart @@ -0,0 +1,63 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:test/test.dart'; +import 'package:value_state/value_state.dart'; + +import 'stream.dart'; + +void main() { + late CounterStream counterStream; + + setUp(() { + counterStream = CounterStream(); + }); + + tearDown(() async { + await counterStream.close(); + }); + + test('test fetchOnValue with stream', () { + TypeMatcher> isFailure({required isFetching}) => + isA>() + .having((value) => value.isFailure, 'is failure', true) + .having( + (value) => value.isFetching, + 'is fetching $isFetching', + isFetching, + ) + .having( + (value) => value.error, + 'failure content', + isA(), + ); + + expect( + counterStream.stream, + emitsInOrder([ + const Value.initial(), + const Value.initial(isFetching: true), + Value.success(0), + Value.success(0, isFetching: true), + Value.success(1), + Value.success(1, isFetching: true), + Value.success(2), + Value.success(2, isFetching: true), + isFailure(isFetching: false), + isFailure(isFetching: true), + isFailure(isFetching: false), + isFailure(isFetching: true), + Value.success(5), + Value.success(5, isFetching: true), + isFailure(isFetching: false) + .having((value) => value.data, 'data has old value', 5), + isFailure(isFetching: true) + .having((value) => value.data, 'data has old value', 5), + Value.success(7), + Value.success(7, isFetching: true), + Value.success(8), + const Value.initial(isFetching: true), + ])); + + streamStandardActions(counterStream); + }); +} diff --git a/packages/value_state/test/helpers_cubit_test.dart b/packages/value_state/test/helpers_cubit_test.dart deleted file mode 100644 index 860caaf..0000000 --- a/packages/value_state/test/helpers_cubit_test.dart +++ /dev/null @@ -1,229 +0,0 @@ -import 'dart:async'; - -import 'package:test/test.dart'; -import 'package:value_state/value_state.dart'; - -import 'stream.dart'; - -class CounterStreamListener { - CounterStreamListener({required this.counterStream, required this.variant}); - - final CounterStream counterStream; - final bool variant; - - Stream> get stream { - var ignoreMapError = false; - - return counterStream.stream.map((state) { - if (!ignoreMapError && state is WithValueState && state.value > 4) { - ignoreMapError = true; - } - - final result = mapState( - (from) => variant && from == 1 ? null : from + 1, state, - currentState: variant ? _state : null, - mapError: variant && !ignoreMapError - ? (errorState) { - return NoValueState(refreshing: errorState.refreshing); - } - : null, - mapNoValue: variant - ? (refreshing) { - return ValueState(-1, refreshing: refreshing); - } - : null); - - _state = result; - - return result; - }); - } - - BaseState? _state; -} - -void main() { - late CounterStream counterStream; - late CounterStreamListener counterStreamListener; - - setUp(() { - counterStream = CounterStream(); - }); - - tearDown(() async { - await counterStream.close(); - }); - - test('with values incremented', () { - counterStreamListener = - CounterStreamListener(counterStream: counterStream, variant: false); - - expect( - counterStreamListener.stream, - emitsInOrder([ - const InitState(), - const PendingState(), - isA>() - .having((state) => state.refreshing, 'first value not refreshing', - false) - .having((state) => state.value, 'first value', 1), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', true) - .having((state) => state.value, 'second value', 1), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', false) - .having((state) => state.value, 'second value', 2), - // refresh with no value after value - isA>() - .having( - (state) => state.refreshing, 'second value refreshing', true) - .having((state) => state.value, 'second value', 2), - isA>() - .having((state) => state.refreshing, 'no value', false), - isA>() - .having((state) => state.refreshing, 'no value refreshing', true), - isA>() - .having((state) => state.refreshing, - 'error for third value not refreshing', false) - .having( - (state) => state.stateBeforeError, - 'no value before erreur', - isA>() - .having((state) => state.refreshing, 'no value', false), - ) - .having((state) => state.hasValue, 'second value before erreur', - false), - // refresh with error after error - isA>().having( - (state) => state.refreshing, - 'error for third value refreshing', - true), - isA>() - .having((state) => state.refreshing, - 'error for fourth value not refreshing', false) - .having( - (state) => state.stateBeforeError, - 'no value before erreur', - isA>() - .having((state) => state.refreshing, 'no value', false), - ) - .having((state) => state.hasValue, 'second value before erreur', - false), - // refresh after arror - isA>().having( - (state) => state.refreshing, - 'error for fourth value refreshing', - true), - isA>() - .having((state) => state.refreshing, 'fifth value not refreshing', - false) - .having((state) => state.value, 'fifth value ', 6), - isA>() - .having( - (state) => state.refreshing, 'fifth value refreshing', true) - .having((state) => state.value, 'fifth value', 6), - isA>() - .having((state) => state.refreshing, - 'error for sixth value refreshing', false) - .having( - (state) => state.hasValue, 'error for sixth has value', true) - .having((state) => state.value, - 'error for sixth value refreshing', 6), - isA>().having((state) => state.refreshing, - 'error for sixth value refreshing', true), - isA>() - .having((state) => state.refreshing, - 'seventh value not refreshing', false) - .having((state) => state.value, 'seventh value', 8), - isA>() - .having( - (state) => state.refreshing, 'seventh value refreshing', true) - .having((state) => state.value, 'seventh value', 8), - isA>() - .having((state) => state.refreshing, - 'eighth value not refreshing', false) - .having((state) => state.value, 'eighth value', 9), - // after _myRefresh.clear() triggered - isA>(), - ])); - - streamStandardActions(counterStream); - }); - - test('with values incremented and variant', () { - counterStreamListener = - CounterStreamListener(counterStream: counterStream, variant: true); - - expect( - counterStreamListener.stream, - emitsInOrder([ - const InitState(), - const PendingState(), - isA>() - .having((state) => state.refreshing, 'first value not refreshing', - false) - .having((state) => state.value, 'first value', 1), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', true) - .having((state) => state.value, 'second value', 1), - isA>().having((state) => state.refreshing, - 'error for fourth value not refreshing', false), - isA>().having((state) => state.refreshing, - 'error for fourth value refreshing', true), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', false) - .having((state) => state.value, 'second value', -1), - isA>() - .having( - (state) => state.refreshing, 'second value refreshing', true) - .having((state) => state.value, 'second value', -1), - // refresh with error after error - isA>().having((state) => state.refreshing, - 'error for fourth value not refreshing', false), - isA>().having((state) => state.refreshing, - 'error for fourth value refreshing', true), - isA>().having((state) => state.refreshing, - 'error for fourth value not refreshing', false), - isA>().having((state) => state.refreshing, - 'error for fourth value refreshing', true), - // refresh after arror - isA>() - .having((state) => state.refreshing, 'fifth value not refreshing', - false) - .having((state) => state.value, 'fifth value ', 6), - isA>() - .having( - (state) => state.refreshing, 'fifth value refreshing', true) - .having((state) => state.value, 'fifth value', 6), - isA>() - .having((state) => state.refreshing, - 'error for sixth value refreshing', false) - .having( - (state) => state.hasValue, 'error for sixth has value', true) - .having((state) => state.value, - 'error for sixth value refreshing', 6), - isA>().having((state) => state.refreshing, - 'error for sixth value refreshing', true), - isA>() - .having((state) => state.refreshing, - 'seventh value not refreshing', false) - .having((state) => state.value, 'seventh value', 8), - isA>() - .having( - (state) => state.refreshing, 'seventh value refreshing', true) - .having((state) => state.value, 'seventh value', 8), - isA>() - .having((state) => state.refreshing, - 'eighth value not refreshing', false) - .having((state) => state.value, 'eighth value', 9), - // after _myRefresh.clear() triggered - isA>(), - ])); - - streamStandardActions(counterStream); - }); -} diff --git a/packages/value_state/test/helpers_test.dart b/packages/value_state/test/helpers_test.dart index c2ef75b..4762f69 100644 --- a/packages/value_state/test/helpers_test.dart +++ b/packages/value_state/test/helpers_test.dart @@ -1,31 +1,97 @@ +import 'dart:async'; + import 'package:test/test.dart'; import 'package:value_state/value_state.dart'; void main() { - test('perform on $ValueState', () { - final stream = - const ValueState(1).perform((state) async => const ValueState(2)); + test('fetch on ${Value}', () { + final stream = const Value.success(1).fetchFrom(() async => 2); expect( stream, emitsInOrder([ - const ValueState(1, refreshing: true), - const ValueState(2, refreshing: false), + const Value.success(1, isFetching: true), + const Value.success(2, isFetching: false), + emitsDone, ])); }); - test('performStream on $ValueState', () { - final stream = const ValueState(1).performStream((state) async* { - yield const ValueState(2); - yield const ValueState(3); - }); + group('fetchStream', () { + test('success', () { + final stream = const Value.success(1).fetchFromStream( + () => Stream.fromIterable(const [Value.success(2), Value.success(3)]), + ); - expect( + expect( stream, emitsInOrder([ - const ValueState(1, refreshing: true), - const ValueState(2, refreshing: false), - const ValueState(3, refreshing: false), - ])); + const Value.success(1, isFetching: true), + const Value.success(2, isFetching: false), + const Value.success(3, isFetching: false), + emitsDone, + ]), + ); + + expect(ValueFetch.errors, emitsInOrder([])); + }); + + group('failure', () { + const myExceptionStr = 'My exception'; + Never throwMyException() => fail(myExceptionStr); + final isMyException = isA().having( + (tf) => tf.message, + 'message', + myExceptionStr, + ); + + test('with keepLastOnError set to false', () { + final stream = const Value.success(1).fetchFromStream(() async* { + yield const Value.success(2); + yield const Value.success(3); + throwMyException(); + }); + + expect( + stream, + emitsInOrder([ + const Value.success(1, isFetching: true), + const Value.success(2, isFetching: false), + const Value.success(3, isFetching: false), + isA() + .having((v) => v.error, 'error', isMyException) + .having((v) => v.data, 'data', 1), + emitsDone, + ]), + ); + + expect(ValueFetch.errors, emits(isA())); + }); + + test('with keepLastOnError set to true', () { + final stream = const Value.success(1).fetchFromStream( + () async* { + yield const Value.success(2); + yield const Value.success(3); + throwMyException(); + }, + keepLastOnError: true, + ); + + expect( + stream, + emitsInOrder([ + const Value.success(1, isFetching: true), + const Value.success(2, isFetching: false), + const Value.success(3, isFetching: false), + isA() + .having((v) => v.error, 'error', isMyException) + .having((v) => v.data, 'data', 3), + emitsDone, + ]), + ); + + expect(ValueFetch.errors, emits(isA())); + }); + }); }); } diff --git a/packages/value_state/test/stream.dart b/packages/value_state/test/stream.dart index 953494d..0753d01 100644 --- a/packages/value_state/test/stream.dart +++ b/packages/value_state/test/stream.dart @@ -2,16 +2,14 @@ import 'dart:async'; import 'package:stream_transform/stream_transform.dart'; import 'package:test/expect.dart'; +import 'package:value_state/src/fetch_on_value.dart'; import 'package:value_state/value_state.dart'; class CounterStream { var _value = 0; - Future _getMyValueFromRepository() async { + Future _getMyValueFromRepository() async { final value = _value++; switch (value) { - case 2: - return null; - case 3: case 4: case 6: @@ -21,33 +19,35 @@ class CounterStream { } } - BaseState state = const InitState(); + Value state = const Value.initial(); - final _resultStreamController = StreamController>(); - Stream> get stream => Stream.value(state) + final _resultStreamController = StreamController>(); + Stream> get stream => Stream.value(state) .followedBy(_resultStreamController.stream) .handleError((_) {}); var errorsRaisedCount = 0; Future incrementValue() async { - await performOnState( - state: () => state, - emitter: (state) { - this.state = state; - _resultStreamController.add(this.state); - }, - action: (state, emitter) async { - final result = await _getMyValueFromRepository(); - - emitter(result == null ? const NoValueState() : ValueState(result)); - }).onError((error, stackTrace) { + await fetchOnValue( + value: () => state, + emitter: (state) { + this.state = state; + _resultStreamController.add(this.state); + }, + action: (state, emitter) async { + final result = await _getMyValueFromRepository(); + + emitter(Value.success(result)); + }, + lastValueOnError: false, + ).onError((error, stackTrace) { errorsRaisedCount++; }); } void clear() { - _resultStreamController.add(const PendingState()); + _resultStreamController.add(const Value.initial(isFetching: true)); } Future close() async { diff --git a/packages/value_state/test/value_state_test.dart b/packages/value_state/test/value_state_test.dart index 99fe38e..be12250 100644 --- a/packages/value_state/test/value_state_test.dart +++ b/packages/value_state/test/value_state_test.dart @@ -1,223 +1,394 @@ +// ignore_for_file: prefer_const_constructors + import 'package:test/test.dart'; import 'package:value_state/value_state.dart'; -import 'stream.dart'; - void main() { - group('test with stream', () { - late CounterStream counterStream; + const value = 0; + final valueStr = value.toString(); + const error = 'Error'; + final stackTrace = StackTrace.fromString('My StackTrace'); - setUp(() { - counterStream = CounterStream(); + group('test getters', () { + test('initial state', () { + final state = Value.initial(); + + expect(state.isInitial, isTrue); + expect(state.isFetching, isFalse); + expect(state.isRefetching, isFalse); + expect(state.isSuccess, isFalse); + expect(state.isFailure, isFalse); + expect(state.dataOnSuccess, isNull); + expect(state.previousDataOnFailure, isNull); + expect(state.hasData, isFalse); + expect(state.hasError, isFalse); + expect(state.hasStackTrace, isFalse); + expect(state.data, isNull); + expect(state.error, isNull); + expect(state.stackTrace, isNull); }); - tearDown(() async { - await counterStream.close(); + test('initial state fetching', () { + final state = Value.initial(isFetching: true); + + expect(state.isInitial, isTrue); + expect(state.isFetching, isTrue); + expect(state.isRefetching, isFalse); + expect(state.isSuccess, isFalse); + expect(state.isFailure, isFalse); + expect(state.dataOnSuccess, isNull); + expect(state.previousDataOnFailure, isNull); + expect(state.hasData, isFalse); + expect(state.hasError, isFalse); + expect(state.hasStackTrace, isFalse); + expect(state.data, isNull); + expect(state.error, isNull); + expect(state.stackTrace, isNull); }); - test('with values incremented', () { - expect( - counterStream.stream, - emitsInOrder([ - isA>() - .having((state) => state.fetching, 'init fetching', true), - isA>() - .having((state) => state.fetching, 'init fetching', true), - isA>() - .having((state) => state.refreshing, - 'first value not refreshing', false) - .having((state) => state.fetching, 'first value not fetching', - false) - .having((state) => state.value, 'first value', 0), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', true) - .having( - (state) => state.fetching, 'second value fetching', true) - .having((state) => state.value, 'second value', 0), - isA>() - .having((state) => state.refreshing, - 'second value not refreshing', false) - .having((state) => state.value, 'second value', 1), - // refresh with no value after value - isA>() - .having((state) => state.refreshing, 'second value refreshing', - true) - .having((state) => state.value, 'second value', 1), - isA>() - .having((state) => state.refreshing, 'no value', false), - isA>().having( - (state) => state.refreshing, 'no value refreshing', true), - isA>() - .having((state) => state.refreshing, - 'error for third value not refreshing', false) - .having( - (state) => state.stateBeforeError, - 'no value before erreur', - isA>() - .having((state) => state.refreshing, 'no value', false), - ) - .having((state) => state.hasValue, 'second value before erreur', - false), - // refresh with error after error - isA>().having( - (state) => state.refreshing, - 'error for third value refreshing', - true), - isA>() - .having((state) => state.refreshing, - 'error for fourth value not refreshing', false) - .having( - (state) => state.stateBeforeError, - 'no value before erreur', - isA>() - .having((state) => state.refreshing, 'no value', false), - ) - .having((state) => state.hasValue, 'second value before erreur', - false), - // refresh after arror - isA>().having( - (state) => state.refreshing, - 'error for fourth value refreshing', - true), - isA>() - .having((state) => state.refreshing, - 'fifth value not refreshing', false) - .having((state) => state.value, 'fifth value ', 5), - isA>() - .having( - (state) => state.refreshing, 'fifth value refreshing', true) - .having((state) => state.value, 'fifth value', 5), - isA>() - .having((state) => state.refreshing, - 'error for sixth value refreshing', false) - .having((state) => state.hasValue, 'error for sixth has value', - true) - .having((state) => state.value, - 'error for sixth value refreshing', 5), - isA>().having( - (state) => state.refreshing, - 'error for sixth value refreshing', - true), - isA>() - .having((state) => state.refreshing, - 'seventh value not refreshing', false) - .having((state) => state.value, 'seventh value', 7), - isA>() - .having((state) => state.refreshing, 'seventh value refreshing', - true) - .having((state) => state.value, 'seventh value', 7), - isA>() - .having((state) => state.refreshing, - 'eighth value not refreshing', false) - .having((state) => state.value, 'eighth value', 8), - // after _myRefresh.clear() triggered - isA>(), - ])); - - streamStandardActions(counterStream); + test('success state', () { + final state = Value.success(value); + + expect(state.isInitial, isFalse); + expect(state.isFetching, isFalse); + expect(state.isRefetching, isFalse); + expect(state.isSuccess, isTrue); + expect(state.isFailure, isFalse); + expect(state.hasData, isTrue); + expect(state.hasError, isFalse); + expect(state.hasStackTrace, isFalse); + expect(state.data, value); + expect(state.error, isNull); + expect(state.stackTrace, isNull); }); - }); - test('equalities and hash', () { - // Dont create object with [const] to avoid [identical] return true - const initState1 = InitState(), initState2 = InitState(); + test('success state', () { + final state = Value.success(value); - expect(initState1, initState2); - expect(initState1.hashCode, initState2.hashCode); + expect(state.isInitial, isFalse); + expect(state.isFetching, isFalse); + expect(state.isRefetching, isFalse); + expect(state.isSuccess, isTrue); + expect(state.isFailure, isFalse); + expect(state.dataOnSuccess, value); + expect(state.previousDataOnFailure, isNull); + expect(state.hasData, isTrue); + expect(state.hasError, isFalse); + expect(state.hasStackTrace, isFalse); + expect(state.data, value); + expect(state.error, null); + expect(state.stackTrace, null); + }); - const waitingState1 = PendingState(), - waitingState2 = PendingState(); + test('success state fetching', () { + final state = Value.success(value, isFetching: true); - expect(waitingState1, waitingState2); - expect(waitingState1.hashCode, waitingState2.hashCode); + expect(state.isInitial, isFalse); + expect(state.isFetching, isTrue); + expect(state.isRefetching, isTrue); + expect(state.isFetching, isTrue); + expect(state.isFailure, isFalse); + expect(state.dataOnSuccess, value); + expect(state.previousDataOnFailure, isNull); + expect(state.hasData, isTrue); + expect(state.hasError, isFalse); + expect(state.hasStackTrace, isFalse); + expect(state.data, value); + expect(state.error, isNull); + expect(state.stackTrace, isNull); + }); - expect(waitingState1.mayRefreshing(), waitingState1); - expect(waitingState1.mayNotRefreshing(), waitingState2); + test('failure state', () { + final state = Value.failure(error, stackTrace: stackTrace); - const noValueState1 = NoValueState(), - noValueState2 = NoValueState(); + expect(state.isInitial, isFalse); + expect(state.isFetching, isFalse); + expect(state.isRefetching, isFalse); + expect(state.isSuccess, isFalse); + expect(state.isFailure, isTrue); + expect(state.hasData, isFalse); + expect(state.hasError, isTrue); + expect(state.hasStackTrace, isTrue); + expect(state.data, isNull); + expect(state.error, error); + expect(state.stackTrace, stackTrace); + }); - expect(noValueState1, noValueState2); - expect(noValueState1.hashCode, noValueState2.hashCode); + test('failure state fetching', () { + final state = Value.failure( + error, + stackTrace: stackTrace, + isFetching: true, + ); - const valueState1 = ValueState(0), valueState2 = ValueState(0); + expect(state.isInitial, isFalse); + expect(state.isFetching, isTrue); + expect(state.isRefetching, isTrue); + expect(state.isSuccess, isFalse); + expect(state.isFailure, isTrue); + expect(state.dataOnSuccess, isNull); + expect(state.previousDataOnFailure, isNull); + expect(state.hasData, isFalse); + expect(state.hasError, isTrue); + expect(state.hasStackTrace, isTrue); + expect(state.data, isNull); + expect(state.error, error); + expect(state.stackTrace, stackTrace); + }); - expect(valueState1, valueState2); - expect(valueState1.hashCode, valueState2.hashCode); + test('success to failure state', () { + final state = Value.success(value).toFailure(error); - final errorState1 = - ErrorState(previousState: const InitState(), error: 'Error'), - errorState2 = - ErrorState(previousState: const InitState(), error: 'Error'); + expect(state.isInitial, isFalse); + expect(state.isFetching, isFalse); + expect(state.isRefetching, isFalse); + expect(state.isSuccess, isFalse); + expect(state.isFailure, isTrue); + expect(state.dataOnSuccess, isNull); + expect(state.previousDataOnFailure, value); + expect(state.hasData, isTrue); + expect(state.hasError, isTrue); + expect(state.hasStackTrace, isFalse); + expect(state.data, value); + expect(state.error, error); + expect(state.stackTrace, isNull); + }); + }); - expect(errorState1, errorState2); - expect(errorState1.hashCode, errorState2.hashCode); + group('merge', () { + test('merge success in initial', () { + final state = Value.initial().merge( + Value.success(valueStr), + mapData: (from) => Value.success(int.parse(from)), + ); - final errorStateWithValue1 = - ErrorState(previousState: const ValueState(1), error: 'Error'), - errorStateWithValue2 = - ErrorState(previousState: const ValueState(1), error: 'Error'); + expect(state.isInitial, isFalse); + expect(state.isFetching, isFalse); + expect(state.isRefetching, isFalse); + expect(state.isSuccess, isTrue); + expect(state.isFailure, isFalse); + expect(state.hasData, isTrue); + expect(state.hasError, isFalse); + expect(state.hasStackTrace, isFalse); + expect(state.data, value); + expect(state.error, isNull); + expect(state.stackTrace, isNull); + }); + + test('merge success in success', () { + final state = Value.success(value).merge( + Value.success(valueStr), + mapData: (from) => Value.success(int.parse(from)), + ); + + expect(state.isInitial, isFalse); + expect(state.isFetching, isFalse); + expect(state.isRefetching, isFalse); + expect(state.isSuccess, isTrue); + expect(state.isFailure, isFalse); + expect(state.hasData, isTrue); + expect(state.hasError, isFalse); + expect(state.hasStackTrace, isFalse); + expect(state.data, value); + expect(state.error, isNull); + expect(state.stackTrace, isNull); + }); + + test('merge success in failure', () { + final state = Value.failure(error).merge( + Value.success(valueStr, isFetching: true), + mapData: (from) => Value.success(int.parse(from)), + ); + + expect(state.isInitial, isFalse); + expect(state.isFetching, isTrue); + expect(state.isRefetching, isTrue); + expect(state.isSuccess, isTrue); + expect(state.isFailure, isFalse); + expect(state.hasData, isTrue); + expect(state.hasError, isFalse); + expect(state.hasStackTrace, isFalse); + expect(state.data, value); + expect(state.error, isNull); + expect(state.stackTrace, isNull); + }); + + test('merge failure in initial', () { + final state = Value.initial().merge( + Value.failure(error), + mapData: (from) => Value.success(int.parse(from)), + ); + + expect(state.isInitial, isFalse); + expect(state.isFetching, isFalse); + expect(state.isRefetching, isFalse); + expect(state.isSuccess, isFalse); + expect(state.isFailure, isTrue); + expect(state.hasData, isFalse); + expect(state.hasError, isTrue); + expect(state.hasStackTrace, isFalse); + expect(state.data, isNull); + expect(state.error, error); + expect(state.stackTrace, isNull); + }); + + test('merge failure in success', () { + final state = Value.success(value).merge( + Value.failure(error), + mapData: (from) => Value.success(int.parse(from)), + ); + + expect(state.isInitial, isFalse); + expect(state.isFetching, isFalse); + expect(state.isRefetching, isFalse); + expect(state.isSuccess, isFalse); + expect(state.isFailure, isTrue); + expect(state.hasData, isTrue); + expect(state.hasError, isTrue); + expect(state.hasStackTrace, isFalse); + expect(state.data, value); + expect(state.error, error); + expect(state.stackTrace, isNull); + }); - expect(errorStateWithValue1, errorStateWithValue2); - expect(errorStateWithValue1.hashCode, errorStateWithValue2.hashCode); + test('merge initial in failure', () { + final state = Value.failure(error).merge( + Value.initial(), + mapData: (from) => Value.success(int.parse(from)), + ); + + expect(state.isInitial, isTrue); + expect(state.isFetching, isFalse); + expect(state.isRefetching, isFalse); + expect(state.isSuccess, isFalse); + expect(state.isFailure, isFalse); + expect(state.hasData, isFalse); + expect(state.hasError, isFalse); + expect(state.hasStackTrace, isFalse); + expect(state.data, isNull); + expect(state.error, isNull); + expect(state.stackTrace, isNull); + }); }); - test('visitor', () { - const visitor = _TestStateVisitor(); + group('Value mappers', () { + const myStr = 'My String'; + const myError = 'Test Error'; + const initial = Value.initial(); + final success = Value.success(myStr); - expect(const InitState().accept(visitor), 1); - expect(const PendingState().accept(visitor), 4); - expect(const NoValueState().accept(visitor), 2); - expect(const ValueState(0).accept(visitor), 3); - expect( - ErrorState(previousState: const InitState(), error: 'Error') - .accept(visitor), - 0); + final failure = Value.failure(myError, stackTrace: stackTrace); + final failureWithData = success.toFailure(myError, stackTrace: stackTrace); + + test('toFailure', () { + expect( + initial.toFailure( + myError, + stackTrace: stackTrace, + ), + Value.failure(myError, stackTrace: stackTrace), + ); + expect( + initial.toFailure( + myError, + stackTrace: stackTrace, + isFetching: true, + ), + Value.failure( + myError, + stackTrace: stackTrace, + isFetching: true, + ), + ); + + expect( + failure.toFailure(myError, stackTrace: stackTrace), + Value.failure(myError, stackTrace: stackTrace), + ); + expect( + failure.toFailure(myError, stackTrace: stackTrace, isFetching: true), + Value.failure( + myError, + stackTrace: stackTrace, + isFetching: true, + ), + ); + + expect( + failureWithData, + isA>() + .having((value) => value.data, 'has data', failureWithData.data!) + .having((value) => value.isFetching, 'is not fetching', false) + .having((value) => value.isFailure, 'is failure', true) + .having( + (value) => value.error, + 'failure content', + myError, + ), + ); + expect( + success.toFailure(myError, isFetching: true), + isA>() + .having((value) => value.isFetching, 'is fetching', true) + .having((value) => value.isFailure, 'is failure', true) + .having( + (value) => value.error, + 'failure content', + myError, + ), + ); + }); }); - test('toString', () { - expect(const InitState().toString(), - 'InitState(fetching: true)'); + test('test equalities and hash', () { + // Dont create object with [const] to avoid [identical] return true + final init1 = Value.initial(), init2 = Value.initial(); - const pendingState = PendingState(); - expect(pendingState.toString(), 'PendingState(fetching: true)'); + expect(init1, init2); + expect(init1.hashCode, init2.hashCode); - expect(const NoValueState().toString(), - 'NoValueState(fetching: false)'); + final fetching1 = Value.initial(isFetching: true), + fetching2 = Value.initial(isFetching: true); - const valueState = ValueState('My value'); + expect(fetching1, fetching2); + expect(fetching1.hashCode, fetching2.hashCode); - expect(valueState.toString(), - 'ValueState(fetching: false, value: ${valueState.value})'); - expect( - ErrorState(previousState: valueState, error: ArgumentError()) - .toString(), - 'ErrorWithPreviousValue(fetching: false, error: Invalid argument(s), stateBeforeError: $valueState)'); - expect( - ErrorState( - previousState: pendingState, - error: ArgumentError(), - stackTrace: StackTrace.fromString('My StackTrace')) - .toString(), - 'ErrorWithoutPreviousValue(fetching: false, error: Invalid argument(s), stackTrace: My StackTrace, ' - 'stateBeforeError: $pendingState)'); + final success1 = Value.success(value), success2 = Value.success(value); + + expect(success1, success2); + expect(success1.hashCode, success2.hashCode); + + final failure1 = Value.failure(error), + failure2 = Value.failure(error); + + expect(failure1, failure2); + expect(failure1.hashCode, failure2.hashCode); + + final failureWithData1 = success1.toFailure(error), + failureWithData2 = success2.toFailure(error); + + expect(failureWithData1, failureWithData2); + expect(failureWithData1.hashCode, failureWithData2.hashCode); }); -} -class _TestStateVisitor extends StateVisitor { - const _TestStateVisitor(); + test('test toString', () { + expect(const Value.initial().toString(), + 'Value(state: ValueState.initial, isFetching: false)'); - @override - visitInitState(InitState state) => 1; - @override - visitPendingState(PendingState state) => 4; + final value = Value.success('My value'); - @override - visitValueState(ValueState state) => 3; - @override - visitNoValueState(NoValueState state) => 2; + expect( + value.toString(), + 'Value(state: ValueState.success, isFetching: false, ' + 'data: My value)'); + expect( + value.toFailure(ArgumentError()).toString(), + 'Value(state: ValueState.failure, isFetching: false, ' + 'data: My value, error: Invalid argument(s))'); - @override - visitErrorState(ErrorState state) => 0; + expect( + Value.failure(ArgumentError()).toString(), + 'Value(state: ValueState.failure, isFetching: false, ' + 'error: Invalid argument(s))'); + }); } diff --git a/pubspec.lock b/pubspec.lock index 3a4303b..fe4823c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -13,10 +13,10 @@ packages: dependency: transitive description: name: args - sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.6.0" async: dependency: transitive description: @@ -25,14 +25,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" charcode: dependency: transitive description: @@ -41,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" cli_launcher: dependency: transitive description: @@ -53,18 +53,26 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" collection: dependency: transitive description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.17.2" + version: "1.19.1" conventional_commit: dependency: transitive description: @@ -77,10 +85,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.1" glob: dependency: transitive description: @@ -101,10 +109,10 @@ packages: dependency: transitive description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.3.0" http_parser: dependency: transitive description: @@ -113,6 +121,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" io: dependency: transitive description: @@ -129,30 +145,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" - matcher: - dependency: transitive - description: - name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" - url: "https://pub.dev" - source: hosted - version: "0.12.16" melos: dependency: "direct dev" description: name: melos - sha256: ccbb6ecd8bb3f08ae8f9ce22920d816bff325a98940c845eda0257cd395503ac + sha256: "3f3ab3f902843d1e5a1b1a4dd39a4aca8ba1056f2d32fd8995210fa2843f646f" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "6.3.2" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.16.0" mustache_template: dependency: transitive description: @@ -173,10 +181,10 @@ packages: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.6" pool: dependency: transitive description: @@ -189,10 +197,10 @@ packages: dependency: transitive description: name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" url: "https://pub.dev" source: hosted - version: "4.2.4" + version: "5.0.3" prompts: dependency: transitive description: @@ -213,26 +221,18 @@ packages: dependency: transitive description: name: pub_updater - sha256: "42890302ab2672adf567dc2b20e55b4ecc29d7e19c63b6b98143ab68dd717d3a" + sha256: "54e8dc865349059ebe7f163d6acce7c89eb958b8047e6d6e80ce93b13d7c9e60" url: "https://pub.dev" source: hosted - version: "0.2.4" - pubspec: - dependency: transitive - description: - name: pubspec - sha256: f534a50a2b4d48dc3bc0ec147c8bd7c304280fff23b153f3f11803c4d49d927e - url: "https://pub.dev" - source: hosted - version: "2.3.0" - quiver: + version: "0.4.0" + pubspec_parse: dependency: transitive description: - name: quiver - sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + name: pubspec_parse + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "1.4.0" source_span: dependency: transitive description: @@ -249,14 +249,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" - url: "https://pub.dev" - source: hosted - version: "2.1.1" string_scanner: dependency: transitive description: @@ -273,14 +265,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" - url: "https://pub.dev" - source: hosted - version: "0.6.0" typed_data: dependency: transitive description: @@ -289,14 +273,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" - uri: + web: dependency: transitive description: - name: uri - sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" yaml: dependency: transitive description: @@ -314,4 +298,4 @@ packages: source: hosted version: "2.1.1" sdks: - dart: ">=2.19.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index c60adc4..b6d7f9c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: value_state_root environment: - sdk: '>=2.17.1 <3.0.0' + sdk: ^3.4.0 dev_dependencies: - melos: ^3.1.0 + melos: ^6.3.2