Runner

Overview

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 StatesDescription

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:

PropertyDescription

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);
  }
}

Using a Runner

1. Start the Runner

To start the runner, call the run method:

  final countFruit = CountFruit();

  await countFruit.run(
    params: const CountFruitParams(['Apple', 'Orange', 'Apple']),
  );

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}'),
          );
      }
    },
  ),
);

Dart Example

To run the Dart demo:

  1. Clone to codenic_clean_arch repository.

  2. Navigate to the packages/codenic_bloc_use_case directory.

  3. Run the example code:

dart run example/main.dart

Last updated