Skip to content

hugo-pcl/usecase

Repository files navigation

Small component that encapsulates an application's scenario logic.

SDK: Dart & Flutter Maintained with Melos Pub.dev


[Changelog] | [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

  • Simple and easy to use API
  • Fully tested (100% coverage)
  • Fully documented

Available usecase types:

  • Usecase<Input, Output>
  • NoParamsUsecase<Output>
  • StreamUsecase<Input, Output>
  • NoParamsStreamUsecase<Output>

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
  FutureOr<int> execute(int params) async => params + params;
}

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

final addition = AdditionUsecase();
await addition(2).then(print, onError: print); // 4

Using a stream usecase

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++) {
      await Future<void>.delayed(const Duration(seconds: 1));
      yield i;
    }
  }
}

You can then use it like this:

final generator = GeneratorUsecase();
final stream = generator();

stream.listen(
  print,
  onError: print,
  onDone: () => print('Done'),
);

Checking preconditions and postconditions

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<ConditionsResult> checkPreconditions((int, int)? params) {
    if (params == null) {
      return ConditionsResult(isValid: false, message: 'Params is null');
    }

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

    return ConditionsResult(isValid: true);
  }

  @override
  FutureOr<double> execute((int, int) params) async => params.$1 / params.$2;
}

You can also add a postcondition check to your usecase, which will be executed after the execute method:

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

  @override
  FutureOr<int> execute(int params) async => params + params;

  @override
  FutureOr<ConditionsResult> checkPostconditions(int? result) {
    if (result == null) {
      return ConditionsResult(isValid: false, message: 'Result is null');
    }

    if (result < 0) {
      return ConditionsResult(isValid: false, message: 'Result is negative');
    }

    return ConditionsResult(isValid: true);
  }
}

Catching exceptions

You can catch exceptions thrown by your usecase by overriding the onException method:

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

  @override
  FutureOr<int> execute(int params) async => params + params;

  @override
  FutureOr<int> onException(Object e) {
    print(e); // Prints the exception
    return super.onException(e);
  }
}

This method will be called when an exception is thrown by the execute method. It will also be called when a precondition or postcondition check fails.

Using a Result

By assembling the previous examples, you can create a usecase that returns a Result object. By catching exceptions and checking preconditions and postconditions, you can return a Result object that will be either a Success or a Failure :

This example uses the sealed_result package.

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

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

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

    return ConditionsResult(isValid: true);
  }

  @override
  FutureOr<Result<double, Failure>> execute((int, int) params) async =>
      Result.success(params.$1 / params.$2);

  @override
  FutureOr<Result<double, Failure>> onException(Object e) {
    if (e case UsecaseException _) {
      return Result.failure(Failure(e.message ?? ''));
    }
    if (e case Exception || Error) {
      return Result.failure(Failure(e.toString()));
    }
    return Result.failure(Failure(''));
  }
}