cool_bedrock

A bedrock package providing the blueprints and abstract definitions necessary to build a scalable and maintainable Dart/Flutter application.

Pub Version Pub Likes Pub Points Pub Downloads Dart SDK Version License codecov


✨ Features

  • πŸ›οΈ Formalized Architecture: Strict base contracts for all layers: Entity, Params, AppService, and Codable.
  • 🎯 Domain Logic (Use Cases): Typified UseCase hierarchy for commands and queries, including assisted flow management (UseCaseHandler).
  • πŸ›‘οΈ Functional Error Handling: Leverages the functional types Either<Failure, T> and Option
  • πŸ›‘ Typed Errors: Coherent error structure using sealed base classes: Issue, Failure (business logic), RepositoryError, and DataSourceException (technical/infrastructure).
  • πŸ”„ Reactive Services: Base classes for creating services that manage state using BehaviorSubject, PublishSubject, and periodic execution logic (Timer).
  • πŸ§ͺ Immutability & Testability: All core domain structures (Entity, Params) are immutable and comparable (Equatable).

πŸš€ Installation

Requires Dart >=3.13.0.

Add the package to your pubspec.yaml:

dependencies:
  cool_bedrock: ^3.0.0

Then run:

dart pub get

πŸ“† Usage

Basic usage

import 'package:cool_bedrock/cool_bedrock.dart';

1. Creating the Use Case

This example demonstrates how to implement a UseCase, defining its specific Failure and parameter validation:

// 1. Define the domain types: params, entity and the raw remote model.
final class FetchUserParams extends Params {
  const FetchUserParams({required this.userId});

  final String userId;

  // Checked automatically before executing the usecase.
  @override
  bool get isValid => userId.isNotEmpty;

  @override
  List<Object?> get props => [userId];
}

final class UserRemote {
  const UserRemote({this.name});

  final String? name;

  UserEntity toEntity() => UserEntity(name: name ?? 'Unknown');
}

final class UserEntity extends Entity {
  const UserEntity({required this.name});

  final String name;

  @override
  List<Object?> get props => [name];
}

// In this example the repository returns a Future<Either<Failure, UserRemote>>.

// 2. Define the specific Failure for this domain
sealed class FetchUserFailure extends Failure {
  const FetchUserFailure({super.message});
}

final class InvalidUserFailure extends FetchUserFailure {
  const InvalidUserFailure() : super(message: 'Invalid User ID provided.');

  @override
  List<Object?> get props => [message];
}

final class InvalidParamsUserFailure extends FetchUserFailure {
  const InvalidParamsUserFailure()
      : super(message: 'Invalid parameters provided.');

  @override
  List<Object?> get props => [message];
}

// 3. Implement the UseCase contract
final class FetchUserUseCase
    extends UseCase<UserEntity, FetchUserParams, FetchUserFailure> {
  const FetchUserUseCase(this.repository);

  final UserRepository repository;

  // Called automatically if params.isNotValid is true.
  @override
  FetchUserFailure onInvalidParams() => const InvalidParamsUserFailure();

  @override
  Future<Either<FetchUserFailure, UserEntity>> execute(
    FetchUserParams params,
  ) async {
    // Core logic goes here. Mappers and Repositories are typically called here.
    try {
      final result = await repository.fetch(params.userId);
      return result.fold(
        // Map the repository failure to a domain failure
        (failure) => const Left(InvalidUserFailure()),
        // Map the remote model to the entity
        (remote) => Right(remote.toEntity()),
      ); // Success
    } catch (e) {
      // Map low-level errors to high-level domain failures
      return const Left(InvalidUserFailure()); // Failure
    }
  }
}

// 4. Or use the Handler: obtain -> transform -> map errors
final class FetchUserUseCaseHandle
    extends UseCaseHandler<
      UserEntity,
      FetchUserParams,
      FetchUserFailure,
      UserRemote
    > {
  const FetchUserUseCaseHandle({required this.repository});

  final UserRepository repository;

  // Called automatically if params.isNotValid is true.
  @override
  FetchUserFailure onInvalidParams() => const InvalidParamsUserFailure();

  // Obtain repository values. Multiple repositories can be called.
  @override
  FutureOr<UserRemote> obtainValues(
    Resolver<FetchUserFailure> $,
    FetchUserParams params,
  ) async {
    // getValue unwraps a Future<Either<Issue, VALUE>> and maps its errors.
    return await $(getValue(() => repository.fetch(params.userId)));
  }

  @override
  UserEntity transformation(UserRemote values) {
    if (values.name == null || values.name!.isEmpty) {
      throw const UsecaseException(InvalidUserFailure());
    }
    // It can throw any exception, it will be handled by [wrapError].
    return values.toEntity();
  }

  @override
  FetchUserFailure wrapError(Object error, StackTrace stackTrace) {
    return const InvalidUserFailure();
  }
}

2. Execution and Error Handling

// Execution with valid parameters
const validParams = FetchUserParams(userId: 'user_123');
final validResult = await fetchUserUsecase.call(validParams);

validResult.fold(
  // LEFT side (Failure)
  (failure) => print('Error: ${failure.message}'),
  // RIGHT side (Success)
  (user) => print('Fetched User: ${user.name}'),
);

πŸ’‘ Reactive Services Example

The base services provide lifecycle control and reactivity. Here's a service that periodically updates a counter:

import 'package:cool_bedrock/cool_bedrock.dart';
import 'dart:async';

final class HeartbeatService extends TimerAndBehaviorService<int> {
  HeartbeatService()
      : super(periodicDuration: const Duration(seconds: 10));

  int _counter = 0;

  @override
  Future<void> work() async {
    // Logic that runs every 10 seconds
    _counter++;
    // Emit the new value to all subscribers
    add(_counter);
  }

  // start(), stop(), and dispose() logic is inherited and controlled externally.
}


πŸ—οΈ Use Case Types Comparison

Choosing the right base class ensures your business logic is expressive and safe. Use this table as a quick guide to decide which one fits your needs:

Use Case Type Success Return Error Return Best For...
UseCase Right(Entity) Left(Failure) Standard business logic with manual error mapping.
UseCaseHandler Right(Entity) Left(Failure) Business logic with data fetching, transformation and error mapping.
OneWayUseCase Some(Entity) None Queries where the absence of a value is a valid result (e.g., Search).
OneWayFailureUseCase None (Success) Some(Failure) Standalone validations and guard checks (e.g., Is email taken?).

πŸ“š API Reference

Check the full API reference, including all generic types and abstract classes, on pub.flutter-io.cn β†’ cool_bedrock.


Authors & Maintainers

This project was created and is primarily maintained by:

🀝 Contributing

Contributions are welcome!

  • Open issues for bugs or feature requests
  • Fork the repo and submit a PR
  • Run dart format and dart test before submitting

πŸ§ͺ Testing

To run tests and see code coverage:

dart test

πŸ“„ License

MIT Β© 2025 Coolosos

Libraries

cool_bedrock