Arquitetura Flutter: como escolher a melhor arquitetura para projetos

Jhoisnáyra Vitória Rodrigues de Almeida
Jhoisnáyra Vitória Rodrigues de Almeida

Compartilhe

Avalie este artigo

Ao começar seus primeiros projetos com Flutter, pode ser tentador não se preocupar tanto com organização e arquitetura de projetos, especialmente para apps pequenos.

No entanto, ao se aprofundar no desenvolvimento de aplicações na linguagem Dart com Flutter, fica claro o quanto a escolha da arquitetura Flutter adequada faz diferença para facilitar manutenção e evolução do projeto. 

Neste artigo, vamos falar sobre o que é arquitetura de software, a noção de arquitetura criada por Robert C. Martin (Uncle Bob ou Tio Bob) e como podemos interpretá-la para começar a aplicar em projetos de Flutter.

A ideia é vamos entender o que é arquitetura de software, explorar o conceito de arquitetura limpa no contexto do Flutter, discutir exemplos práticos e mostrar como organizar um projeto Flutter com boas práticas." 

O que é arquitetura de software e por que ela é importante em projetos Flutter? 

Arquitetura de software refere-se à organização fundamental de um sistema de software, incluindo seus componentes, suas interações e os princípios que orientam seu design.

Ela envolve a tomada de decisões sobre a estrutura geral do sistema, a divisão em módulos ou componentes menores, a comunicação entre eles e a definição dos padrões e princípios que guiarão o desenvolvimento e a manutenção do software. 

Desenvolvemos uma arquitetura de software com base nos requisitos do projeto, identificando o que é importante entregar para quem vai usar ou contratar a solução. A partir dessas necessidades, escolhemos tecnologias, bibliotecas e definimos a estrutura, a organização e os padrões que vão orientar o desenvolvimento. 

Existem diversas arquiteturas de software, cada uma tem uma maneira diferente de resolver problemas diferentes. O ideal é escolher uma que se aproxime mais do que vai atender as nossas necessidades.

É muito importante notar que nenhuma arquitetura vai resolver 100% com perfeição o que nós estamos precisando, às vezes precisamos adaptar algo que já existe. 

Vamos nos imaginar  na seguinte situação:
Você está iniciando um novo projeto Flutter e começa a criar suas telas, definir funcionalidades e escrever código. No início, tudo parece simples e direto. No entanto, à medida que seu aplicativo cresce, você começa a notar alguns desafios: 

  • À medida que o código cresce, a complexidade também aumenta, tornando-o difícil de entender e manter; 
  • À medida que novos recursos são adicionados, fica cada vez mais complicado integrá-los sem quebrar partes existentes do aplicativo; 
  • A adaptação a mudanças, como a substituição do banco de dados ou a integração com novas APIs, se torna uma tarefa complicada. 

Estes são problemas comuns enfrentados pelos desenvolvedores de software.

Bom, a definição de arquitetura ainda é um tanto subjetiva, visto que existem diversas visões de diferentes autores sobre o assunto, mas, de modo geral, o principal objetivo de utilizar uma arquitetura no projeto é minimizar o custo ao longo da vida útil do sistema.

Isso significa torná-lo mais fácil de entender, desenvolver, manter, expandir e implantar.  

Sabendo disso, como podemos solucionar os problemas mencionados no projeto acima? Talvez não seja possível eliminar todos esses problemas, mas adotar uma arquitetura de software adequada pode amenizar bastante seus efeitos 

Nesse exemplo, vamos utilizar a arquitetura limpa.  

Banner promocional da Alura destacando oferta especial com 40% de desconto em cursos de tecnologia. A mensagem convida a transformar a carreira na maior escola tech da América Latina, com botão “Aproveite” para acessar a promoção.

Arquitetura limpa no Flutter: como ela funciona?  

A arquitetura limpa separa responsabilidades, dividindo o software em camadas. Quais são as camadas (divisões) dessa arquitetura? Veja a seguir: 

