Clean Architecture em Flutter

Texto para quem tem dúvidas básicas, com exemplos e perguntas comuns. (Esse texto teve ajuda do GPT para ser realizado)

Clean Architecture é um conjunto de princípios e padrões de projeto de software que visa separar as responsabilidades de uma aplicação em camadas de forma a maximizar a coesão e minimizar a acoplamento. A ideia principal por trás da Clean Architecture é criar uma estrutura de projeto flexível, escalável e fácil de manter, tornando a aplicação mais fácil de adaptar e evoluir com o tempo.

A Clean Architecture é composta por três camadas principais: a camada de entrada, a camada de domínio e a camada de saída. Cada camada tem uma responsabilidade específica e uma relação de dependência entre elas:

  • A camada de entrada é a camada mais externa e é responsável por gerenciar as interações do usuário com a aplicação. Ela pode ser implementada como uma interface de linha de comando, uma API REST, uma interface gráfica de usuário, etc. A camada de entrada não deve conter lógica de negócio e não deve depender de nenhuma outra camada.
  • A camada de domínio é a camada central e é responsável por implementar a lógica de negócio da aplicação. Ela é composta por entidades, regras de negócio e serviços que representam o core da aplicação. A camada de domínio deve ser independente de qualquer implementação de entrada ou saída e deve ser facilmente testável.
  • A camada de saída é a camada mais interna e é responsável por fazer a interface da aplicação com o mundo exterior. Ela pode ser implementada como um banco de dados, um serviço externo, uma interface de usuário, etc. A camada de saída deve depender da camada de domínio, mas não deve depender da camada de entrada.

Exemplo

Imagine que você está criando uma aplicação de gerenciamento de tarefas. A aplicação deve permitir que o usuário adicione, remova e liste tarefas. A seguir, um exemplo de como a Clean Architecture poderia ser aplicada nesse caso:

  • Na camada de entrada, você poderia criar uma interface de linha de comando que permita que o usuário execute comandos para adicionar, remover e listar tarefas. Essa interface não deve conter lógica de negócio.
  • Na camada de domínio, você poderia criar uma classe Task que represente uma tarefa e uma classe TaskService que implemente as regras de negócio para adicionar, remover e listar tarefas. A classe TaskService não deve depender de nenhuma implementação específica de entrada ou saída e deve ser facilmente testável.
  • Na camada de saída, você poderia criar uma classe TaskRepository que se conecte a um banco de dados para armazenar e recuperar tarefas. A classe TaskRepository deve depender da classe Task e da classe TaskService da camada de domínio, mas não deve depender da camada de entrada.
  • Com essa estrutura, a camada de entrada pode ser facilmente alterada para usar uma interface de usuário gráfica ou uma API REST sem afetar a camada de domínio ou a camada de saída. Além disso, a camada de saída pode ser alterada para usar um serviço externo ou um arquivo de texto como repositório sem afetar a camada de domínio ou a camada de entrada. Isso torna a aplicação mais flexível e fácil de manter ao longo do tempo.
  • Espero que essa explicação tenha sido útil e que você tenha uma melhor compreensão do conceito de Clean Architecture e de como ele pode ser aplicado em um projeto de software.

O Clean substitui um MVVM ou MVP da vida?

Clean Architecture é um conjunto de princípios e padrões de projeto de software que se aplica a qualquer arquitetura de software. Ele não é uma arquitetura em si mesmo, mas sim uma forma de organizar o código de uma aplicação de forma a maximizar a coesão e minimizar o acoplamento.

Isso significa que a Clean Architecture pode ser usada em conjunto com qualquer outra arquitetura, como MVVM (Model-View-ViewModel), MVP (Model-View-Presenter), MVC (Model-View-Controller), etc. Na verdade, a Clean Architecture é uma forma de aplicar os princípios do SOLID (Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) a qualquer arquitetura de software, tornando-a mais flexível, escalável e fácil de manter.

Por exemplo, você poderia aplicar a Clean Architecture a uma aplicação MVVM da seguinte forma:

  • Na camada de entrada, você poderia colocar a camada de visão (View) do MVVM, responsável por exibir a interface de usuário e capturar as interações do usuário.
  • Na camada de domínio, você poderia colocar a camada de modelo (Model) do MVVM, responsável por implementar a lógica de negócio da aplicação e por representar os dados da aplicação.
  • Na camada de saída, você poderia colocar a camada de visualização do modelo (ViewModel) do MVVM, responsável por fazer a interface da aplicação com o mundo exterior e por fornecer os dados para a camada de visão.

Dessa forma, você pode aproveitar os benefícios da arquitetura MVVM, como a separação clara das responsabilidades entre a camada de visão e a camada de visualização do modelo, enquanto também aplica os princípios da Clean Architecture para maximizar a coesão e minimizar o acoplamento entre as camadas da aplicação.

