Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>) {
print('Success: ${status.value}');
} else if (status is FailureCommand<String>) {
print('Failure: ${status.error}');
final state = fetchGreetingCommand.state;
if (state is SuccessCommand<String>) {
print('Success: ${state.value}');
} else if (state is FailureCommand<String>) {
print('Failure: ${state.error}');
}
});
```
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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.');
}
```
Expand Down
2 changes: 1 addition & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
26 changes: 13 additions & 13 deletions example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand All @@ -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"
135 changes: 91 additions & 44 deletions lib/src/command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ sealed class Command<T extends Object> extends ChangeNotifier

Command([this.onCancel, int maxHistoryLength = 10]) : super() {
initializeHistoryManager(maxHistoryLength);
_setValue(IdleCommand<T>(), metadata: {'reason': 'Command created'});
_setState(IdleCommand<T>(), metadata: {'reason': 'Command created'});
}

/// Sets the default observer listener for all commands.
Expand All @@ -34,7 +34,7 @@ sealed class Command<T extends Object> extends ChangeNotifier
}

/// The current state of the command.
CommandState<T> _value = IdleCommand<T>();
CommandState<T> _state = IdleCommand<T>();

SuccessCommand<T>? _cachedSuccessCommand;
FailureCommand<T>? _cachedFailureCommand;
Expand All @@ -51,28 +51,13 @@ sealed class Command<T extends Object> 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<T> get state => _state;

/// Prefer using the `state` property instead.
/// Used for implementation with ValueListenable interface.
@override
CommandState<T> get value => _value;
CommandState<T> get value => _state;

/// Filters the current command state and returns a ValueListenable with the transformed value.
///
Expand Down Expand Up @@ -111,15 +96,15 @@ sealed class Command<T extends Object> extends ChangeNotifier
/// If the command is in the [RunningCommand] state, the [onCancel] callback is invoked,
/// and the state transitions to [CancelledCommand].
void cancel({Map<String, dynamic>? metadata}) {
if (value.isRunning) {
if (state.isRunning) {
try {
onCancel?.call();
} catch (e) {
_setValue(FailureCommand<T>(e is Exception ? e : Exception('$e')),
_setState(FailureCommand<T>(e is Exception ? e : Exception('$e')),
metadata: metadata);
return;
}
_setValue(CancelledCommand<T>(),
_setState(CancelledCommand<T>(),
metadata: metadata ?? {'reason': 'Manually cancelled'});
}
}
Expand All @@ -128,15 +113,77 @@ sealed class Command<T extends Object> extends ChangeNotifier
///
/// This clears the current state, allowing the command to be reused.
void reset({Map<String, dynamic>? metadata}) {
if (value.isRunning) {
if (state.isRunning) {
return;
}
_cachedFailureCommand = null;
_cachedSuccessCommand = null;
_setValue(IdleCommand<T>(),
_setState(IdleCommand<T>(),
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<String>(() 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<T>():
(onIdle ?? orElse)?.call();
case CancelledCommand<T>():
(onCancelled ?? orElse)?.call();
case RunningCommand<T>():
(onRunning ?? orElse)?.call();
case FailureCommand<T>(:final error):
onFailure != null ? onFailure(error) : orElse?.call();
case SuccessCommand<T>(: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,
Expand All @@ -145,10 +192,10 @@ sealed class Command<T extends Object> 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<void> _execute(CommandAction0<T> action, {Duration? timeout}) async {
if (value.isRunning) {
if (state.isRunning) {
return;
} // Prevent multiple concurrent executions.
_setValue(RunningCommand<T>(), metadata: {'status': 'Execution started'});
_setState(RunningCommand<T>(), metadata: {'status': 'Execution started'});
bool hasError = false;

late Result<T> result;
Expand All @@ -163,44 +210,44 @@ sealed class Command<T extends Object> extends ChangeNotifier
}
} catch (e, stackTrace) {
hasError = true;
_setValue(FailureCommand<T>(Exception('Unexpected error: $e')),
_setState(FailureCommand<T>(Exception('Unexpected error: $e')),
metadata: {'error': '$e', 'stackTrace': stackTrace.toString()});
return;
} finally {
if (!hasError) {
final newValue = result
final newState = result
.map(SuccessCommand<T>.new)
.mapError(FailureCommand<T>.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<T> newValue) {
if (newValue is SuccessCommand<T>) {
_cachedSuccessCommand = newValue;
} else if (newValue is FailureCommand<T>) {
_cachedFailureCommand = newValue;
void _updateCache(CommandState<T> newState) {
if (newState is SuccessCommand<T>) {
_cachedSuccessCommand = newState;
} else if (newState is FailureCommand<T>) {
_cachedFailureCommand = newState;
}
}

/// Sets the current state of the command and notifies listeners.
///
/// Additionally, records the change in the state history with optional metadata
/// and updates the cache.
void _setValue(CommandState<T> newValue, {Map<String, dynamic>? metadata}) {
if (newValue.instanceName == _value.instanceName &&
void _setState(CommandState<T> newState, {Map<String, dynamic>? 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();
}
}
Expand Down
Loading