Arquitetura Limpa: Aplicando com Flutter
Tags: braziliandevs, flutter, cleanarchitecture, dart
Published: 2022-07-01 21:21:07

Antes de iniciar a leitura desse texto, é recomendável ter noções de alguns conceitos básicos como [Arquitetura Limpa](https://dev.to/rubemfsv/arquitetura-limpa-o-conceito-por-tras-do-codigo-4kdf) e [SOLID](https://dev.to/rubemfsv/solid-o-conceito-por-tras-do-codigo-5fd1), pois eles facilitam o entendimento do que será apresentado.

Este texto tem dois propósitos: I. Mostrar uma divisão arquitetural de uma aplicação Flutter usando Arquitetura Limpa; II. Guiar a implementação de novas features nesta arquitetura proposta.

O código analisado é baseado na abordagem de Arquitetura Limpa proposta por [Rodrigo Manguinho](https://www.linkedin.com/in/rmanguinho/) em seu [curso de Flutter](https://rmanguinho.github.io). Sua abordagem segue alinhada com a proposta original de [Robert Martin](http://cleancoder.com/products).

## Divisão Arquitetural

O primeiro passo é analisar como é feita a divisão.

```
android/
ios/
lib/
  data/
    cache/
    http/
    models/
    usecases/
  domain/
    entities/
    helpers/
    usecases/
  infra/
    cache/
    http/
  main/
    builders/
    composites/
    decorators/
    factories/
      cache/
      http/
      pages/
      usecases/
    main.dart
  presentation/
    mixins/
    presenters/
    protocols/
  ui/
    assets/
    components/
    helpers/
    mixins/
    pages/
  validation/
    protocols/
    validators/
requirements/
    bdd_specs/
    checklists/
    usecases/
test/
    data/
    domain/
    infra/
    main/
    mocks/
    presentation/
    ui/
    validation/
```
E a partir daí, podemos fazer associações com a [teoria da Arquitetura Limpa](https://dev.to/rubemfsv/arquitetura-limpa-o-conceito-por-tras-do-codigo-4kdf) para uma compreensão mais fácil da [divisão de responsabilidades](https://dev.to/rubemfsv/arquitetura-limpa-camadas-e-limites-22p9).

A seguir, vamos ver em detalhes o propósito de cada estrutura de arquivos.

- **Android**: Contém os arquivos necessários para buildar a aplicação em sistemas android.
- **iOS**: Contém os arquivos necessários para buildar a aplicação em sistemas iOS.
- **Lib**: Contém todos os arquivos necessários para a aplicação.
 - **Data**: A pasta data representa a camada de dados da Arquitetura Limpa, sendo dependente da camada de domínio. Contém as implementações das regras de negócio que são declaradas no _domain_.
 - **Domain**: Representa a camada de domínio da Arquitetura Limpa, a camada mais interna da aplicação, não apresentando dependência com nenhuma outra camada, onde contém as regras de negócio.
 - **Infra**: Essa pasta contém as implementações referentes ao protocolo HTTP e ao cache, também é único local onde terá acesso a dependências externas relacionadas para esses dois itens citados.
 - **Main**: Corresponde a camada principal da aplicação, ponto que ocorre a integração das interfaces desenvolvidas na camada de UI, com as regras de negócio criadas nas pastas que representam as camadas mais internas da Arquitetura Limpa. Tudo isso se dá devido ao uso de padrões de projetos como Factory Method, Composite e Builder.
 - **Presentation**: Nessa camada é onde os dados são preparados para serem consumidos na UI, tratando as lógicas por trás das telas.
 - **UI**: Contém os componentes e interfaces visuais que são vistas no sistema, é aqui que propriamente são criadas as telas.
 - **Validation**: Onde contém as implementações das validações utilizadas nos campos (ex: quantidade mínima de carácteres, campo obrigatório, email válido, dentre outros).
- **Requirements**: Contém os requisitos do sistema documentados, essa pasta pode ou não ter todas as subpastas a seguir, depende muito de como o time trabalha.
 - **Bdd_specs**: Contém os arquivos escritos em linguagem Gherkin para descrever o comportamento esperado do sistema.
 - **Checklist**: Contém a descrição do comportamento das páginas, com o intuito de facilitar durante os testes unitários, para saber o que validar e o que esperar.
 - **Usecases**: Contém o comportamento esperado dos casos de uso do sistema, onde descreve as variações das regras de negócio para facilitar os testes unitários e implementação.
- **Test**: Contém todos os testes unitários da aplicação, cada pasta interna representa a camada que os testes pertencem, e a pasta de _mocks_ contém os mocks utilizados nos testes.

## Guia de Implementação

Após compreender a razão da divisão e quais responsabilidades estão contidas em cada pasta da estrutura, será descrita uma sequência lógica recomendada para um melhor desempenho de implementação utilizando esta arquitetura. 

Para finalidade de simplificar a explicação, não será descrito em detalhes os testes unitários. No entanto, é fortemente recomendado começar pelos testes unitários antes do desenvolvimento (TDD) de cada passo utilizando os _requirements_ para embasar os cenários.

A demonstração a seguir é da criação do fluxo de Login para entrar em uma aplicação.

### Primeiro passo: Criar as regras de negócio na camada de domínio

Dentro de lib/domain/usecases, criar o authentication.dart. Esse arquivo vai ser uma classe abstrata que vai descrever a regra de negócio da autenticação.

```
import '../entities/entities.dart';

abstract class Authentication {
  Future<AccountEntity> auth(AuthenticationParams params);
}

class AuthenticationParams {
  final String email;
  final String password;

  AuthenticationParams({required this.email, required this.password});
}
```
Como vemos, é uma classe abstrata que tem um método `auth()` que recebe os parâmetros _AuthenticationParams_ que são declarados abaixo (email e password), e espera retornar um _AccountEntity_ de maneira assíncrona através do _Future_. 

O _AccountEntity_ é uma classe criada em lib/domain/entities que representa o token que é retornado após a autenticação para persistir a sessão.

```
class AccountEntity {
  final String token;

  AccountEntity({required this.token});
}
```

### Segundo passo: Implementar as regras na camada de dados

Nessa camada, criamos o caso de uso para implementar a regra criada anteriormente na camada de domínio, porém dentro de lib/data/usecases.

O arquivo costuma ficar como o exemplo abaixo.

```
import '../../../domain/entities/entities.dart';
import '../../../domain/helpers/helpers.dart';
import '../../../domain/usecases/usecases.dart';

import '../../http/http.dart';
import '../../models/models.dart';

class RemoteAuthentication implements Authentication {
  final HttpClient httpClient;
  final String url;

  RemoteAuthentication({required this.httpClient, required this.url});

  Future<AccountEntity> auth(AuthenticationParams params) async {
    final body = RemoteAuthenticationParams.fromDomain(params).toJson();
    try {
      final hpptResponse =
          await httpClient.request(url: url, method: 'post', body: body);

      return RemoteAccountModel.fromJson(hpptResponse).toEntity();
    } on HttpError catch (error) {
      throw error == HttpError.unauthorized
          ? DomainError.invalidCredentials
          : DomainError.unexpected;
    }
  }
}

class RemoteAuthenticationParams {
  final String email;
  final String password;

  RemoteAuthenticationParams({required this.email, required this.password});

  factory RemoteAuthenticationParams.fromDomain(AuthenticationParams params) =>
      RemoteAuthenticationParams(
          email: params.email, password: params.password);

  Map toJson() => {'email': email, 'password': password};
}
```

Como podemos observar, a classe _RemoteAuthentication_ implementa a classe abstrata _Authentication_, recebendo o cliente HTTP e a url para a requisição. No método `auth()` ele recebe os parâmetros, e chama a factory `RemoteAuthenticationParams.fromDomain(params)` criada abaixo com o propósito de converter o que vem no formato padrão para o formato json para ser enviado na requisição HTTP dentro do body. Após isso, é feita a requisição e armazenado o valor retornado em _httpResponse_, e essa _httpResponse_ é retornada no método dentro de um model com o intuito de converter o resultado para o formato padrão de se trabalhar (entity) através de `RemoteAccountModel.fromJson(hpptResponse).toEntity()`.

Essa factory e model são criadas nessa camada com o propósito de não poluir a camada de domínio, pois o que acontece na camada de dados não deve influenciar o que acontece na de domínio.

Para fim de curiosidade, a implementação do _RemoteAccountModel_ está abaixo. Ele recebe um accessToken no formato de json e o converte para um Entity.

```
import '../../domain/entities/entities.dart';
import '../http/http.dart';

class RemoteAccountModel {
  final String accessToken;

  RemoteAccountModel(this.accessToken);

  factory RemoteAccountModel.fromJson(Map json) {
    if (!json.containsKey('accessToken')) {
      throw HttpError.invalidData;
    }
    return RemoteAccountModel(json['accessToken']);
  }

  AccountEntity toEntity() => AccountEntity(token: accessToken);
}
```

### Terceiro passo: Implementar as telas na camada de UI

Para simplificar o entendimento, será apresentado apenas trechos de códigos referentes a chamada do método de autenticação. A tela de Login contém mais ações e detalhes que vão além da autenticação. Levar em consideração o protótipo da tela abaixo para facilitar a visualização.


![Login screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/47uxjby862reezok9mk1.png)

Em lib/ui/pages/ será necessário ao menos dois arquivos: I. `login_page.dart`, que será a página de Login; II. `login_presenter.dart` que conterá a classe abstrata com os métodos e streams que são utilizados na página, e a implementação dessa classe abstrata ocorre na camada de presentation.

O arquivo `login_presenter.dart` é similar ao exemplo abaixo.

```
import 'package:flutter/material.dart';

abstract class LoginPresenter implements Listenable {
  void validateEmail(String email);
  void validatePassword(String password);

  Future<void> auth();
}
```

E o código abaixo é do botão de Login.

```
class LoginButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final presenter = Provider.of<LoginPresenter>(context);

    return StreamBuilder<bool>(
      stream: presenter.isFormValidStream,
      builder: (context, snapshot) {
        return ElevatedButton(
          onPressed: snapshot.data == true ? presenter.auth : null,
          child: Text(R.translations.enterButtonText.toUpperCase()),
        );
      },
    );
  }
}
```

Onde o `auth()` clicado no `onPressed` chama o presenter para fazer a autenticação. Nesse caso não está passando parâmetros no `auth()` por os parâmetros serem pegos no presenter durante a interação com a tela (são usados os `validateEmail()` e `validatePassword()` declarados acima no presenter da página). Veremos mais detalhes no próximo passo.

### Quarto passo: Implementar a classe abstrata do presenter da UI na camada de presentation

Para facilitar o trabalho com Streams, é recomendado o uso da biblioteca [GetX](https://pub.dev/packages/get) (ou alguma outra da sua preferência) para deixar menos verboso. A escolha pela _GetX_ é devido ao seu grande suporte e estar em constante atualização.

Em lib/presentation/presenters é criado o `getx_login_presenter.dart`. É uma classe _GetxLoginPresenter_ que extende o _GetxController_ e implementa o _LoginPresenter_. Apesar do exemplo abaixo ter _Validation_ e _SaveCurrentAccount_, focaremos no _Authentication_.

```
import 'dart:async';
import 'package:get/get.dart';

import '../../ui/pages/login/login_presenter.dart';
import '../../domain/helpers/domain_error.dart';
import '../../domain/usecases/usecases.dart';
import '../protocols/protocols.dart';

class GetxLoginPresenter extends GetxController
  implements LoginPresenter {
  final Validation validation;
  final Authentication authentication;
  final SaveCurrentAccount saveCurrentAccount;

  String? _email;
  String? _password;

  GetxLoginPresenter({
    required this.validation,
    required this.authentication,
    required this.saveCurrentAccount,
  });

  void validateEmail(String email) {
    _email = email;
    // Validation code here
  }

  void validatePassword(String password) {
    _password = password;
    // Validation code here
  }

  Future<void> auth() async {
    try {
      final account = await authentication.auth(AuthenticationParams(
        email: _email!,
        password: _password!,
      ));
      await saveCurrentAccount.save(account);
    } on DomainError catch (error) {
      // Handle errors here
    }
  }
}
```

Nos métodos `validateEmail(String email)` e `validatePassword(String password)` são capturados o email e senha do usuário ao digitar nos _Inputs_ da tela de Login. Em `auth()`, é onde há a chamada ao método de autenticação implementados anteriormente, que recebem o email e senha capturados pelos validates anteriores. 

A chamada `authentication.auth(AuthenticationParams(email: _email!, password: _password!))` retorna um token (como explicado anteriormente), é atribuído a uma variável chamada _account_ e em seguida salva em cache através do `saveCurrentAccount.save(account)` (não foi explicado sobre esse ponto nesse texto, porém é através dele que há a persistência da sessão do usuário no aparelho).

### Quinto passo: Conectar todas as camadas para que as requisições funcionem

Após tudo implementado, agora basta conectar todas as partes. Para isso, é utilizado o padrão de projeto [Factory Method](https://refactoring.guru/pt-br/design-patterns/factory-method).

Dentro de lib/main/factories/usecases, criamos a _factory_ do caso de uso que está sendo implementado. No caso desse exemplo, é o relacionado a autenticação. 

É criado o _authentication_factory.dart_, que retorna o _RemoteAuthentication_ que recebe como parâmetro a _factory_ do _Http Client_ e a _factory_ que cria a URL. É passado como parâmetro a URL da API que deseja requisitar junto da _factory_ que cria a URL. No exemplo é a URL que finaliza com _/login_.

```
import '../../../domain/usecases/usecases.dart';
import '../../../data/usecases/usecases.dart';
import '../factories.dart';

Authentication makeRemoteAuthentication() {
  return RemoteAuthentication(
    httpClient: makeHttpAdapter(),
    url: makeApiUrl('login'),
  );
}
```

Após isso, em lib/main/factories/pages, é criada a pasta para as factories do Login. Para essa explicação, focaremos no _login_page_factory.dart_ e _login_presenter_factory.dart_.

Primeiro, é feito a _login_presenter_factory.dart_, que é um Widget que retorna o _GetxLoginPresenter_. Esse _presenter_ foi criado anteriormente, e dentro dele é injetando as _factories_ de autenticação (que foi criado logo a cima) e as de validação e salvar token no cache (que não foram abordadas nesse texto, porém seguem as mesmas premissas da _factory_ de autenticação).

```
import '../../factories.dart';
import '../../../../presentation/presenters/presenters.dart';
import '../../../../ui/pages/pages.dart';

LoginPresenter makeGetxLoginPresenter() {
  return GetxLoginPresenter(
    authentication: makeRemoteAuthentication(),
    validation: makeLoginValidation(),
    saveCurrentAccount: makeLocalSaveCurrentAccount(),
  );
}
```

Em seguida, seguindo a mesma linha de pensamento, é feita a _factory_ da página de Login. Como a _factory_ do _presenter_, é um Widget, mas nesse caso retorna a _LoginPage_ com a _factory_ do _presenter_  criado anteriormente sendo injetado como parâmetro.

```
import 'package:flutter/material.dart';
import '../../../../ui/pages/pages.dart';
import '../../factories.dart';

Widget makeLoginPage() {
  return LoginPage(makeGetxLoginPresenter());
}
```

### Sexto passo: Aplicar a tela criada na aplicação

Por fim, é necessário chamar a _factory_ do Login na aplicação para que ela consiga ser acessada pelo usuário. 

No arquivo _main.dart_ que fica localizado em lib/main, adicionar dentro do array de páginas (_getPages_) a _factory_ página criada. É passado no _name_ a rota, no caso é a _/login_, e a página, que no caso é o ponteiro para a _factory_ `makeLoginPage`. Essa lógica é utilizada com todas as outras páginas. O código fica como está abaixo.

```
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';

import '../ui/components/components.dart';
import 'factories/factories.dart';

void main() {
  runApp(App());
}

class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.light);
    final routeObserver = Get.put<RouteObserver>(RouteObserver<PageRoute>());

    return GetMaterialApp(
      title: 'Flutter Clean App',
      debugShowCheckedModeBanner: false,
      theme: makeAppTheme(),
      navigatorObservers: [routeObserver],
      initialRoute: '/',
      getPages: [
        GetPage(
            name: '/login', page: makeLoginPage, transition: Transition.fadeIn),
      ],
    );
  }
}
```

## Conclusão

Apesar de ser um pouco complexo de se entender no começo e parecer um pouco redundante, as abstrações são necessárias. São aplicados diversos padrões de projetos para garantir a qualidade e independência do código, facilitando a evolução e manutenção.

Seguir o processo de desenvolvimento e entender o porquê está fazendo de tal maneira facilita a produção do código. Após um tempo acaba sendo feito de maneira natural, pois tem um processo linear de desenvolvimento: I. Caso de uso na camada de domínio; II. Caso de uso na camada de dados; III. Criação da UI; IV. Criação das lógicas para chamada da requisição na camada de presentation; V. Criação das _factories_ para integrar todas as camadas na camada principal; VI. E a chamada da _factory_ principal nas rotas da aplicação para que seja disponível para o usuário. 

Apesar de ter muitas partes abstraídas, é recomendável a leitura do código das partes ocultas para uma maior compreensão. [Nesse repositório](https://github.com/rmanguinho/clean-flutter-app) do curso do Rodrigo Manguinho você consegue ter acesso a esses códigos abstraídos. 

## Referências

* Rodrigo Manguinho https://github.com/rmanguinho/clean-flutter-app
* MARTIN, Robert C. Clean Architecture: A Craftsman’s Guide to Software Structure and Design. 1st. ed. USA: Prentice Hall Press, 2017. ISBN 0134494164.