A arquitetura limpa (pela visão de Robert C. Martin) é uma arquitetura baseada em camadas, dividindo o software em partes diferentes. Cada camada tem uma responsabilidade e/ou funcionalidade específica. 

Algumas outras características são: 

  • Separação de preocupações: a divisão clara e eficaz das diferentes funcionalidades do sistema. Isso significa que o código deve ser organizado de modo a separar as regras de negócio da lógica de apresentação, da persistência de dados e de outras partes, garantindo que cada componente tenha uma única responsabilidade bem definida. 
  • Dependência de direção única: para evitar acoplamento excessivo e tornar o código mais testável, as camadas ou componentes mais internos não devem depender diretamente das camadas mais externas, criando uma hierarquia onde a direção das dependências flui de fora para dentro. 
  • Princípio da inversão de dependência: as dependências entre os módulos do software devem ser invertidas, de modo que os módulos de alto nível não dependam dos de baixo nível, mas ambos dependem de abstrações. 
  • Testabilidade: a arquitetura promove a testabilidade por meio da separação das camadas e da redução da dependência de componentes externos. Isso facilita a criação de testes unitários e de integração, tornando o software mais robusto e confiável. 
  • Alto grau de coesão e baixo acoplamento: a arquitetura visa alcançar alto grau de coesão dentro de cada camada e baixo acoplamento entre as camadas. Isso significa que os componentes devem ter responsabilidades bem definidas e não devem depender fortemente uns dos outros. 
  • Princípio da Responsabilidade Única (SRP): as classes e módulos devem ter uma única responsabilidade. Isso ajuda a manter o código mais compreensível e facilita a manutenção. 
  • Evolução gradual: a arquitetura limpa permite a evolução gradual do software, facilitando a adição de novas funcionalidades e a realização de modificações sem comprometer a estabilidade do sistema. 
  • Padrões de design: a arquitetura limpa incentiva o uso de padrões de design, como o Model-View-Controller (MVC), Injeção de Dependência, entre outros, para resolver problemas comuns de design de software. Esses padrões fornecem soluções comprovadas para problemas recorrentes. 

Camadas da arquitetura limpa em projetos Flutter  

Existem quatro camadas na arquitetura limpa, sendo elas: 

  1. Camada de entidades: camada onde ficam as entidades (objetos de negócio) e as regras de negócio da aplicação; 
  2. Camada de casos de uso: camada que contém classes com as regras de negócio mais específicas da aplicação. Ela implementa todos os casos de uso (situações possíveis) do projeto e também é onde as entidades e suas regras são utilizadas; 
  3. Camada de adaptadores de interface: é a camada cujo objetivo é criar adaptadores de dados que estão em um formato que faz sentido para as camadas de entidade e casos de uso, para dados que façam sentido para a camada mais externa (para o banco de dados ou API, por exemplo); 
  4. Camada de Frameworks e Drivers: camada mais externa da arquitetura. Ela é formada pelas ferramentas e frameworks que utilizamos no projeto. 

Abaixo, uma releitura da imagem de representação das camadas da arquitetura limpa, descrita por Robert C. Martin: 

Imagem Colorida. Diagrama circular da arquitetura limpa com quatro camadas concêntricas: Entidades (centro), Casos de uso, Adaptadores de Interface e Frameworks e Drivers (externa) 

É importante entender como essas camadas se relacionam e quais são os limites delas. Portanto, é fundamental compreender a Regra de Dependência, a qual, segundo Robert C. Martin, deve apontar apenas para o interior.

Em outras palavras, as camadas mais internas não precisam e não devem ter conhecimento das camadas mais externas. 

Por que isso é bom para o seu projeto? Bem, se por exemplo, ao alterar o banco de dados, a API (Interface de Programação da Aplicação) ou a interface do usuário, isso não deve afetar as camadas internas do projeto, como as regras de negócio. Dessa forma, o esforço necessário para realizar essas substituições é consideravelmente reduzido. 