E como fica uma estrutura de pastas nesse modelo do Clean com o MVVM?

A estrutura de pastas de uma aplicação Flutter que foi feita usando MVVM e respeitando o Clean Architecture poderia ser algo como o seguinte:

├── lib
│   ├── data
│   │   ├── models
│   │   ├── repositories
│   ├── domain
│   │   ├── entities
│   │   ├── services
│   ├── presentation
│   │   ├── views
│   │   ├── view_models
│   ├── utils
│   ├── helpers
│   ├── layers
│   ├── main.dart
├── test
  • A pasta lib é a pasta principal da aplicação e contém os arquivos de código-fonte da aplicação.
  • A pasta data contém os arquivos relacionados ao acesso a dados da aplicação, como modelos de dados e repositórios.
  • A pasta domain contém os arquivos relacionados ao domínio da aplicação, como entidades e serviços de negócio.
  • A pasta presentation contém os arquivos relacionados à apresentação da aplicação, como visões e visualizações do modelo.
  • A pasta test contém os arquivos de teste da aplicação.

Cada pasta da estrutura acima representa uma camada da arquitetura da aplicação. A camada de entrada, responsável por gerenciar as interações do usuário com a aplicação, ficaria na pasta presentation/views. A camada de domínio, responsável por implementar a lógica de negócio da aplicação, ficaria nas pastas domain/entities e domain/services. E a camada de saída, responsável por fazer a interface da aplicação com o mundo exterior, ficaria nas pastas data/models e data/repositories.

Mas todos meus arquivos ficam agrupados nessas pastas? todos models na model por exemplo?

Sim, é comum colocar todos os modelos da aplicação em uma única pasta models dentro da pasta data, assim como colocar todas as visões (views) em uma única pasta views dentro da pasta presentation e todas as visualizações do modelo (view models) em uma única pasta view_models também dentro da pasta presentation. Dessa forma, fica mais fácil de encontrar os arquivos de cada camada da aplicação e de manter a organização do código.

No entanto, essa é apenas uma sugestão de estrutura de pastas e você pode adaptá-la de acordo com as necessidades da sua aplicação. Por exemplo, se a sua aplicação tiver muitos modelos e você quiser dividi-los em categorias, pode criar sub-pastas dentro da pasta models para cada categoria de modelos. Da mesma forma, você pode criar sub-pastas dentro das pastas views e view_models para organizar os arquivos de acordo com suas responsabilidades ou funcionalidades.

O importante é encontrar uma estrutura de pastas que faça sentido para a sua aplicação e que ajude a manter a organização do código ao longo do tempo.

A pasta repositories é um bom lugar para colocar arquivos que implementam a lógica de acesso a dados da aplicação, como uma classe de repositório que se conecta a um banco de dados para armazenar e recuperar entidades.

A pasta entities é um bom lugar para colocar arquivos que representam os objetos de domínio da aplicação, como uma classe de entidade que representa uma tarefa ou um usuário.

A pasta services é um bom lugar para colocar arquivos que implementam as regras de negócio da aplicação, como uma classe de serviço que valida uma tarefa antes de adicioná-la ao repositório ou que calcula o valor total de uma compra.

A seguir, um exemplo de como essas pastas podem ser usadas em uma aplicação de gerenciamento de tarefas:

lib
├── data
│   ├── models
│   │   └── task.dart
│   └── repositories
│       └── task_repository.dart
├── domain
│   ├── entities
│   │   └── task.dart
│   └── services
│       └── task_service.dart
└── presentation
    ├── views
    └── view_models

Nesse exemplo, a classe Task da pasta data/models representa o modelo de dados de uma tarefa, enquanto a classe Task da pasta domain/entities representa a entidade de domínio de uma tarefa. A classe TaskRepository da pasta data/repositories implementa a lógica de acesso a dados para armazenar e recuperar tarefas, enquanto a classe TaskService da pasta domain/services implementa as regras de negócio para adicionar, remover e listar tarefas.

Espero que essa explicação tenha sido útil e que você tenha uma melhor compreensão do que colocar nas pastas repositories, entities e services.

É comum haver uma certa confusão em relação à diferença entre um modelo e uma entidade, pois ambos podem ser usados para representar objetos de domínio da aplicação. No entanto, a principal diferença é que um modelo é geralmente usado para representar o estado de um objeto, enquanto uma entidade é usada para representar o objeto em si, incluindo suas operações e comportamentos.

Para ilustrar essa diferença, aqui estão dois exemplos simples de arquivos, um para um modelo e outro para uma entidade:

Modelo:

class TaskModel {
  final int id;
  final String title;
  final String description;
  final bool completed;

