239 lines
6.5 KiB
Markdown
239 lines
6.5 KiB
Markdown
<!--
|
|
* Copyright (C) 2023 WYATT GROUP
|
|
* Please see the AUTHORS file for details.
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* any later version.
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
-->
|
|
|
|
# Wyatt Architecture
|
|
|
|
<p align="left">
|
|
<a href="https://git.wyatt-studio.fr/Wyatt-FOSS/wyatt-packages/src/branch/master/packages/wyatt_analysis"><img src="https://img.shields.io/badge/Style-Wyatt%20Analysis-blue.svg?style=flat-square" alt="Style: Wyatt Analysis" /></a>
|
|
<img src="https://img.shields.io/badge/SDK-Dart%20%7C%20Flutter-blue?style=flat-square" alt="SDK: Dart & Flutter" />
|
|
</p>
|
|
|
|
The Wyatt Architecture for Flutter. Contains useful classes to help you to create a clean architecture following the Wyatt Architecture. (core, data, domain, presentation).
|
|
|
|
## Features
|
|
|
|
* Usecase
|
|
* Repository
|
|
* DataSource
|
|
* Entity
|
|
|
|
## Usage
|
|
|
|
### Domain
|
|
|
|
Create your entities by extending `Entity` :
|
|
|
|
```dart
|
|
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 `BaseDataSource` :
|
|
|
|
```dart
|
|
abstract class PhotoRemoteDataSource extends BaseDataSource {
|
|
Future<Photo> getPhoto(int id);
|
|
Future<List<Photo>> getAllPhotos({int? start, int? limit});
|
|
}
|
|
|
|
```
|
|
|
|
Then you can create your repositories by extenting `BaseRepository` :
|
|
|
|
```dart
|
|
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 :
|
|
|
|
```dart
|
|
class RetrieveAllAlbums extends AsyncUseCase<QueryParameters, List<Album>> {
|
|
const RetrieveAllAlbums(this._photoRepository);
|
|
final PhotoRepository _photoRepository;
|
|
|
|
@override
|
|
FutureOrResult<List<Album>> execute(QueryParameters params) {
|
|
final albums = _photoRepository.getAllAlbums(
|
|
start: params.start,
|
|
limit: params.limit,
|
|
);
|
|
return albums;
|
|
}
|
|
}
|
|
```
|
|
|
|
* No parameter usecase :
|
|
|
|
```dart
|
|
class DisplayFavorites extends NoParamsAsyncUseCase<List<Photo>> {
|
|
const DisplayFavorites(this._photoRepository);
|
|
final PhotoRepository _photoRepository;
|
|
|
|
@override
|
|
FutureOrResult<List<Photo>> execute() {
|
|
final photos = _photoRepository.getAllPhotosFromFavorites();
|
|
return photos;
|
|
}
|
|
}
|
|
```
|
|
|
|
You can add alternatve scenarios and check pre/post conditions using our extensions :
|
|
|
|
```dart
|
|
class RetrieveAllAlbums extends AsyncUseCase<QueryParameters, List<Album>> {
|
|
const RetrieveAllAlbums(this._photoRepository);
|
|
final PhotoRepository _photoRepository;
|
|
|
|
@override
|
|
FutureOrResult<List<Album>> execute(QueryParameters params) {
|
|
final albums = _photoRepository.getAllAlbums(
|
|
start: params.start,
|
|
limit: params.limit,
|
|
);
|
|
return albums;
|
|
}
|
|
|
|
@override
|
|
FutureOr<void> onStart(QueryParameters? params) {
|
|
if (params.start < 0) {
|
|
throw const ClientException('Invalid start parameter');
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
You can implement error scenarios overriding `onException` , or check postconditions by overriding `onComplete` .
|
|
|
|
* Stream usecase :
|
|
|
|
```dart
|
|
class RetrieveAllAlbums extends AsyncUseCase<QueryParameters, List<Album>> {
|
|
const RetrieveAllAlbums(this._photoRepository);
|
|
final PhotoRepository _photoRepository;
|
|
|
|
@override
|
|
FutureOrResult<List<Album>> execute(QueryParameters params) {
|
|
final albums = _photoRepository.getAllAlbums(
|
|
start: params.start,
|
|
limit: params.limit,
|
|
);
|
|
return albums;
|
|
}
|
|
|
|
@override
|
|
FutureOrResult<List<Album>> onException(Object e) => Ok([]);
|
|
}
|
|
```
|
|
|
|
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:
|
|
|
|
```dart
|
|
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:
|
|
|
|
```dart
|
|
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:
|
|
|
|
```dart
|
|
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.
|