Exemplo prático de arquitetura Flutter: projeto Hyrule"  

Vamos usar como exemplo uma aplicação de compêndio sobre os jogos The Legend of Zelda: Breath of the Wild e The Legend of Zelda: Tears of the Kingdom, chamada Hyrule. 

GIF demonstrando o aplicativo Hyrule com navegação entre tela de categorias (criaturas, monstros, tesouros) e tela de listagem com detalhes dos itens de Zelda: Breath of the Wild e Tears of the Kingdom 

Se tiver interesse, você pode conferir o projeto completo no GitHub

Como aplicar arquitetura limpa em projetos Flutter 

Atualmente, até a data da publicação deste artigo, ainda não existe uma versão consolidada de como utilizar arquitetura limpa num projeto mobile com Dart e Flutter, mas podemos nos inspirar na arquitetura que vimos anteriormente e aplicar as regras para algo que faça sentido para nós.

A figura abaixo demonstra uma versão simplificada da arquitetura limpa, com as seguintes camadas: 

Imagem colorida. Diagrama circular de arquitetura limpa adaptada para Flutter com quatro camadas concêntricas: Domínio (centro), Dados, Controlador e Apresentação (externa), com legendas explicativas para cada camada. 

Como podemos separar o projeto Hyrule em camadas utilizando o modelo acima baseado em arquitetura limpa? 

Camada de domínio: entidades e regras de negócio na arquitetura Flutter 

Começando pela camada de Domain (Domínio), podemos criar nela as entidades e regras de negócio. Vamos precisar de uma classe que representa as entradas, Entry, e pensando no que vimos até agora, ela seria uma entidade para o nosso projeto, algo “puro”, um modelo ou representação. Veja como seria: 

DART  
class Entry { 
  int id; 
  String name; 
  String image; 
  String description; 
  String category; 
  String commonLocations; 
  Entry({ 
required this.id, 
required this.name, 
required this.image, 
required this.description, 
required this.category, 
required this.commonLocations, 
  }); 
}

E na aplicação é necessário realizar algumas ações com a entidade, como listar, salvar, excluir. Essas ações podem ser chamadas de casos de uso (Use Cases).

Então, terão casos de uso para listar as entradas, para salvar uma entrada e para excluir. Veja um exemplo: 

DART  
abstract class DaoWorkflow { 
  Future<List<Entry>> getSavedEntries(); 
  Future<void> addEntry({required Entry entry}); 
  Future<void> removeEntry({required Entry entry}); 
}

DAO é uma classe ou componente que fornece uma interface para interagir com um banco de dados ou outra fonte de dados. 

Por que usar interfaces? A interface define um "contrato" com os métodos que devem ser implementados (os casos de uso), independentemente de como essas tarefas serão executadas.

O detalhamento de implementação fica para um momento posterior, quando serão escolhidas as bibliotecas, métodos ou ferramentas mais adequadas. 

Camada de dados: persistência e acesso em Flutter architecture 

Em seguida, criamos a camada de Data (Dados), que deve conter a fonte dos dados da API ou banco de dados. Para o nosso caso, a camada de dados vai implementar as operações de banco de dados e vamos usar a biblioteca floor. Ela facilita as operações de banco de dados utilizando o sqflite, mas claro que você poderia implementar da maneira que achasse melhor. 

É importante citar que o Floor continua funcional e estável com Flutter 3.41+ e Dart 3.11+, embora seu ciclo de atualização seja mais lento comparado a alternativas como Drift ou Isar. 

Veja abaixo: 

Database

DART  
import 'dart:async'; 
import 'package:floor/floor.dart'; 
import 'package:sqflite/sqflite.dart' as sqflite; 
import 'package:hyrule/domain/models/entry.dart'; 
import 'package:hyrule/data/dao/entry_dao.dart'; 
part 'database.g.dart'; 
@Database(version: 1, entities: [Entry]) 
abstract class AppDatabase extends FloorDatabase { 
  EntryDao get entryDao; 
}

