Flutter: Arquitetura limpa com Riverpod
A arquitetura limpa fornece uma clara separação de preocupações, incentiva interfaces e abstrações e facilita mudanças nas dependências sem afetar a lógica principal do aplicativo.
Conteudo
Representação da arquitetura
Camadas de Arquitetura Limpa
Data
A camada de dados é a camada mais externa do aplicativo e é responsável pela comunicação com o lado do servidor ou um banco de dados local e a lógica de gerenciamento de dados. Ele também contém implementações de repositório.
a. Data Souce
Descreve o processo de aquisição e atualização dos dados. Consistem em fontes de dados remotas e locais. Fonte de dados remota executará solicitações HTTP na API. Ao mesmo tempo, as fontes de dados locais armazenarão em cache ou persistirão os dados.
b. Repository
A ponte entre a camada de Dados e a camada de Domínio. Implementações reais dos repositórios na camada de Domínio. Os repositórios são responsáveis por coordenar os dados das diferentes fontes de dados.
Domain
A camada de domínio é responsável por toda a lógica de negócios. Ele é escrito puramente em Dart sem elementos flutuantes porque o domínio deve se preocupar apenas com a lógica de negócios do aplicativo, não com os detalhes da implementação.
a. Providers
Descreve o processamento lógico necessário para o aplicativo. Comunica-se diretamente com os repositórios.
b. Repositories
Classes abstratas que definem a funcionalidade esperada das camadas externas.
Presentation
A camada de apresentação é a camada mais dependente da estrutura. Ele é responsável por toda a interface do usuário e pela manipulação dos eventos na interface do usuário. Ele não contém nenhuma lógica de negócios.
a. Widget (Screens/Views)
Os widgets notificam os eventos e escutam os estados emitidos pelo StateNotifierProvider
.
b. Providers
Descreve o processamento lógico necessário para a apresentação. Comunica-se diretamente com os Providers
da camada de domínio.
Descrição do Projeto
- O arquivo
main.dar
t tem código de inicialização de serviços e envolve o MyApp raiz com um ProviderScope - main/app.dart tem o MaterialApp raiz e inicializa AppRouter para lidar com a rota em todo o aplicativo e AppTheme para fornecer um tema.
services
abstraem serviços de nível de aplicativo com suas implementações.- A pasta
shared
contém o código compartilhado entre os recursos. theme
contém estilos gerais (cores, temas e estilos de texto)model
contém todos os modelos de dados necessários no aplicativo.http
é implementado com Dio.storage
é implementado com SharedPreferences.- Os padrões do localizador de serviços e o Riverpod são usados para abstrair serviços quando usados em outras camadas.
Programação Funcional
A Clean Architecture não deve ser cheia de surpresas, por isso estamos implementando a programação funcional.
A ideia central da arquitetura:
O <DataSource>
abstrato é acessado a partir da implementação do repositório. Em seguida, o abstrato <Repository>
é acessado a partir do <StateNotifier>
e a implementação de <StateNotifier>
é acessada a partir do widget e como cada uma dessas camadas obtém separação e escalabilidade, fornecendo a capacidade de alternar a implementação, fazer alterações e testar cada camada separadamente .
Por exemplo:
final storageServiceProvider = Provider((ref) { return SharedPrefsService(); }); // Uso: // ref.watch(storageServiceProvider);
- A pasta
features
: o padrão separa a lógica necessária para acessar as fontes de dados da camada de domínio. Por exemplo, o DashboardRepository abstrai e centraliza as funções necessárias para buscar oProduct
do remoto.
abstract class DashboardRepository { Future<Either<AppException, PaginatedResponse>> fetchProducts({required int skip}); Future<Either<AppException, PaginatedResponse>> searchProducts({required int skip, required String query}); }
A implementação do repositório com o DashboardDatasource
:
class DashboardRepositoryImpl extends DashboardRepository { final DashboardDatasource dashboardDatasource; DashboardRepositoryImpl(this.dashboardDatasource); @override Future<Either<AppException, PaginatedResponse>> fetchProducts( {required int skip}) { return dashboardDatasource.fetchPaginatedProducts(skip: skip); } @override Future<Either<AppException, PaginatedResponse>> searchProducts( {required int skip, required String query}) { return dashboardDatasource.searchPaginatedProducts( skip: skip, query: query); } }
Usando o Riverpod Provider
para acessar esta implementação:
final dashboardRepositoryProvider = Provider<DashboardRepository>((ref) { final datasource = ref.watch(dashboardDatasourceProvider(networkService)); return DashboardRepositoryImpl(datasource); });
E, finalmente, acessando a implementação do repositório a partir da camada de apresentação usando um Riverpod StateNotifierProvider
:
final dashboardNotifierProvider = StateNotifierProvider<DashboardNotifier, DashboardState>((ref) { final repository = ref.watch(dashboardRepositoryProvider); return DashboardNotifier(repository)..fetchProducts(); });
Observe como o NetworkService
abstrato é acessado a partir da implementação do repositório. Em seguida, o DashboardRepository
abstrato é acessado a partir do DashboardNotifier
e como cada camada obtém separação e escalabilidade, fornecendo a capacidade de alternar a implementação, fazer alterações e testar cada camada separadamente.
Teste
A pasta de test
espelha a pasta lib
, além de alguns utilitários de teste.
state_notifier_test
é usado para testar o StateNotifier
e o mock Notifier.
mocktail é usado para mock dependências.
Além disso, com arquitetura limpa, você pode substituir SharedPreferences por Hive sem afetar a lógica principal do aplicativo. Você precisa modificar a camada de acesso a dados responsável pela interação com o sistema de armazenamento de dados.
Ao trocar a implementação SharedPreferences
pela implementação Hive
, você pode alterar o sistema de armazenamento de dados sem afetar o restante do aplicativo.
Neste artigo, alteramos a implementação e os arquivos de teste para alterar o sistema de armazenamento de dados sem afetar a lógica principal do aplicativo.