Data Shaft πŸš€

Data Shaft is a modular, robust, and type-safe data layer framework for Dart and Flutter. Built on Clean Architecture principles, it provides a standardized way to manage remote and local data sources, repositories, and error handling.

This package eliminates boilerplate code and solves common challenges such as error mapping, memory caching, and request deduplication.

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


✨ Key Features

  • βœ… Standardized Remote Drivers: Decouple your app from HTTP clients (Dio, Http, etc.).
  • βœ… Typed DataSources: Specialized mixins for GET, HEAD, POST, PUT, PATCH, and DELETE operations.
  • βœ… Advanced Repository Mixins: Built-in Memory Cache, Request Deduplication, and Safe Execution.
  • βœ… Full Observability: Lifecycle and network logging powered by dart:developer.
  • βœ… Structured Error Handling: Automatic mapping from DataSource exceptions to Repository errors.

πŸš€ Implementation Examples

1. Implementing the Driver (with Dio)

The RemoteDriver acts as an adapter. Here is how you bridge Dio with Data Shaft:

import 'package:dio/dio.dart';
import 'package:data_shaft/data_shaft.dart';

class DioRemoteDriver implements RemoteDriver<Response> {
  final Dio dio;
  new(this.dio);

  @override
  Future<RequestResponse<Response>> get(Uri uri, {Map<String, String>? headers, Object? options}) async {
    final response = await dio.getUri(uri, options: options as Options);
    return RequestResponse(
      statusCode: response.statusCode ?? 500,
      body: () => response.data.toString(),
      headers: response.headers.map.map((k, v) => MapEntry(k, v.join(','))),
      originalResponse: response,
    );
  }

  // Implement head, post, put, patch and delete following the same pattern
  // (forward `encoding` and `options` when needed)...
}

2. DataSource

2.1 Datasource Pre-build class

DataSources are specialized for specific operations. Use the pre-built base classes to save time:

final class UserParams extends Params {
  const new({required this.id, required this.name});
  final String id;
  final String name;

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

  @override
  bool get isValid => id.isNotEmpty;
}

class GetUserDataSource extends DatasourceGetRemote<User, MyDriver> {
  new({required super.driver});

  @override
  GetParams generateCallRequirement({required covariant UserParams params}) {
    return GetParams(urlParams: {'id': params.id});
  }
}

2.2 DataSource using Mixins

You can use mixins directly on a DatasourceRemote to define request behavior without deep inheritance:

class UpdateUserDataSource extends DatasourceRemote<User, DioRemoteDriver>
    with PatchCall<User, DioRemoteDriver> {
  new({required super.driver});

  @override
  PatchParams generateCallRequirement({required covariant UserParams params}) {
    return PatchParams(
      encodeBody: () => json.encode({'name': params.name}),
    );
  }
}

There is also a DatasourceHeadRemote/HeadCall for HEAD requests (no payload), useful for checking resource existence or fetching response headers only.

3. Safe Repository Execution

The SafeRepositoryDatasourceCallable catches all exceptions and converts them into Either types, shielding your Domain layer from crashes:

// The Repository handles safety, mapping, and deduplication
final class GetUserDetailRepository extends DeduplicationRepository<User, GetUserDataSource> {
  new({required super.dataSource});
  
  // Calling this repository returns: Future<Either<RepositoryError, User>>
}

// Usage in a Bloc, Controller, or UseCase:
final result = await userRepository(repositoryParams: UserParams(id: '123'));

result.fold(
  (error) => print('Error: ${error.message}'), // Inadmissible, UnControl, etc.
  (user) => print('User loaded: ${user.name}'),
);

3.1 Repository using Mixins

You can use mixins directly on a Repository to define datasource reply without deep inheritance:

class GetUserDetailRepository extends RepositoryDataSourceCallable<User, GetUserDataSource>
    with DeduplicationManagement<User, GetUserDataSource>, SafeRepositoryHelper<User> {
  
  new({required super.dataSource});

  @override
  Future<Either<RepositoryError, User>> call({
    required covariant Params repositoryParams,
  }) async {
    return safeCall(
      call: () => super.call(repositoryParams: repositoryParams),
    );
  }

  @override
  RepositoryError Function(InadmissibleDataSourceException, StackTrace)
      get onInadmissibleException => (exception, stackTrace) {
            return InadmissibleRepositoryError(
              message: 'User not found or unauthorized access',
            );
          };

  @override
  RepositoryError Function(UnControlDataSourceException, StackTrace)
      get onUnControlException => (exception, stackTrace) {
            return const UnControlRepositoryError(
              message: 'Communication error with the remote server',
            );
          };

  @override
  RepositoryError Function(Object, StackTrace) 
      get onException => (exception, stackTrace) {
            return const OnExceptionRepositoryError(
              message: 'Unexpected error during user data processing',
            );
          };
}

πŸ›  Layered Architecture

πŸ›‘ Safety First

The framework manages three error levels to ensure your UI never receives an unhandled exception:

  • InadmissibleRepositoryError: Controlled business failures (e.g., 404 Not Found).
  • OnExceptionRepositoryError: Failures during data transformation or orchestration.
  • UnControlRepositoryError: Unexpected failures (Null pointers, unexpected types).

⏱ Smart Caching

Use SafeMemoryCacheRepository to get an out-of-the-box memory cache with a configurable refreshDuration. A failed refresh clears the cache by default (refreshCache returns null, which means "clear"), so the next call always re-queries the datasource. Need a different policy? Override refreshCache β€” it is invoked after every call and gives you full control, e.g. return datasourceResponse.toNullable() ?? cache; keeps the last good value on failure.

πŸ‘― Deduplication

Prevent redundant requests. If two identical calls are triggered simultaneously, DeduplicationManagement ensures both wait for the same result, saving bandwidth and backend resources. If the underlying call throws, every waiter receives the same error and the in-flight key is released, so retries always start a fresh call.


πŸ“Š Observability

Data Shaft features an integrated logging system using dart:developer tags. You can filter your console by DS.REMOTE to see network traffic or REPO to see business logic flow.

// Example console output:
// [DS.REMOTE.GetUserDataSource] πŸ”› CALLING: [https://api.com/user/1](https://api.com/user/1)
// [REPO.GetUserRepository] βœ… Safe Call Complete | Result: Instance of 'User' | Elapsed: 42ms

Customize logging by implementing HttpDatasourceObserver or RepositoryObserver:

DatasourceObserverInstances.httpDatasourceObserver = MyCustomLogStrategy();

If you only implement a higher-level observer (e.g. HttpDatasourceObserver) and want it to also receive basic lifecycle events (onCreate, onDispose), enable the fallback:

DatasourceObserverInstances.httpDatasourceObserver = MyCustomLogStrategy();
DatasourceObserverInstances.useHigherObserver = true;

The same applies to repositories: RepositoryObserverInstances.useHigherObserver = true makes your safeCallableObserver serve as fallback for repositoryObserver and repositoryDatasourceCallableObserver.

To observe thrown DataSource failures at the repository level (with timing info), assign a RepositoryCallErrorObserver:

RepositoryObserverInstances.callErrorObserver = MyCallErrorReporter();

πŸ“š API Reference

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


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