• DAO (Data Access Object): 

DART  
import 'package:floor/floor.dart'; 
import '../../domain/models/entry.dart'; 
@dao 
abstract class EntryDao { 
  @Query('SELECT * from Entry') 
  Future<List<Entry>> getAllEntries(); 
  @Insert(onConflict: OnConflictStrategy.replace) 
  Future<void> addEntry(Entry entry); 
  @delete 
  Future<void> removeEntry(Entry entry); 
}

Controlador: conectando dados e domínio na arquitetura de projetos Flutter 

Então, criamos a camada de Controller, que possui os controladores da aplicação, adaptadores de interface. No controlador podemos realmente implementar a lógica de negócio criada na camada de domínio, os nossos casos de uso que utilizam a entidade Entry

O DaoController deve implementar a classe com os contratos que criamos, logo, obrigatoriamente, ele deve ter os três métodos (listar, salvar e excluir), os quais são de fato implantados pela camada de dados. Veja: 

DART  
class DaoController implements DaoWorkflow { 
Future<EntryDao> createDatabase () async { 
final database = await $FloorAppDatabase.databaseBuilder('app_database.db').build(); 
final EntryDao entryDao = database.entryDao; 
return entryDao; 
  } 
  @override 
  Future<List<Entry>> getSavedEntries() async { 
final EntryDao entryDao = await createDatabase(); 
return entryDao.getAllEntries(); 
  } 
  @override 
  Future<void> addEntry({required Entry entry}) async { 
final EntryDao entryDao = await createDatabase(); 
entryDao.addEntry(entry); 
  } 
  @override 
  Future<void> removeEntry({required Entry entry}) async { 
final EntryDao entryDao = await createDatabase(); 
entryDao.removeEntry(entry); 
  } 
}

Camada de apresentação: UI e experiência do usuário em projetos Flutter 

Por último, a camada de Presenter (Apresentação) que contém a parte visual da aplicação, aquilo que o usuário vai ver e interagir, a UI (User Interface). Nessa camada é onde criamos as nossas telas e onde verdadeiramente utilizamos os Widgets, criamos componentes e lidamos com estados. 

Abaixo, o exemplo da tela de favoritos, onde utilizamos o Controller no FutureBuilder para listar as entradas salvas: 

DART  
 FutureBuilder( 
       future: daoController.getSavedEntries(), 
       builder: (context, snapshot) { 
         switch (snapshot.connectionState) { 
           case ConnectionState.none: 
             break; 
           case ConnectionState.active: 
             break; 
           case ConnectionState.waiting: 
             return const Center( 
               child: CircularProgressIndicator(), 
             ); 
           case ConnectionState.done: 
             if (snapshot.hasData) { 
               return ListView.builder( 
                 itemBuilder: (context, index) => 
                     EntryCard(entry: snapshot.data![index], isSaved: true), 
                 itemCount: snapshot.data!.length, 
               ); 
             } 
           default: 
         } 
         return Container(); 
       }, 
     ),

FutureBuilder é um Widget que executa funções assíncronas e atualiza a UI (User Interface) com base nessas funções.

Em nosso caso, o FutureBuilder vai executar a função assíncrona que busca as entradas salvas localmente no banco de dados e atualizar a tela com a lista de entradas quando os dados estiverem disponíveis. 

No componente EntryCard quando precisamos excluir uma entrada utilizando o WidgetDismissible, também utilizamos o Controller

DART  
     onDismissed: (direction) { 
       daoController.removeEntry(entry: entry); 
       ScaffoldMessenger.of(context) 
           .showSnackBar(const SnackBar(content: Text('Deletado'))); 
     },

Dismissible é um Widget que ao ser arrastado para determinada direção, desliza para fora da tela. Em nosso caso, ao ser arrastado do fim para o início da tela, ele também é excluído da lista de favoritos. 

