Small component that encapsulates an application's scenario logic.

SDK: Dart & Flutter Maintained with Melos Pub.dev


[Changelog](./CHANGELOG.md) | [License](./LICENSE)


Introduction

Playing around with the clean architecture, I often found myself rewriting the generic code of my usecases.

These class enable you to encapsulate your logic in an atomic elements that you can then inject and use throughout your application.

Features

  • x Simple and easy to use API
  • x Fully tested (100% coverage)
  • x Fully documented
  • x sealed_result compatible (see sealed_result)

Available usecase types:

  • Usecase<Input, Output>
  • NoParamsUsecase<Output>
  • ResultUsecase<Input, Output, Failure>
  • NoParamsResultUsecase<Output, Failure>
  • StreamUsecase<Input, Output>
  • NoParamsStreamUsecase<Output>
  • StreamResultUsecase<Input, Output, Failure>
  • NoParamsStreamResultUsecase<Output, Failure>

Usage

Simple usecase

Let's say you want to add two numbers together, you can create a usecase like this:

class AdditionUsecase extends Usecase<int, int> {
  const AdditionUsecase();

  @override
  Future<int> execute(int params) async {
    return params + params;
  }
}

The execute method is the one that will be called when you call the call method on your usecase.

final usecase = AdditionUsecase();
final result = await usecase(2);
print(result); // 4

Checking preconditions

You can add a precondition check to your usecase, which will be executed before the execute method:

class DivisionUsecase extends Usecase<(int, int), double> {
  const DivisionUsecase();

  @override
  FutureOr<PreconditionsResult> checkPrecondition((int, int)? params) {
    if (params == null) {
      return PreconditionsResult(isValid: false, message: 'Params is null');
    }

    if (params.$2 == 0) {
      return PreconditionsResult(isValid: false, message: 'Cannot divide by 0');
    }

    return PreconditionsResult(isValid: true);
  }

  @override
  Future<double> execute((int, int) params) async {
    return params.$1 / params.$2;
  }
}

Using a result

You can use a result (see sealed_result) usecase to return a Result object instead of a raw value:

class DivisionResultUsecase extends ResultUsecase<(int, int), double, Failure> {
  const DivisionResultUsecase();

  @override
  FutureOr<PreconditionsResult> checkPrecondition((int, int)? params) {
    if (params == null) {
      return PreconditionsResult(isValid: false, message: 'Params is null');
    }

    if (params.$2 == 0) {
      return PreconditionsResult(isValid: false, message: 'Cannot divide by 0');
    }

    return PreconditionsResult(isValid: true);
  }

  @override
  Future<Result<double, Failure>> execute((int, int) params) async {
    return Result.success(params.$1 / params.$2);
  }

  @override
  Result<double, Failure> onException(UsecaseException e) =>
      Result.failure(Failure(e.message ?? ''));
}

You need to override the onException method to build the Failure object from the UsecaseException .

Using a stream

You can use a stream usecase to return a Stream instead of a raw value:

class GeneratorUsecase extends NoParamsStreamUsecase<int> {
  const GeneratorUsecase();

  @override
  Stream<int> execute() async* {
    for (int i = 0; i < 10; i++) {
      yield i;
    }
  }
}

Libraries

generic_usecase
Small component that encapsulates an application's scenario logic.