Wyatt Architecture
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
:
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
:
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
:
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 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 :
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 :
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 :
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:
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.