Flutter: Arquitetura limpa com Riverpod

Tempo de leitura: 3 minutes

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.

 

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.dart 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 o Product 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.