A simple use case that executes a specific task which returns a Left Value when an error occurs or a Right Value when the task succeeds.
This is ideal for create, read, update and delete (CRUD) operations.
A Runner use case can emit 4 possible class states, all of which inherit from the RunnerState sealed class:
Runner States
Description
RunnerInitial
The initial state or the state emitted when the use case has been reset.
Running
The state emitted when the task execution is in progress.
RunSuccess
The state emitted when the use case fails.
RunFailed
The state emitted when the use case succeeds.
A Runner also has the following properties that you can access:
Property
Description
value
The latest value returned when calling run(). This may either be the leftValue if RunFailed was recently emitted. Otherwise, this will be equal to the rightValue if RunSuccess was more recent.
params
The latest params passed when calling run(). This may either be the leftParams if RunFailed was recently emitted. Otherwise, this will be equal to the rightParams if RunSuccess was more recent.
leftValue
The last left value returned when a failed run() was called.
leftParams
The last left params passed when a failed run() was called.
rightValue
The last right value returned when a successful run() was called.
rightParams
The last right params passed when a successful run() was called.
Creating a Runner
Let's create a Runner use case named CountFruits for categorizing and counting the number of fruits given in our fruit basket.
Create the following classes:
1. Create the Parameter class
This is the parameter class passed to the Runner when being executed.
/// The parameter for the [CountFruit] use case containing all the available
/// fruits to count.
class CountFruitParams {
const CountFruitParams(this.fruits);
/// The fruits in our fruit basket.
///
/// Example:
/// [ apple, orange, mango, apple, apple, mango ]
final List<String> fruits;
@override
String toString() => {'fruits': fruits}.toString();
}
2. Create the Left value class
This is the object returned when the Runner fails and emits a RunFailed state:
class Failure {
const Failure(this.message);
final String message;
@override
String toString() => 'Failure: $message';
}
3. Create the Right value class
This is the object returned when the Runner succeeds and emits a RunSuccess state:
/// The right value for [CountFruit] containing the count for each fruit.
class CountFruitResult {
const CountFruitResult(this.fruitCount);
/// The fruits counted by type
///
/// Example:
/// { apple: 3, orange: 1, mango: 2 }
final Map<String, int> fruitCount;
@override
String toString() => '$fruitCount';
}
4. Create the Runner
Pass the Parameter, Left and Right classes to the Runner's generic arguments. Afterwards, implement the code logic in the onCall method:
/// A runner that counts the quantity of each given fruit.
class CountFruit extends Runner<CountFruitParams, Failure, CountFruitResult> {
/// A callback function triggered when the [run] method is called.
@override
FutureOr<Either<Failure, CountFruitResult>> onCall(
CountFruitParams params,
) async {
if (params.fruits.isEmpty) {
// When the given fruits is empty, then a left value is returned
return const Left(Failure('There are no fruits to count'));
}
final fruitCount = <String, int>{};
for (final fruit in params.fruits) {
fruitCount[fruit] = (fruitCount[fruit] ?? 0) + 1;
}
// Returns a right value containing the fruit count
final result = CountFruitResult(fruitCount);
return Right(result);
}
}
The Runner will emit the Running state, followed either by the RunSuccess with a Right Value or RunFailed state with a Left Value depending on the result of the runner.
2. View the Runner properties
The Runner gives you access to the following properties:
// The recent value returned when calling `run()`. This may either be the
// `leftValue` if the state current is `RunFailed` or the `rightValue` if the
// current state is `RunSuccess`
print('Current value: ${runner.value}');
// The recent params passed when calling `run()`. This may either be the
// `leftParams` if the state current is `RunFailed` or the `rightParams` if
// the current state is `RunSuccess`
print('Current params: ${runner.params}');
// The last left value returned when a failed `run()` was called
print('Last left value: ${runner.leftValue}');
// The last left params passed when a failed `run()` was called
print('Last left params: ${runner.leftParams}');
// The last right value returned when a successful `run()` was called
print('Last right value: ${runner.rightValue}');
// The last right params passed when a successful `run()` was called
print('Last right params: ${runner.rightParams}');
3.Reset the Runner
To clear the Runner and reset it back to its initial state, call the reset method:
runner.reset();
Flutter Example
Every use case is a descendent of BLoC cubit. Hence, we can manage its states via the flutter_bloc package.
BlocProvider(
lazy: false,
create: (context) =>
// Initialize and eagerly run the Runner
CountFruit()
..run(
params:
const CountFruitParams(['apple', 'orange', 'mango', 'apple']),
),
child: BlocConsumer<CountFruit, RunnerState>(
listener: (context, runnerState) {
if (runnerState is RunnerInitial) {
// Handle initial state. This is also triggered when the Runner has
// been reset
} else if (runnerState is Running) {
// Handle running state
} else if (runnerState is RunFailed<Failure>) {
// Handle failure
print('Left value: ${runnerState.leftValue}');
} else if (runnerState is RunSuccess<CountFruitResult>) {
// Handle success
print('Right value: ${runnerState.rightValue}');
}
},
builder: (context, runnerState) {
switch (runnerState) {
case RunnerInitial():
return Container();
case Running():
return const Center(child: CircularProgressIndicator());
case RunFailed():
return Center(
child: Text('Left value: ${runnerState.leftValue}'),
);
case RunSuccess():
return Center(
child: Text('Right value: ${runnerState.rightValue}'),
);
}
},
),
);