E por fim, na tela de Detalhes da entrada, quando salvamos uma nova entrada: 

DART  
     floatingActionButton: FloatingActionButton( 
       onPressed: () { 
         daoController.addEntry(entry: entry); 
         ScaffoldMessenger.of(context) 
             .showSnackBar(const SnackBar(content: Text('Favoritado'))); 
       }, 
       child: const Icon(Icons.bookmark), 
     ),

Nessa camada, também podem ser usados os gerenciadores de estados da aplicação, embora nesse exemplo não esteja sendo utilizado. 

Como já foi dito, a noção de arquitetura é um pouco subjetiva e Flutter ainda não possui uma forma consolidada de arquitetura limpa.

Então, essa é apenas uma das formas que você pode pensar em aplicar uma arquitetura no seu projeto, mas você pode buscar outras maneiras de implementar uma arquitetura baseada na arquitetura limpa para a sua aplicação. 

Organização de pastas e arquivos em projetos com arquitetura Flutter  

Até aqui, ficou claro que a arquitetura está relacionada à forma de estruturar o projeto e à comunicação entre as camadas. Além disso, a organização das pastas também é essencial para facilitar o uso e a manutenção do projeto. Veja abaixo o esquema final de organização de pastas usado: 

├───lib 
│   ├───controllers 
│   │    └───api_controller.dart 
│   ├───data 
│   │   ├───api 
│   │ │  ├───database.dart 
│   │ │  ├───database.g.dart 
│   │ │  └───entry_dao.dart 
│   │   └───dao 
│   │        └───database.dart 
│   ├───domain 
│   │    ├───business 
│   │    │   ├───dao_workflow.dart 
│   │    │   └───api_workflow.dart 
│   │    └───models 
│   │             └───entry.dart 
│   ├───screens 
│   │   ├───components 
│   │   │   ├───category_card.dart 
│   │   │   └───entry_card.dart 
│   │   ├───categories.dart 
│   │   ├───details.dart 
│   │   ├───favorites.dart 
│   │   └───results.dart 
│   ├───utils 
│ │  ├───consts 
│ │  │   ├───api.dart 
│ │  │   └───categories.dart 
│ │  └───theme.dart

Como aprender mais sobre Flutter 

Dê o próximo passo na sua jornada de desenvolvimento mobile e domine a criação de aplicativos com a Carreira Desenvolvimento Mobile com Flutter da Alura!

Com um caminho de aprendizado organizado de forma lógica, do básico ao avançado, você começará criando seus primeiros widgets e corrigindo bugs simples, até evoluir para a construção de apps completos com autenticação via API, segurança e as melhores práticas do mercado.

Conectando teoria e prática a cada novo desafio, nossa formação prepara você exatamente para o que as empresas de tecnologia exigem atualmente.  

Por que adotar arquitetura Flutter melhora seus projetos 

Parabéns por concluir a leitura! Agora você já sabe o que é arquitetura de software, entende os princípios da arquitetura limpa, suas camadas aplicadas ao Flutter e conhece as vantagens de adotar essa abordagem. 

O conteúdo de arquitetura limpa é um assunto bastante complexo e ainda existem muitos detalhes que você pode estudar para se aprofundar ainda mais. Para te ajudar, deixo aqui um Podcast do Hipsters. Tech sobre Arquitetura de sistemas, arquitetura limpa e Hexagonal, e muito mais. 

Espero que tenha aproveitado a leitura e continue avançando em seus estudos! 

Avalie este artigo

Jhoisnáyra Vitória Rodrigues de Almeida
Jhoisnáyra Vitória Rodrigues de Almeida

Scuba Team Mobile (com foco em Flutter). Estudante de Ciência da Computação na UFPI, pesquisadora da área de Internet das Coisas e formada em Eletrônica pelo IFPI.

Veja outros artigos sobre Mobile