generic_usecase 3.0.0 copy "generic_usecase: ^3.0.0" to clipboard
generic_usecase: ^3.0.0 copied to clipboard

Small component that encapsulates an application's scenario logic.

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;
}
copied to clipboard

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
copied to clipboard

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;
    }
  }
}
copied to clipboard

You can then use it like this:

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

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

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;
}
copied to clipboard

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);
  }
}
copied to clipboard

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);
  }
}
copied to clipboard

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(''));
  }
}
copied to clipboard
3
likes
160
points
64
downloads

Publisher

verified publisherhugop.cl

Weekly Downloads

2024.09.16 - 2025.03.31

Small component that encapsulates an application's scenario logic.

Repository (GitHub)

Documentation

API reference

License

MIT (license)

Dependencies

meta

More

Packages that depend on generic_usecase