diff --git a/README.md b/README.md index a6d92ee..da1577f 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.value}'); + } else if (state is FailureCommand) { + print('Failure: ${state.error}'); } }); ``` @@ -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: (value) => 'Success: $value', 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.'); } ``` 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/src/command.dart b/lib/src/command.dart index ec3b0e6..c81a074 100644 --- a/lib/src/command.dart +++ b/lib/src/command.dart @@ -23,7 +23,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,7 +34,7 @@ sealed class Command extends ChangeNotifier } /// The current state of the command. - CommandState _value = IdleCommand(); + CommandState _state = IdleCommand(); SuccessCommand? _cachedSuccessCommand; FailureCommand? _cachedFailureCommand; @@ -51,28 +51,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. /// @@ -111,15 +97,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 +114,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: (value) => print('Success: $value'), + /// 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 value)? 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 value): + onSuccess != null ? onSuccess(value) : 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 +193,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 +211,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 +240,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/states.dart b/lib/src/states.dart index f14c4b2..63fe833 100644 --- a/lib/src/states.dart +++ b/lib/src/states.dart @@ -6,46 +6,84 @@ sealed class CommandState { /// 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 a value 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: (value) => Text('Success: $value'), + /// 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 value)? 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 value) => success?.call(value) ?? orElse(), + }; + } + + /// 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 a value 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: (value) => context.go('/success'), + /// failure: (e) => showErrorSnackBar(e), + /// ); + /// ``` + R? maybeWhen({ + R Function(T value)? 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 value) => success?.call(value) ?? orElse?.call(), }; } diff --git a/pubspec.yaml b/pubspec.yaml index 0fdbd59..4f71bb9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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_ref_test.dart b/test/src/command_ref_test.dart index 51552de..2eaf1c6 100644 --- a/test/src/command_ref_test.dart +++ b/test/src/command_ref_test.dart @@ -13,7 +13,7 @@ void main() { ); commandRef.addListener(expectAsync0(() { - final status = commandRef.value; + final status = commandRef.state; if (status is SuccessCommand) { expect(status.value, 10); } diff --git a/test/src/command_test.dart b/test/src/command_test.dart index ca14ff6..1ffff4d 100644 --- a/test/src/command_test.dart +++ b/test/src/command_test.dart @@ -37,7 +37,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute(); @@ -46,7 +46,7 @@ void main() { isA>(), ]); - expect(command.value, isA>()); + expect(command.state, isA>()); // Verify history final history = command.stateHistory; @@ -72,7 +72,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute('Test'); @@ -81,8 +81,8 @@ void main() { isA>(), ]); - expect(command.value, isA>()); - expect((command.value as SuccessCommand).value, 'Test'); + expect(command.state, isA>()); + expect((command.state as SuccessCommand).value, 'Test'); // Verify history final history = command.stateHistory; @@ -110,7 +110,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 +121,7 @@ void main() { isA>(), ]); - expect(command.value, isA>()); + expect(command.state, isA>()); // Verify history final history = command.stateHistory; @@ -141,7 +141,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute(); @@ -150,8 +150,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 +176,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute(); @@ -184,8 +184,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 +212,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 +228,7 @@ void main() { command.execute(); command.cancel(); - expect(command.value, isA>()); + expect(command.state, isA>()); expect(command.stateHistory.last.state, isA>()); }); @@ -242,7 +242,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 +259,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 +289,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).value, 'INPUT'); // Verify history final history = command.stateHistory; @@ -318,12 +318,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).value, 'Value 42'); // Verify history final history = command.stateHistory; @@ -358,7 +358,7 @@ void main() { final states = >[]; command.addListener(() { - states.add(command.value); + states.add(command.state); }); await command.execute(); @@ -386,10 +386,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 +403,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 +414,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 +426,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 +440,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 +456,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 +475,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 +492,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 +522,461 @@ 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>()); + }); }); }