wyatt-packages/packages/wyatt_architecture
Hugo Pointcheval b1d66dc6e4
All checks were successful
continuous-integration/drone/push Build is passing
chore(release): publish packages
- wyatt_analysis@2.4.1
2023-02-24 10:20:00 +01:00
..
2023-02-24 10:20:00 +01:00
2023-02-23 19:19:48 +01:00
2022-11-08 20:08:55 -05:00
2022-12-04 21:08:15 -05:00
2023-02-24 10:20:00 +01:00

Flutter - Wyatt Architecture

<img src="https://img.shields.io/badge/Style-Wyatt%20Analysis-blue.svg?style=flat-square" alt="Style: Wyatt Analysis" />
SDK: Flutter

The Wyatt Architecture for Flutter.

Features

  • Usecase
  • Repository
  • DataSource
  • Entity

Usage

Domain

Create your entities by extending Entity :

class Photo extends Entity {
  final int id;
  final String url;

  const Photo({
    required this.id,
    required this.url,
  });
}

Then create the data sources by extending BaseLocalDataSource or BaseRemoteDataSource depending the type of data source.

abstract class PhotoRemoteDataSource extends BaseRemoteDataSource {
  Future<Photo> getPhoto(int id);
  Future<List<Photo>> getAllPhotos({int? start, int? limit});
}

Then you can create your repositories by extenting BaseRepository :

abstract class PhotoRepository extends BaseRepository {
  FutureResult<Photo> getPhoto(int id);
  FutureResult<List<Photo>> getAllPhotos({int? start, int? limit});
}

Here the repository is just a proxy of the data sources with result type (to have beautiful error handling).

And finaly create your different usecases :

Several use cases are supported :

  • Classic usecase :
class Test extends AsyncUseCase<QueryParameters, List<Photo>>> {

  @override
  FuturOrResult<List<Photo>>> call(QueryParameters? params) {
     final photos = _photoRepository.getAllPhotos(
      start: params.start,
      limit: params.limit,
    );
    return photos;
  }
}

You can add alternatve scenarios and check pre/post conditions using our extensions :

class SearchPhotos extends AsyncUseCase<QueryParameters, List<Photo>>> {

  @override
  FutureOrResult<List<Photo>>> call(QueryParameters? params) {
     final photos = _photoRepository.getAllPhotos(
      start: params.start,
      limit: params.limit,
    );
    return photos;
  }


  @override
  FutureOr<void> onStart(QueryParameters? params) {
    if(params.start == null || params.limit == null){
      throw ClientException('Préconndition non valides');
    }
  }

}

You can implement error scenarios overriding onError, or check postconditions by overriding onComplete .

  • Stream usecase :
class SearchPhotos extends StreamUseCase<QueryParameters, List<Photo>>> {

  @override
  FutureOrResult<Stream<List<Photo>>>> call(QueryParameters? params) {
     final photos = _photoRepository.getAllPhotos(
      start: params.start,
      limit: params.limit,
    );
    return photos;
  }

}

On this case, observers allow you to add alternative scénarios when data changed, overriding onData or onDone.

Please note that to use handlers, call call method and not execute.

In fact, here we need a new parameter object, so let's create it:

class QueryParameters {
  final int? start;
  final int? limit;

  QueryParameters(this.start, this.limit);
}

Data

We start by creating models for photos and list of photos. You can use freezed. The PhotoModel extends Photo with some de/serializer capabilities. And those are used only in data layer.

Then implements your data sources:

class PhotoApiDataSourceImpl extends PhotoRemoteDataSource {
  final MiddlewareClient _client;

  PhotoApiDataSourceImpl(this._client);

  @override
  Future<Photo> getPhoto(int id) async {
    final response = await _client.get(Uri.parse('/photos/$id'));
    final photo =
        PhotoModel.fromJson(jsonDecode(response.body) as Map<String, Object?>);
    return photo;
  }

  @override
  Future<List<Photo>> getAllPhotos({int? start, int? limit}) async {
    final startQuery = start.isNotNull ? '_start=$start' : '';
    final limitQuery = limit.isNotNull ? '_limit=$limit' : '';
    final delimiter1 =
        (startQuery.isNotEmpty || limitQuery.isNotEmpty) ? '?' : '';
    final delimiter2 =
        (startQuery.isNotEmpty && limitQuery.isNotEmpty) ? '&' : '';
    final url = '/photos$delimiter1$startQuery$delimiter2$limitQuery';
    final response = await _client.get(Uri.parse(url));
    final photos =
        ListPhotoModel.fromJson({'photos': jsonDecode(response.body)});
    return photos.photos;
  }
}

1: Note that here we use MiddlewareClient from our http package.

2: You can create multiple implementations (one real and one mock for example).

And implement the repositories:

class PhotoRepositoryImpl extends PhotoRepository {
  final PhotoRemoteDataSource _photoRemoteDataSource;

  PhotoRepositoryImpl(
    this._photoRemoteDataSource,
  );

  @override
  FutureResult<Photo> getPhoto(int id) => Result.tryCatchAsync(
        () => _photoRemoteDataSource.getPhoto(id),
        (error) => ServerException('Cannot retrieve photo $id.'),
      );

  @override
  FutureResult<List<Photo>> getAllPhotos({int? start, int? limit}) async =>
      Result.tryCatchAsync(
        () => _photoRemoteDataSource.getAllPhotos(start: start, limit: limit),
        (error) => ServerException('Cannot retrieve all photos.'),
      );
}

That's all.