diff --git a/README.md b/README.md index a6d92ee..0b70294 100644 --- a/README.md +++ b/README.md @@ -50,11 +50,11 @@ Commands are `Listenable`, meaning you can react to their state changes: #### Using `addListener` ```dart fetchGreetingCommand.addListener(() { - final status = fetchGreetingCommand.value; - if (status is SuccessCommand) { - print('Success: ${status.value}'); - } else if (status is FailureCommand) { - print('Failure: ${status.error}'); + final state = fetchGreetingCommand.state; + if (state is SuccessCommand) { + print('Success: ${state.data}'); + } else if (state is FailureCommand) { + print('Failure: ${state.error}'); } }); ``` @@ -67,7 +67,7 @@ Widget build(BuildContext context) { builder: (context, _) { return switch (state) { RunningCommand() => CircularProgressIndicator(), - SuccessCommand(:final value) => Text('Success: $value'), + SuccessCommand(:final data) => Text('Success: $data'), FailureCommand(:final error) => Text('Failure: $error'), _ => ElevatedButton( onPressed: () => fetchGreetingCommand.execute(), @@ -83,12 +83,13 @@ Widget build(BuildContext context) { The `when` method simplifies state management by mapping each state to a specific action or value: ```dart fetchGreetingCommand.addListener(() { - final status = fetchGreetingCommand.value; - final message = status.when( - data: (value) => 'Success: $value', + final message = fetchGreetingCommand.state.when( + success: (data) => 'Success: $data', failure: (exception) => 'Error: ${exception?.message}', - running: () => 'Fetching...', - orElse: () => 'Idle', + idle: () => 'Idle', + running: () => 'Running...', + cancelled: () => 'Cancelled', + orElse: () => 'Not provided state', ); print(message); @@ -146,7 +147,7 @@ To simplify interaction with commands, several helper methods and getters are av #### State Check Getters These getters allow you to easily check the current state of a command: ```dart - if (command.isRunning) { + if (command.state.isRunning) { print('Command is idle and ready to execute.'); } ``` @@ -182,7 +183,7 @@ final filteredValue = command.filter( 'Default Value', (state) { if (state is SuccessCommand) { - return 'Success: ${state.value}'; + return 'Success: ${state.data}'; } else if (state is FailureCommand) { return 'Error: ${state.error}'; } @@ -199,7 +200,31 @@ This method simplifies state management by allowing you to focus on specific asp --- -### 7. CommandRef +### 7. Conditional State Handlers (`ifState` methods) + +The `CommandState` class provides convenient methods to execute actions conditionally based on the current state. These methods are useful for side effects like navigation, showing snackbars, or logging. + +#### Available Methods: +- **`ifIdle(action)`**: Executes the action if the state is `IdleCommand`. +- **`ifRunning(action)`**: Executes the action if the state is `RunningCommand`. +- **`ifSuccess(action)`**: Executes the action if the state is `SuccessCommand`, passing the `data`. +- **`ifFailure(action)`**: Executes the action if the state is `FailureCommand`, passing the `error`. +- **`ifCancelled(action)`**: Executes the action if the state is `CancelledCommand`. + +#### Example: +```dart +await command.execute(); + +command.state + ..ifSuccess((data) => print('Success: $data')) + ..ifFailure((error) => showErrorSnackBar(error)); +``` + +These methods provide a clean and readable way to handle specific states without requiring a full `when` or `switch` statement. + +--- + +### 8. CommandRef The `CommandRef` class allows you to create commands that listen to changes in one or more `ValueListenables` and execute actions based on derived values. @@ -213,9 +238,9 @@ final commandRef = CommandRef( ); commandRef.addListener(() { - final status = commandRef.value; + final status = commandRef.state; if (status is SuccessCommand) { - print('Result: ${status.value}'); + print('Result: ${status.data}'); } }); diff --git a/example/lib/main.dart b/example/lib/main.dart index 973b5bf..98d1b31 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -62,7 +62,7 @@ class MyHomePage extends StatelessWidget { valueListenable: incrementCommand, builder: (context, state, snapshot) { return FloatingActionButton( - onPressed: incrementCommand.isRunning + onPressed: incrementCommand.state.isRunning ? null : () => incrementCommand.execute(), tooltip: 'Increment', diff --git a/example/pubspec.lock b/example/pubspec.lock index d37ec47..778a84e 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -79,26 +79,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -127,10 +127,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" path: dependency: transitive description: @@ -203,18 +203,18 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.7" vector_math: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -224,5 +224,5 @@ packages: source: hosted version: "15.0.0" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/lib/result_command.dart b/lib/result_command.dart index 47ea9c0..29effe8 100644 --- a/lib/result_command.dart +++ b/lib/result_command.dart @@ -39,14 +39,14 @@ ///- **`CancelledCommand`**: The action was explicitly stopped. /// ///### Accessing the State -///You can access the current state using the `value` property of the command: +///You can access the current state using the `state` property of the command: ///```dart ///final command = Command0(() async { /// return Success('Hello, World!'); ///}); /// ///// The current state of the command. -///print(command.value); // Outputs: SuccessCommand +///print(command.state); // Outputs: SuccessCommand ///``` /// ///### Reacting to State Changes diff --git a/lib/src/command.dart b/lib/src/command.dart index ec3b0e6..46de941 100644 --- a/lib/src/command.dart +++ b/lib/src/command.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; + import 'package:result_dart/functions.dart'; import 'package:result_dart/result_dart.dart'; @@ -23,7 +24,7 @@ sealed class Command extends ChangeNotifier Command([this.onCancel, int maxHistoryLength = 10]) : super() { initializeHistoryManager(maxHistoryLength); - _setValue(IdleCommand(), metadata: {'reason': 'Command created'}); + _setState(IdleCommand(), metadata: {'reason': 'Command created'}); } /// Sets the default observer listener for all commands. @@ -34,16 +35,16 @@ sealed class Command extends ChangeNotifier } /// The current state of the command. - CommandState _value = IdleCommand(); + CommandState _state = IdleCommand(); SuccessCommand? _cachedSuccessCommand; FailureCommand? _cachedFailureCommand; - /// Returns the cached value of the [SuccessCommand], or `null` if not found. + /// Returns the cached data of the [SuccessCommand], or `null` if not found. /// - /// This method retrieves the value associated with a successful command execution + /// This method retrieves the data associated with a successful command execution /// from the cache. If the command is not a [SuccessCommand], it returns `null`. - T? getCachedSuccess() => _cachedSuccessCommand?.value; + T? getCachedSuccess() => _cachedSuccessCommand?.data; /// Returns the cached exception of the [FailureCommand], or `null` if not found. /// @@ -51,28 +52,14 @@ sealed class Command extends ChangeNotifier /// from the cache. If the command is not a [FailureCommand], it returns `null`. Exception? getCachedFailure() => _cachedFailureCommand?.error; - ///[isIdle]: Checks if the command is in the idle state. - @Deprecated('Use value.isIdle instead') - bool get isIdle => value.isIdle; - - ///[isRunning]: Checks if the command is currently running. - @Deprecated('Use value.isRunning instead') - bool get isRunning => value.isRunning; - - ///[isCancelled]: Checks if the command has been cancelled. - @Deprecated('Use value.isCancelled instead') - bool get isCancelled => value.isCancelled; - - ///[isSuccess]: Checks if the command execution was successful. - @Deprecated('Use value.isSuccess instead') - bool get isSuccess => value.isSuccess; - - ///[isFailure]: Checks if the command execution failed. - @Deprecated('Use value.isFailure instead') - bool get isFailure => value.isFailure; + /// The current state of the command. + CommandState get state => _state; + /// Prefer using the `state` property instead. + /// Used only for implementation with ValueListenable interface. @override - CommandState get value => _value; + @Deprecated('Use the `state` property instead.') + CommandState get value => _state; /// Filters the current command state and returns a ValueListenable with the transformed value. /// @@ -90,7 +77,7 @@ sealed class Command extends ChangeNotifier /// 'Default Value', /// (state) { /// if (state is SuccessCommand) { - /// return 'Success: ${state.value}'; + /// return 'Success: ${state.data}'; /// } else if (state is FailureCommand) { /// return 'Error: ${state.error}'; /// } @@ -111,15 +98,15 @@ sealed class Command extends ChangeNotifier /// If the command is in the [RunningCommand] state, the [onCancel] callback is invoked, /// and the state transitions to [CancelledCommand]. void cancel({Map? metadata}) { - if (value.isRunning) { + if (state.isRunning) { try { onCancel?.call(); } catch (e) { - _setValue(FailureCommand(e is Exception ? e : Exception('$e')), + _setState(FailureCommand(e is Exception ? e : Exception('$e')), metadata: metadata); return; } - _setValue(CancelledCommand(), + _setState(CancelledCommand(), metadata: metadata ?? {'reason': 'Manually cancelled'}); } } @@ -128,15 +115,77 @@ sealed class Command extends ChangeNotifier /// /// This clears the current state, allowing the command to be reused. void reset({Map? metadata}) { - if (value.isRunning) { + if (state.isRunning) { return; } _cachedFailureCommand = null; _cachedSuccessCommand = null; - _setValue(IdleCommand(), + _setState(IdleCommand(), metadata: metadata ?? {'reason': 'Command reset'}); } + /// Adds a listener that executes specific callbacks based on command state changes. + /// + /// This method provides a convenient way to listen to command state changes and react + /// with appropriate callbacks. Each state has its corresponding optional callback, and if no + /// callback is provided for the current state, the [orElse] callback will be executed if provided. + /// + /// The listener will be triggered immediately with the current state, and then every time + /// the command state changes. + /// + /// Returns a [VoidCallback] that can be called to remove the listener. + /// + /// Example: + /// ```dart + /// final command = Command0(() async { + /// return Success('Hello, World!'); + /// }); + /// + /// final removeListener = command.addWhenListener( + /// onSuccess: (data) => print('Success: $data'), + /// onFailure: (error) => print('Error: $error'), + /// onIdle: () => print('Command is ready'), + /// onRunning: () => print('Command is executing'), + /// onCancelled: () => print('Command was cancelled'), + /// orElse: () => print('Unknown state'), + /// ); + /// + /// // Later, remove the listener + /// removeListener(); + /// ``` + VoidCallback addWhenListener({ + void Function(T data)? onSuccess, + void Function(Exception? exception)? onFailure, + void Function()? onIdle, + void Function()? onRunning, + void Function()? onCancelled, + void Function()? orElse, + }) { + void listener() { + switch (state) { + case IdleCommand(): + (onIdle ?? orElse)?.call(); + case CancelledCommand(): + (onCancelled ?? orElse)?.call(); + case RunningCommand(): + (onRunning ?? orElse)?.call(); + case FailureCommand(:final error): + onFailure != null ? onFailure(error) : orElse?.call(); + case SuccessCommand(:final data): + onSuccess != null ? onSuccess(data) : orElse?.call(); + } + } + + // Execute immediately with current state + listener(); + + // Add listener for future state changes + addListener(listener); + + // Return a function to remove the listener + return () => removeListener(listener); + } + /// Executes the given [action] and updates the command state accordingly. /// /// The state transitions to [RunningCommand] during execution, @@ -145,10 +194,10 @@ sealed class Command extends ChangeNotifier /// Optionally accepts a [timeout] duration to limit the execution time of the action. /// If the action times out, the command is cancelled and transitions to [FailureCommand]. Future _execute(CommandAction0 action, {Duration? timeout}) async { - if (value.isRunning) { + if (state.isRunning) { return; } // Prevent multiple concurrent executions. - _setValue(RunningCommand(), metadata: {'status': 'Execution started'}); + _setState(RunningCommand(), metadata: {'status': 'Execution started'}); bool hasError = false; late Result result; @@ -163,28 +212,28 @@ sealed class Command extends ChangeNotifier } } catch (e, stackTrace) { hasError = true; - _setValue(FailureCommand(Exception('Unexpected error: $e')), + _setState(FailureCommand(Exception('Unexpected error: $e')), metadata: {'error': '$e', 'stackTrace': stackTrace.toString()}); return; } finally { if (!hasError) { - final newValue = result + final newState = result .map(SuccessCommand.new) .mapError(FailureCommand.new) .fold(identity, identity); - if (value.isRunning) { - _setValue(newValue); + if (state.isRunning) { + _setState(newState); } } } } /// Updates the cache whenever the command state changes. - void _updateCache(CommandState newValue) { - if (newValue is SuccessCommand) { - _cachedSuccessCommand = newValue; - } else if (newValue is FailureCommand) { - _cachedFailureCommand = newValue; + void _updateCache(CommandState newState) { + if (newState is SuccessCommand) { + _cachedSuccessCommand = newState; + } else if (newState is FailureCommand) { + _cachedFailureCommand = newState; } } @@ -192,15 +241,15 @@ sealed class Command extends ChangeNotifier /// /// Additionally, records the change in the state history with optional metadata /// and updates the cache. - void _setValue(CommandState newValue, {Map? metadata}) { - if (newValue.instanceName == _value.instanceName && + void _setState(CommandState newState, {Map? metadata}) { + if (newState.instanceName == _state.instanceName && stateHistory.isNotEmpty) { return; } - _value = newValue; - _updateCache(newValue); - _defaultObserverListener?.call(newValue); - addHistoryEntry(CommandHistoryEntry(state: newValue, metadata: metadata)); + _state = newState; + _updateCache(newState); + _defaultObserverListener?.call(newState); + addHistoryEntry(CommandHistoryEntry(state: newState, metadata: metadata)); notifyListeners(); } } diff --git a/lib/src/command_ref.dart b/lib/src/command_ref.dart index ca60c2f..6a701be 100644 --- a/lib/src/command_ref.dart +++ b/lib/src/command_ref.dart @@ -18,9 +18,9 @@ part of 'command.dart'; /// ); /// /// commandRef.addListener(() { -/// final status = commandRef.value; +/// final status = commandRef.state; /// if (status is SuccessCommand) { -/// print('Result: ${status.value}'); +/// print('Result: ${status.data}'); /// } /// }); /// diff --git a/lib/src/states.dart b/lib/src/states.dart index f14c4b2..8ad0acb 100644 --- a/lib/src/states.dart +++ b/lib/src/states.dart @@ -4,60 +4,158 @@ part of 'command.dart'; sealed class CommandState { const CommandState(); + /// Returns the current state as a string representation. + /// This is useful for debugging and logging purposes. + String get instanceName; + + bool get isIdle => this is IdleCommand; + bool get isRunning => this is RunningCommand; + bool get isSuccess => this is SuccessCommand; + bool get isFailure => this is FailureCommand; + bool get isCancelled => this is CancelledCommand; + /// Maps the current state to a value of type [R] based on the object's state. /// - /// This method allows you to handle different states of an object (`Idle`, `Cancelled`, `Running`, `Failure`, and `Success`), - /// and map each state to a corresponding value of type [R]. If no handler for a specific state is provided, the fallback - /// function [orElse] will be invoked. + /// This method allows you to handle only specific states, with a required fallback [orElse] + /// function for unhandled states. Returns a non-nullable value of type [R]. /// - /// - [data]: Called when the state represents success, receiving a value of type [T] (the successful result). + /// - [success]: Called when the state represents success, receiving the data of type [T] (the successful result). Optional. /// - [failure]: Called when the state represents failure, receiving an [Exception?]. Optional. + /// - [idle]: Called when the state represents idle (not running). Optional. /// - [cancelled]: Called when the state represents cancellation. Optional. /// - [running]: Called when the state represents a running operation. Optional. - /// - [orElse]: A fallback function that is called when the state does not match any of the provided states. - /// It is required and will be used when any of the other parameters are not provided or when no state matches. + /// - [orElse]: A required fallback function that is called when the state does not match any of the provided states. /// - /// Returns a value of type [R] based on the state of the object. If no matching state handler is provided, the fallback - /// function [orElse] will be called. + /// Returns a non-nullable value of type [R] based on the state of the object. /// /// Example: /// ```dart - /// final result = command.value.when( - /// data: (value) => 'Success: $value', - /// failure: (e) => 'Error: ${e?.message}', - /// cancelled: () => 'Cancelled', - /// running: () => 'Running', - /// orElse: () => 'Unknown state', // Required fallback function + /// return command.state.when( + /// success: (data) => Text('Success: $data'), + /// failure: (e) => Text('Error: ${e?.message}'), + /// running: () => CircularProgressIndicator(), + /// orElse: () => Button( + /// onPressed: () => command.execute(), + /// child: Text('Execute'), + /// ), /// ); /// ``` - /// - /// If any of the optional parameters (`failure`, `cancelled`, `running`) are missing, you must provide [orElse] - /// to ensure a valid fallback is available. R when({ - required R Function(T value) data, + R Function(T data)? success, R Function(Exception? exception)? failure, - R Function()? cancelled, + R Function()? idle, R Function()? running, - required Function() orElse, + R Function()? cancelled, + required R Function() orElse, }) { return switch (this) { - IdleCommand() => orElse.call(), + IdleCommand() => idle?.call() ?? orElse(), CancelledCommand() => cancelled?.call() ?? orElse(), RunningCommand() => running?.call() ?? orElse(), FailureCommand(:final error) => failure?.call(error) ?? orElse(), - SuccessCommand(:final value) => data.call(value) ?? orElse(), + SuccessCommand(:final data) => success?.call(data) ?? orElse(), }; } - /// Returns the current state as a string representation. - /// This is useful for debugging and logging purposes. - String get instanceName; + /// Maps the current state to a value of type [R] based on the object's state. + /// + /// This method allows you to handle only specific states. All parameters are optional, + /// and returns a nullable value of type [R?]. If no handler matches the current state, + /// returns null (or the result of [orElse] if provided). + /// + /// - [success]: Called when the state represents success, receiving the data of type [T] (the successful result). Optional. + /// - [failure]: Called when the state represents failure, receiving an [Exception?]. Optional. + /// - [idle]: Called when the state represents idle (not running). Optional. + /// - [cancelled]: Called when the state represents cancellation. Optional. + /// - [running]: Called when the state represents a running operation. Optional. + /// - [orElse]: A fallback function that is called when the state does not match any of the provided states. Optional. + /// + /// Returns a nullable value of type [R?] based on the state of the object. + /// + /// Example: + /// ```dart + /// await command.execute(); + /// command.state.maybeWhen( + /// success: (data) => context.go('/success'), + /// failure: (e) => showErrorSnackBar(e), + /// ); + /// ``` + R? maybeWhen({ + R Function(T data)? success, + R Function(Exception? exception)? failure, + R Function()? idle, + R Function()? running, + R Function()? cancelled, + R Function()? orElse, + }) { + return switch (this) { + IdleCommand() => idle?.call() ?? orElse?.call(), + CancelledCommand() => cancelled?.call() ?? orElse?.call(), + RunningCommand() => running?.call() ?? orElse?.call(), + FailureCommand(:final error) => failure?.call(error) ?? orElse?.call(), + SuccessCommand(:final data) => success?.call(data) ?? orElse?.call(), + }; + } - bool get isIdle => this is IdleCommand; - bool get isRunning => this is RunningCommand; - bool get isSuccess => this is SuccessCommand; - bool get isFailure => this is FailureCommand; - bool get isCancelled => this is CancelledCommand; + /// Executes [action] if this is an [IdleCommand]. + /// + /// Example: + /// ```dart + /// command.state.ifIdle(() => print('Command is idle')); + /// ``` + void ifIdle(void Function() action) { + if (this case IdleCommand()) { + action(); + } + } + + /// Executes [action] if this is a [RunningCommand]. + /// + /// Example: + /// ```dart + /// command.state.ifRunning(() => showLoadingIndicator()); + /// ``` + void ifRunning(void Function() action) { + if (this case RunningCommand()) { + action(); + } + } + + /// Executes [action] if this is a [SuccessCommand], passing the [data]. + /// + /// Example: + /// ```dart + /// command.state.ifSuccess((data) => showData(data)); + /// ``` + void ifSuccess(void Function(T data) action) { + if (this case SuccessCommand(:final data)) { + action(data); + } + } + + /// Executes [action] if this is a [FailureCommand], passing the [error]. + /// + /// Example: + /// ```dart + /// command.state.ifFailure((error) => showError(context, error)); + /// ``` + void ifFailure(void Function(Exception error) action) { + if (this case FailureCommand(:final error)) { + action(error); + } + } + + /// Executes [action] if this is a [CancelledCommand]. + /// + /// Example: + /// ```dart + /// command.state.ifCancelled(() => showCancelledMessage()); + /// ``` + void ifCancelled(void Function() action) { + if (this case CancelledCommand()) { + action(); + } + } } /// Represents the idle state of a command (not running). @@ -98,11 +196,11 @@ final class FailureCommand extends CommandState { /// Represents a command that executed successfully. final class SuccessCommand extends CommandState { - /// Creates a [SuccessCommand] with the given [value]. - const SuccessCommand(this.value); + /// Creates a [SuccessCommand] with the given [data]. + const SuccessCommand(this.data); /// The result of the successful execution. - final T value; + final T data; @override final String instanceName = 'SuccessCommand'; diff --git a/pubspec.yaml b/pubspec.yaml index 0fdbd59..e60d6f9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: result_command description: "A command pattern implementation for Dart and Flutter using result_dart package." -version: 2.1.0 +version: 2.1.1 # Fork baseado na versão 2.1.0 do package. Assim, nossa versão própria apenas alterna a última casa. repository: https://github.com/Flutterando/result_command environment: @@ -14,42 +14,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^4.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. -flutter: - uses-material-design: true - - # To add assets to your package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/to/asset-from-package - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/to/font-from-package + flutter_lints: ^6.0.0 diff --git a/test/src/command_filtered_test.dart b/test/src/command_filtered_test.dart index 05e8a88..4274af4 100644 --- a/test/src/command_filtered_test.dart +++ b/test/src/command_filtered_test.dart @@ -1,7 +1,9 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:result_command/src/command.dart'; + import 'package:result_dart/result_dart.dart'; +import 'package:result_command/src/command.dart'; + void main() { test( 'Filter Ok', @@ -18,7 +20,7 @@ void main() { final fiteredSuccess = command.filter( 0, - (value) => value is SuccessCommand ? value.value : null, + (value) => value is SuccessCommand ? value.data : null, ); fiteredSuccess.addListener(expectAsync0(() { diff --git a/test/src/command_ref_test.dart b/test/src/command_ref_test.dart index 51552de..0260457 100644 --- a/test/src/command_ref_test.dart +++ b/test/src/command_ref_test.dart @@ -1,8 +1,10 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:result_command/src/command.dart'; + import 'package:result_dart/result_dart.dart'; +import 'package:result_command/src/command.dart'; + void main() { test('Command Ref', () { final listenable = ValueNotifier(0); @@ -13,9 +15,9 @@ void main() { ); commandRef.addListener(expectAsync0(() { - final status = commandRef.value; + final status = commandRef.state; if (status is SuccessCommand) { - expect(status.value, 10); + expect(status.data, 10); } }, count: 2)); diff --git a/test/src/command_test.dart b/test/src/command_test.dart index ca14ff6..9072725 100644 --- a/test/src/command_test.dart +++ b/test/src/command_test.dart @@ -1,7 +1,9 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:result_command/src/command.dart'; + import 'package:result_dart/result_dart.dart'; +import 'package:result_command/src/command.dart'; + void main() { group('Command tests', () { test('User getters', () async { @@ -37,7 +39,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute(); @@ -46,7 +48,7 @@ void main() { isA>(), ]); - expect(command.value, isA>()); + expect(command.state, isA>()); // Verify history final history = command.stateHistory; @@ -72,7 +74,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute('Test'); @@ -81,8 +83,8 @@ void main() { isA>(), ]); - expect(command.value, isA>()); - expect((command.value as SuccessCommand).value, 'Test'); + expect(command.state, isA>()); + expect((command.state as SuccessCommand).data, 'Test'); // Verify history final history = command.stateHistory; @@ -110,7 +112,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); Future.delayed(const Duration(milliseconds: 100), () => command.cancel()); @@ -121,7 +123,7 @@ void main() { isA>(), ]); - expect(command.value, isA>()); + expect(command.state, isA>()); // Verify history final history = command.stateHistory; @@ -141,7 +143,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute(); @@ -150,8 +152,8 @@ void main() { isA>(), ]); - expect(command.value, isA>()); - expect((command.value as FailureCommand).error.toString(), + expect(command.state, isA>()); + expect((command.state as FailureCommand).error.toString(), contains('failure')); // Verify history @@ -176,7 +178,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute(); @@ -184,8 +186,8 @@ void main() { isA>(), isA>(), ]); - expect(command.value, isA>()); - expect((command.value as FailureCommand).error.toString(), + expect(command.state, isA>()); + expect((command.state as FailureCommand).error.toString(), contains('Unexpected exception')); // Verify history @@ -212,7 +214,7 @@ void main() { command.execute(); command.cancel(metadata: {'customKey': 'customValue'}); - expect(command.value, isA>()); + expect(command.state, isA>()); expect(command.stateHistory.last.metadata, containsPair('customKey', 'customValue')); }); @@ -228,7 +230,7 @@ void main() { command.execute(); command.cancel(); - expect(command.value, isA>()); + expect(command.state, isA>()); expect(command.stateHistory.last.state, isA>()); }); @@ -242,7 +244,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute(timeout: const Duration(milliseconds: 500)); @@ -259,14 +261,14 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute(); - expect(command.value, isA>()); + expect(command.state, isA>()); command.reset(); - expect(command.value, isA>()); + expect(command.state, isA>()); // Verify history after reset final history = command.stateHistory; @@ -289,12 +291,12 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute('input'); - expect(command.value, isA>()); - expect((command.value as SuccessCommand).value, 'INPUT'); + expect(command.state, isA>()); + expect((command.state as SuccessCommand).data, 'INPUT'); // Verify history final history = command.stateHistory; @@ -318,12 +320,12 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute('Value', 42); - expect(command.value, isA>()); - expect((command.value as SuccessCommand).value, 'Value 42'); + expect(command.state, isA>()); + expect((command.state as SuccessCommand).data, 'Value 42'); // Verify history final history = command.stateHistory; @@ -358,7 +360,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute(); @@ -386,10 +388,10 @@ void main() { test('Command isIdle returns true when state is IdleCommand', () { final command = Command0(() async => const Success('idle')); - expect(command.isIdle, isTrue); - expect(command.isCancelled, isFalse); - expect(command.isSuccess, isFalse); - expect(command.isFailure, isFalse); + expect(command.state.isIdle, isTrue); + expect(command.state.isCancelled, isFalse); + expect(command.state.isSuccess, isFalse); + expect(command.state.isFailure, isFalse); }); test('Command isCancelled returns true when state is CancelledCommand', @@ -403,10 +405,10 @@ void main() { command.execute(); command.cancel(); - expect(command.isIdle, isFalse); - expect(command.isCancelled, isTrue); - expect(command.isSuccess, isFalse); - expect(command.isFailure, isFalse); + expect(command.state.isIdle, isFalse); + expect(command.state.isCancelled, isTrue); + expect(command.state.isSuccess, isFalse); + expect(command.state.isFailure, isFalse); }); test('Command isSuccess returns true when state is SuccessCommand', @@ -414,10 +416,10 @@ void main() { final command = Command0(() async => const Success('success')); await command.execute(); - expect(command.isIdle, isFalse); - expect(command.isCancelled, isFalse); - expect(command.isSuccess, isTrue); - expect(command.isFailure, isFalse); + expect(command.state.isIdle, isFalse); + expect(command.state.isCancelled, isFalse); + expect(command.state.isSuccess, isTrue); + expect(command.state.isFailure, isFalse); }); test('Command isFailure returns true when state is FailureCommand', @@ -426,10 +428,10 @@ void main() { Command0(() async => Failure(Exception('failure'))); await command.execute(); - expect(command.isIdle, isFalse); - expect(command.isCancelled, isFalse); - expect(command.isSuccess, isFalse); - expect(command.isFailure, isTrue); + expect(command.state.isIdle, isFalse); + expect(command.state.isCancelled, isFalse); + expect(command.state.isSuccess, isFalse); + expect(command.state.isFailure, isTrue); }); test( @@ -440,10 +442,10 @@ void main() { await command.execute(); - final result = command.value.when( - data: (_) => 'none', - running: () => 'running', + final result = command.state.maybeWhen( + success: (_) => 'none', failure: (exception) => exception.toString(), + running: () => 'running', orElse: () => 'default value', ); @@ -456,10 +458,10 @@ void main() { await command.execute(); - final result = command.value.when( - data: (value) => value, - running: () => 'running', + final result = command.state.maybeWhen( + success: (value) => value, failure: (exception) => exception.toString(), + running: () => 'running', orElse: () => 'default value', ); @@ -475,9 +477,9 @@ void main() { await command.execute('param'); - final result = command.value.when( - data: (value) => value, + final result = command.state.maybeWhen( running: () => 'running', + success: (value) => value, failure: (exception) => exception.toString(), orElse: () => 'default value', ); @@ -492,8 +494,8 @@ void main() { await command.execute(); - final result = command.value.when( - data: (value) => 'otherValue', + final result = command.state.maybeWhen( + success: (value) => 'otherValue', orElse: () => 'default value', ); @@ -522,6 +524,625 @@ void main() { command1.execute().then((_) => command2.execute()); }); + + group('.when() method tests', () { + setUp(() { + // Reset the global observer to avoid interference with other tests + Command.setObserverListener((state) {}); + }); + + test('when() handles success state correctly', () async { + final command = Command0(() async => const Success('success value')); + + await command.execute(); + + final result = command.state.when( + success: (value) => 'Success: $value', + failure: (e) => 'Failure: $e', + idle: () => 'Idle', + running: () => 'Running', + cancelled: () => 'Cancelled', + orElse: () => 'Unknown', + ); + + expect(result, 'Success: success value'); + }); + + test('when() handles failure state correctly', () async { + final command = Command0(() async => Failure(Exception('error'))); + + await command.execute(); + + final result = command.state.when( + success: (value) => 'Success: $value', + failure: (e) => 'Failure: $e', + idle: () => 'Idle', + running: () => 'Running', + cancelled: () => 'Cancelled', + orElse: () => 'Unknown', + ); + + expect(result, 'Failure: Exception: error'); + }); + + test('when() handles idle state correctly', () async { + final command = Command0(() async => const Success('success')); + + final result = command.state.when( + success: (value) => 'Success: $value', + failure: (e) => 'Failure: $e', + idle: () => 'Idle', + running: () => 'Running', + cancelled: () => 'Cancelled', + orElse: () => 'Unknown', + ); + + expect(result, 'Idle'); + }); + + test('when() handles running state correctly', () async { + final command = Command0(() async { + await Future.delayed(const Duration(milliseconds: 100)); + return const Success('success'); + }); + + final executeFuture = command.execute(); + + // Check state during execution + await Future.delayed(const Duration(milliseconds: 10)); + + final result = command.state.when( + success: (value) => 'Success: $value', + failure: (e) => 'Failure: $e', + idle: () => 'Idle', + running: () => 'Running', + cancelled: () => 'Cancelled', + orElse: () => 'Unknown', + ); + + expect(result, 'Running'); + + await executeFuture; + }); + + test('when() handles cancelled state correctly', () async { + final command = Command0(() async { + await Future.delayed(const Duration(milliseconds: 100)); + return const Success('success'); + }); + + final executeFuture = command.execute(); + await Future.delayed(const Duration(milliseconds: 10)); + command.cancel(); + + await executeFuture; + + final result = command.state.when( + success: (value) => 'Success: $value', + failure: (e) => 'Failure: $e', + idle: () => 'Idle', + running: () => 'Running', + cancelled: () => 'Cancelled', + orElse: () => 'Unknown', + ); + + expect(result, 'Cancelled'); + }); + + test('when() calls orElse when success handler is not provided', () async { + final command = Command0(() async => const Success('success')); + + await command.execute(); + + final result = command.state.when( + failure: (e) => 'Failure: $e', + idle: () => 'Idle', + running: () => 'Running', + cancelled: () => 'Cancelled', + orElse: () => 'Default from orElse', + ); + + expect(result, 'Default from orElse'); + }); + + test('when() calls orElse when failure handler is not provided', () async { + final command = Command0(() async => Failure(Exception('error'))); + + await command.execute(); + + final result = command.state.when( + success: (value) => 'Success: $value', + idle: () => 'Idle', + running: () => 'Running', + cancelled: () => 'Cancelled', + orElse: () => 'Default from orElse', + ); + + expect(result, 'Default from orElse'); + }); + + test('when() calls orElse when idle handler is not provided', () async { + final command = Command0(() async => const Success('success')); + + final result = command.state.when( + success: (value) => 'Success: $value', + failure: (e) => 'Failure: $e', + running: () => 'Running', + cancelled: () => 'Cancelled', + orElse: () => 'Default from orElse', + ); + + expect(result, 'Default from orElse'); + }); + + test('when() calls orElse when running handler is not provided', () async { + final command = Command0(() async { + await Future.delayed(const Duration(milliseconds: 100)); + return const Success('success'); + }); + + final executeFuture = command.execute(); + await Future.delayed(const Duration(milliseconds: 10)); + + final result = command.state.when( + success: (value) => 'Success: $value', + failure: (e) => 'Failure: $e', + idle: () => 'Idle', + cancelled: () => 'Cancelled', + orElse: () => 'Default from orElse', + ); + + expect(result, 'Default from orElse'); + + await executeFuture; + }); + + test('when() calls orElse when cancelled handler is not provided', () async { + final command = Command0(() async { + await Future.delayed(const Duration(milliseconds: 100)); + return const Success('success'); + }); + + final executeFuture = command.execute(); + await Future.delayed(const Duration(milliseconds: 10)); + command.cancel(); + + await executeFuture; + + final result = command.state.when( + success: (value) => 'Success: $value', + failure: (e) => 'Failure: $e', + idle: () => 'Idle', + running: () => 'Running', + orElse: () => 'Default from orElse', + ); + + expect(result, 'Default from orElse'); + }); + + test('when() returns non-nullable value', () async { + final command = Command0(() async => const Success('success')); + + await command.execute(); + + // This should compile without ! operator + final String result = command.state.when( + success: (value) => 'Success: $value', + failure: (e) => 'Failure: $e', + idle: () => 'Idle', + running: () => 'Running', + cancelled: () => 'Cancelled', + orElse: () => 'Default', + ); + + expect(result, isA()); + expect(result, 'Success: success'); + }); + + test('when() works with different return types (Widget example)', () async { + final command = Command0(() async => const Success('data')); + + await command.execute(); + + final result = command.state.when( + success: (value) => value.length, + failure: (e) => 0, + idle: () => -1, + running: () => -2, + cancelled: () => -3, + orElse: () => -4, + ); + + expect(result, 4); // 'data'.length = 4 + }); + + test('when() with only orElse always returns orElse value', () async { + final command = Command0(() async => const Success('success')); + + await command.execute(); + + final result = command.state.when( + orElse: () => 'Only orElse', + ); + + expect(result, 'Only orElse'); + }); + + test('when() with Command1 handles success with value', () async { + final command = Command1( + (int value) async => Success('Number: $value'), + ); + + await command.execute(42); + + final result = command.state.when( + success: (value) => 'Got: $value', + failure: (e) => 'Error: $e', + idle: () => 'Idle', + running: () => 'Running', + cancelled: () => 'Cancelled', + orElse: () => 'Default', + ); + + expect(result, 'Got: Number: 42'); + }); + + test('when() with Command2 handles success with multiple parameters', () async { + final command = Command2( + (int a, String b) async => Success('$a-$b'), + ); + + await command.execute(10, 'test'); + + final result = command.state.when( + success: (value) => 'Result: $value', + failure: (e) => 'Error: $e', + idle: () => 'Idle', + running: () => 'Running', + cancelled: () => 'Cancelled', + orElse: () => 'Default', + ); + + expect(result, 'Result: 10-test'); + }); + }); + }); + + group('Command addWhenListener tests', () { + setUp(() { + // Reset the global observer to avoid interference with other tests + Command.setObserverListener((state) {}); + }); + + test('addWhenListener executes immediately with current state', () { + final command = Command0(() async => const Success('test')); + var idleCalled = false; + + command.addWhenListener(onIdle: () => idleCalled = true); + + expect(idleCalled, isTrue); + }); + + test('addWhenListener calls onSuccess when command succeeds', () async { + final command = Command0(() async => const Success('test value')); + var successCalled = false; + String? receivedValue; + + command.addWhenListener( + onSuccess: (value) { + successCalled = true; + receivedValue = value; + }, + ); + + await command.execute(); + + expect(successCalled, isTrue); + expect(receivedValue, equals('test value')); + }); + + test('addWhenListener calls onFailure when command fails', () async { + final testException = Exception('test error'); + final command = Command0(() async => Failure(testException)); + var failureCalled = false; + Exception? receivedException; + + command.addWhenListener( + onFailure: (exception) { + failureCalled = true; + receivedException = exception; + }, + ); + + await command.execute(); + + expect(failureCalled, isTrue); + expect(receivedException, equals(testException)); + }); + + test('addWhenListener calls onRunning during command execution', () async { + final command = Command0(() async { + await Future.delayed(const Duration(milliseconds: 50)); + return const Success('test'); + }); + var runningSeen = false; + + command.addWhenListener(onRunning: () => runningSeen = true); + + await command.execute(); + + expect(runningSeen, isTrue); + }); + + test('addWhenListener calls onCancelled when command is cancelled', () async { + final command = Command0(() async { + await Future.delayed(const Duration(seconds: 2)); + return const Success('test'); + }); + var cancelledCalled = false; + + command.addWhenListener(onCancelled: () => cancelledCalled = true); + + command.execute(); + command.cancel(); + + expect(cancelledCalled, isTrue); + }); + + test('addWhenListener calls orElse as fallback', () async { + final command = Command0(() async => const Success('test')); + var elseCalled = false; + + command.addWhenListener( + onFailure: (error) => {}, + orElse: () => elseCalled = true, + ); + + await command.execute(); + + expect(elseCalled, isTrue); + }); + + test('addWhenListener removes listener correctly', () async { + final command = Command0(() async => const Success('test')); + var callCount = 0; + + final removeListener = command.addWhenListener( + onSuccess: (value) => callCount++, + ); + + await command.execute(); + expect(callCount, equals(1)); + + removeListener(); + command.reset(); + await command.execute(); + + expect(callCount, equals(1)); // Should still be 1 because listener was removed + }); + + test('addWhenListener supports multiple independent listeners', () async { + final command = Command0(() async => const Success('test')); + var listener1Called = false; + var listener2Called = false; + + command.addWhenListener(onSuccess: (value) => listener1Called = true); + final removeListener2 = command.addWhenListener(onSuccess: (value) => listener2Called = true); + + await command.execute(); + + expect(listener1Called, isTrue); + expect(listener2Called, isTrue); + + // Reset and remove one listener + command.reset(); + removeListener2(); + listener1Called = false; + listener2Called = false; + + await command.execute(); + + expect(listener1Called, isTrue); + expect(listener2Called, isFalse); + }); + + test('addWhenListener works with Command1', () async { + final command = Command1((value) async => Success('Result: $value')); + var successCalled = false; + String? receivedValue; + + command.addWhenListener( + onSuccess: (value) { + successCalled = true; + receivedValue = value; + }, + ); + + await command.execute(42); + + expect(successCalled, isTrue); + expect(receivedValue, equals('Result: 42')); + }); + + test('addWhenListener handles listener exceptions gracefully', () async { + final command = Command0(() async => const Success('test')); + var commandCompleted = false; + + command.addWhenListener( + onSuccess: (value) => throw Exception('Listener error'), + ); + + // Command should complete normally despite listener exception + await command.execute(); + commandCompleted = true; + + expect(commandCompleted, isTrue); + expect(command.state, isA>()); + }); + }); + + group('ifState methods', () { + test('ifIdle executes action when state is IdleCommand', () { + final command = Command0(() async => const Success('test')); + var idleCalled = false; + + command.state.ifIdle(() => idleCalled = true); + + expect(idleCalled, isTrue); + }); + + test('ifIdle does not execute action when state is not IdleCommand', + () async { + final command = Command0(() async { + await Future.delayed(const Duration(milliseconds: 100)); + return const Success('test'); + }); + var idleCalled = false; + + await command.execute(); + command.state.ifIdle(() => idleCalled = true); + + expect(idleCalled, isFalse); + }); + + test('ifRunning executes action when state is RunningCommand', () async { + final command = Command0(() async { + await Future.delayed(const Duration(milliseconds: 100)); + return const Success('test'); + }); + var runningCalled = false; + + command.addListener(() { + command.state.ifRunning(() => runningCalled = true); + }); + + await command.execute(); + + expect(runningCalled, isTrue); + }); + + test('ifRunning does not execute action when state is not RunningCommand', + () { + final command = Command0(() async => const Success('test')); + var runningCalled = false; + + command.state.ifRunning(() => runningCalled = true); + + expect(runningCalled, isFalse); + }); + + test('ifSuccess executes action with data when state is SuccessCommand', + () async { + final command = Command0(() async => const Success('success')); + String? receivedData; + + await command.execute(); + command.state.ifSuccess((data) => receivedData = data); + + expect(receivedData, 'success'); + }); + + test( + 'ifSuccess does not execute action when state is not SuccessCommand', + () { + final command = Command0(() async => const Success('test')); + String? receivedData; + + command.state.ifSuccess((data) => receivedData = data); + + expect(receivedData, isNull); + }); + + test('ifFailure executes action with error when state is FailureCommand', + () async { + final command = Command0( + () async => Failure(Exception('test error'))); + Exception? receivedError; + + await command.execute(); + command.state.ifFailure((error) => receivedError = error); + + expect(receivedError, isA()); + expect(receivedError.toString(), contains('test error')); + }); + + test( + 'ifFailure does not execute action when state is not FailureCommand', + () async { + final command = Command0(() async => const Success('test')); + Exception? receivedError; + + await command.execute(); + command.state.ifFailure((error) => receivedError = error); + + expect(receivedError, isNull); + }); + + test('ifCancelled executes action when state is CancelledCommand', + () async { + final command = Command0(() async { + await Future.delayed(const Duration(seconds: 1)); + return const Success('test'); + }); + var cancelledCalled = false; + + Future.delayed( + const Duration(milliseconds: 50), () => command.cancel()); + + await command.execute(); + command.state.ifCancelled(() => cancelledCalled = true); + + expect(cancelledCalled, isTrue); + }); + + test( + 'ifCancelled does not execute action when state is not CancelledCommand', + () async { + final command = Command0(() async => const Success('test')); + var cancelledCalled = false; + + await command.execute(); + command.state.ifCancelled(() => cancelledCalled = true); + + expect(cancelledCalled, isFalse); + }); + + test('multiple ifState methods can be chained', () async { + final command = Command0(() async => const Success('data')); + var successCalled = false; + var failureCalled = false; + + await command.execute(); + + command.state + ..ifSuccess((data) => successCalled = true) + ..ifFailure((error) => failureCalled = true); + + expect(successCalled, isTrue); + expect(failureCalled, isFalse); + }); + + test('ifState methods work with Command1', () async { + final command = + Command1((value) async => Success(value * 2)); + int? receivedData; + + await command.execute(5); + command.state.ifSuccess((data) => receivedData = data); + + expect(receivedData, 10); + }); + + test('ifState methods work with Command2', () async { + final command = Command2( + (a, b) async => Success('$a: $b')); + String? receivedData; + + await command.execute('Count', 42); + command.state.ifSuccess((data) => receivedData = data); + + expect(receivedData, 'Count: 42'); + }); }); }