Skip to content

Instantly share code, notes, and snippets.

@tuliofmoura
Last active July 25, 2020 12:20
Show Gist options
  • Save tuliofmoura/1900d4fab464aa842fefd3dfc24397cb to your computer and use it in GitHub Desktop.
Save tuliofmoura/1900d4fab464aa842fefd3dfc24397cb to your computer and use it in GitHub Desktop.

Clean architecture aplicada a um aplicativo Flutter

Camadas

project_layers

Domain

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 é:

Entity

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] : '';
}

Repository

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();
}

Service

É 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;
  }
}

App

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.


Data

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 é:

RepositoryImp

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();
    }
  }
}

Data Transfer Object (DTO)

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,
      );
}

Infrastructure

Esta camada agrupa os elementos mais baixo nível da aplicação, como acesso à base de dados, à arquivos e requisições HTTP.

Estrutura de diretórios

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

Exemplo

Camada app

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);
	}
}

Camada domain

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;
	}

Camada data

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());
	}

Camada infrastructure

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;
    }
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment