A camada domain provê as regras e as unidades de negócio usadas na aplicação. Nesta camada estão os Services que são consumidos pelos BLoCs/PageControllers da camada app, as Entities e os contratos dos repositórios de dados, cuja implementação concreta fica na camada data.
A responsabilidade de cada um dos elementos acima é:
Representação (modelo) de uma unidade do negócio. Mantém apenas os atributos da unidade e métodos auxiliares que iteram exclusivamente nesses atributos. No escopo desta aplicação, as entidades devem implementar a interface Jsonable
porque, como estamos persistindo os dados localmente em SharedPreferences
como strings no formato JSON, os métodos fromJson()
e toJson()
são especialmente importantes. Exemplo:
class UserEntity implements Jsonable {
String id;
String name;
String email;
String phone;
UserEntity({
this.id,
this.name,
this.email,
this.phone,
});
factory UserEntity.fromJson(Map<String, dynamic> json) => UserEntity(
id: json["id"],
name: json["name"],
email: json["email"],
phone: json["phone"],
);
Map<String, dynamic> toJson() {
final Map<String, dynamic> json = Map();
json['id'] = this.id;
json['name'] = this.name;
json['email'] = this.email;
json['phone'] = this.phone;
return json;
}
String get firstName => name != null ? name.split(' ')[0] : '';
}
Apenas expõe os **contratos ** dos métodos que buscam ou persistem os dados relativos a uma Entity (nada impede que o repositório lide com mais de uma entidade, mas não é recomendado). Normalmente, os clientes dos repositórios são os Services e estes conhecem apenas os contratos, não a implementação concreta. Exemplo:
abstract class AuthLocalRepository {
Future<UserCredentialsEntity> getCredentials();
Future<bool> saveCredentials(UserCredentialsEntity credentials);
Future<void> clearLocalData();
}
É onde são implementadas as regras de negócio da aplicação. Mesmo que as regras de fato estejam no back-end, faz sentido que o front-end replique algumas delas ou tenha as suas próprias. As classes do tipo Service são as que mantém estas regras e as expõem na forma de métodos para que um cliente qualquer possa consumi-los. Estas classes recebem parâmetros de seus clientes, validam, processam e lhes dão um retorno. Sabem quais validações fazer, quais os dados devem ser buscados dos repositórios, quando busca-los e quando persisti-los. Exemplo:
class UserService {
UserLocalRepository _localRepository;
UserRemoteRepository _remoteRepository;
UserService({
@required UserLocalRepository localRepository,
@required UserRemoteRepository remoteRepository,
}) {
_localRepository = localRepository;
_remoteRepository = remoteRepository;
}
Future<bool> updateUser(
UserEntity user,
) async {
bool updateResult;
// !fakeuserId
if (user.id != 'abc123') {
updateResult = await _remoteRepository.updateUser(user);
} else
updateResult = true;
if (updateResult) {
final bool saveResult = await _localRepository.saveUser(user);
if (saveResult) return true;
}
return false;
}
}
A camada app é responsável por apresentar os dados para o usuário e capturar seus inputs. Nesta camada estão pages, views, widgets, streams, providers, blocs/pageControllers.
Qualquer elemento dessa camada pode conhecer as Entities da camada domain, podendo recebê-las como resultado de algum serviço e escolher quais os dados das entidades serão exibidos na tela e como o serão.
Os elementos da camada app também podem manipular as entidades (settando dados que o usuário inputou, por exemplo) e solicitar que os respectivos serviços as persistam, seja local ou remotamente.
Seria responsabilidade dos BLoCs/PageControllers usarem as ações expostas pelas classes Services da camada domain, controlando o fluxo dos dados da aplicação para prover uma funcionalidade completa. Ou seja, dentro das classes BLoCs/PageControllers serão injetados os serviços.
A camada data é a camada de acesso aos dados. Ela tem a implementação dos Repositories definidos na camada domain, seja local ou remoto. Tem também os Data Transfer Objects (DTOs), que são os dados crus trafegados nos data sources, e também sabe transcrever estes dados crus para Entities. Nenhum de seus elementos possui regra de negócio.
A responsabilidade de cada um dos elementos citados acima é:
Implementa os métodos definidos nos repositórios da camada domain. Não manipula os dados e não tem nenhuma regra de negócio, apenas sabe o caminho para buscar o DTO e qual o formato da query para busca-lo. Exemplo:
class UserRemoteRepositoryImp implements UserRemoteRepository {
HttpService _httpService; //data source
UserRemoteRepositoryImp({
@required HttpService httpService,
dynamic httpRequestAdapter,
}) {
this._httpService = httpService;
}
@override
Future<UserEntity> saveUser(UserEntity user) async {
try {
String url = '/users';
UserDTO result = await _httpService.post(url, data: user.toJson());
return UserDTO.toUserEntity(result);
} catch (err) {
throw InternalServerError();
}
}
}
Abstração do dado cru fornecido pelo e para o data source. Mapeia o dado de acordo com o data source onde ele é trafegado. No máximo contém a transcrição do dado para a entidade correspondente. Exemplo:
class RequestCodeResponseDTO {
String email;
String phone;
RequestCodeResponseDTO({
this.email,
this.phone,
});
factory RequestCodeResponseDTO.fromJson(Map<String, dynamic> json) =>
RequestCodeResponseDTO(
email: json["email"],
phone: json["phone"],
);
Map<String, dynamic> toJson() => {
"email": email,
"phone": phone,
};
RequestCodeEntity toRequestCodeEntity() => RequestCodeEntity(
email: this.email,
phone: this.phone,
);
}
Esta camada agrupa os elementos mais baixo nível da aplicação, como acesso à base de dados, à arquivos e requisições HTTP.
lib
├── domain
| └── auth
| ├── entities
| | ├── UserEntity
| | └── UserAddressEntity
| ├── repositories
| | ├── UserLocalRepository
| | └── UserRemoteRepository
| └── services
| └── UserService
├── data
| └── auth
| ├── dtos
| | ├── UserDTO
| └── repositories_imp
| ├── UserLocalRepositoryImp
| └── UserRemoteRepositoryImp
├── app
| ├── pages
| | └── home
| | ├── HomePage
| | └── HomeBloc
| └── widgets
| └── FooWidget
└── infrastructure
└── local_storate
└── shared_preferences
└── SharedPreferencesServices
class HomeBloc {
UserEntity user;
UserService service;
/// Chama o UserService->fetchUser(), altera o estado da página para loading, por exemplo, salva nas streams ou onde precisar para ser consumido na tela.
Future<UserEntity> fetchUser(String userId) async {
// lógica para alterar estado da tela
user = await service.fetchUser(String userId);
}
}
class UserService {
UserLocalRepository localRepository;
UserRemoteRepository remoteRepository;
/// Verifica se ja possui um user cacheado utilizando UserLocalRepository
/// Caso não tenha, busca utilizando UserRemoteRepository
/// Retorna o UserEntity encontrado, seja ele local ou remoto
Future<UserEntity> fetchUser(String userId) async {
UserEntity result;
result = await localRepository.getUser(String userId);
if (result == null)
result = await remoteRepository.getUser(String userId);
return result;
}
class UserLocalRepositoryImp implements UserLocalRepository {
DataProvider dataProvider;
/// Procura na chave definida os dados de um user salvo e retorna uma entity
Future<UserEntity> getUser(String userId) async {
return await dataProvider.getObject('user', deserializer());
}
class SharedPreferencesService implements DataProvider {
final SharedPreferences _prefs;
@override
Future<T> getObject<T>(String key, Function serializer) async {
try {
String objectData = _prefs.getString(key);
if (objectData == null) return null;
Map<String, dynamic> objectJsonData = jsonDecode(objectData);
return serializer(objectJsonData);
} catch (err) {
return null;
}
}
}