  TaskModel({this.id, this.title, this.description, this.completed});
}

Entidade:

class Task {
  int id;
  String title;
  String description;
  bool completed;

  Task({this.id, this.title, this.description, this.completed});

  void markAsCompleted() {
    completed = true;
  }

  void markAsIncomplete() {
    completed = false;
  }
}

No exemplo acima, o modelo TaskModel é uma classe que representa o estado de uma tarefa, com quatro propriedades: id, title, description e completed. Já a entidade Task é uma classe que representa a tarefa em si, incluindo as operações markAsCompleted e markAsIncomplete que alteram o valor da propriedade completed.

Aqui está um exemplo simples de um arquivo de repositório que implementa uma classe de repositório de tarefas:

import 'package:myapp/data/models/task_model.dart';
import 'package:myapp/domain/entities/task.dart';

class TaskRepository {
  Future<List<Task>> getAll() async {
    // Faz uma chamada à API para recuperar a lista de tarefas
    final response = await http.get('http://myapi.com/tasks');
    // Converte a resposta em uma lista de modelos de tarefas
    final tasks = (json.decode(response.body) as List)
        .map((item) => TaskModel.fromJson(item))
        .toList();
    // Converte a lista de modelos em uma lista de entidades
    return tasks.map((task) => Task.fromModel(task)).toList();
  }

  Future<Task> getById(int id) async {
    // Faz uma chamada à API para recuperar a tarefa com o ID especificado
    final response = await http.get('http://myapi.com/tasks/$id');
    // Converte a resposta em um modelo de tarefa
    final task = TaskModel.fromJson(json.decode(response.body));
    // Converte o modelo em uma entidade
    return Task.fromModel(task);
  }

  Future<Task> add(Task task) async {
    // Converte a entidade em um modelo
    final taskModel = TaskModel.fromEntity(task);
    // Faz uma chamada à API para adicionar a tarefa
    final response = await http.post('http://myapi.com/tasks',
        body: json.encode(taskModel.toJson()));
    // Converte a resposta em um modelo de tarefa
    final addedTask = TaskModel.fromJson(json.decode(response.body));
    // Converte o modelo em uma entidade
    return Task.fromModel(addedTask);
  }

Future<Task> update(Task task) async {
  // Converte a entidade em um modelo
  final taskModel = TaskModel.fromEntity(task);
  // Faz uma chamada à API para atualizar a tarefa
  final response = await http.put('http://myapi.com/tasks/${task.id}',
      body: json.encode(taskModel.toJson()));
  // Converte a resposta em um modelo de tarefa
  final updatedTask = TaskModel.fromJson(json.decode(response.body));
  // Converte o modelo em uma entidade
  return Task.fromModel(updatedTask);
}

Future<void> delete(int id) async {
  // Faz uma chamada à API para remover a tarefa
  await http.delete('http://myapi.com/tasks/$id');
}

Nesse exemplo, a função update faz uso de uma chamada HTTP PUT para atualizar a tarefa no servidor, enquanto a função delete faz uso de uma chamada HTTP DELETE para remover a tarefa.

Espero que essas informações tenham sido úteis e que você tenha uma melhor compreensão do que é um arquivo de repositório e como ele pode ser usado em uma aplicação.

Aqui está um exemplo simples de um arquivo de serviço que implementa uma classe de serviço de gerenciamento de tarefas:

import 'package:myapp/domain/entities/task.dart';
import 'package:myapp/data/repositories/task_repository.dart';

class TaskService {
  final TaskRepository _repository;

  TaskService(this._repository);

  Future<List<Task>> getAll() async {
    return await _repository.getAll();
  }

  Future<Task> getById(int id) async {
    return await _repository.getById(id);
  }

  Future<Task> add(Task task) async {
    if (task.title.isEmpty || task.description.isEmpty) {
      throw Exception('Title and description are required');
    }
    return await _repository.add(task);
  }

  Future<Task> update(Task task) async {
    if (task.id == null) {
      throw Exception('Task id is required');
    }
    return await _repository.update(task);
  }

  Future<void> delete(int id) async {
    await _repository.delete(id);
  }
}

Nesse exemplo, a classe TaskService implementa as operações básicas de gerenciamento de tarefas, como listar todas as tarefas, adicionar uma tarefa, atualizar uma tarefa e remover uma tarefa. Cada operação é implementada como uma função assíncrona que faz uso de um repositório de tarefas (TaskRepository) para acessar os dados das tarefas.

Espero que esse exemplo tenha sido útil e que você tenha uma melhor compreensão do que é um arquivo de serviço e como ele pode ser usado em uma aplicação. Qualquer outra dúvida, não hesite em perguntar.

Related Posts

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *