Injeção de dependência em Flutter usando Riverpod
Neste post falarei sobre o uso do pacote RiverPod como framework de injeção de dependência no Flutter.
Gerenciar dependências é importante ao escrever um projeto escalonável. Um componente que depende de outra dependência deve ser capaz de obtê-las sem precisar saber como criá-las. Uma abordagem para conseguir isso é através da injeção de dependência.
Conteudo
O que é injeção de dependência?
A injeção de dependência é um conceito para gerenciar dependências e separar as preocupações dos diferentes blocos do seu código.
Digamos que eu precisaria de um professor
com quem aprender quando assistisse a uma palestra. Eu não me importo com quem é o professor
designado nesta aula em particular. Só preciso de alguém para ministrar a palestra que estou participando. A injeção de dependência seria a escola fornecer um professor com quem eu pudesse aprender sem que eu escolhesse quem seria essa pessoa.
Por que precisamos usar injeção de dependência?
A principal razão por trás do uso de uma estrutura de injeção de dependência é tornar nossa experiência de codificação mais suave.
O componente que estamos construindo deve ser capaz de localizar e usar outro componente facilmente. Nosso componente também deve ser testável de forma que possa obter facilmente um stub ou uma versão falsa de sua dependência quando executarmos testes.
// Exemplo Ruim class Leitura { const Leitura(); final Professor _professor = const Professor('Pedro'); void participante(Studente studente) { studente.entradaSalaClasse(); studente.situacao(); studente.listenTo(_professor); } }
Para ser consistente com nosso exemplo anterior, observe o código de exemplo acima. O método participante()
primeiro fará com que o aluno entre na sala de aula, sente-se em uma cadeira e ouça o _professor
.
Isso funcionará, mas e se quisermos testar este componente Leitura? Como o professor foi definido internamente e não foi injetado, não podemos zombar ou eliminar essa dependência. É sempre definido com o mesmo valor; Professor Pedro
.
// Bom Exemplo class Leitura { const Leitura({ required Professor professor, }): _professor = professor; final Professor _professor; void participante(Studente studente) { studente.entradaSalaClasse(); studente.situacao(); studente.listenTo(_professor); } }
Um exemplo melhor seria usar o construtor
da classe como meio de injetar a dependência. À medida que criamos uma instância de uma Leitura
, podemos facilmente passar um professor falso como argumento de parâmetro ao executar testes.
Mas o que acontece então quando passamos a ter mais de uma dependência? Usar a injeção de construtor é entediante à medida que seu aplicativo cresce. Este é o problema que queremos resolver usando uma estrutura de injeção de dependência.
O que é Riverpod?
Riverpod é uma estrutura reativa de cache e vinculação de dados construída por Remi Rousselet. É uma solução popular desenvolvida para fazer gerenciamento de estado, injeção de dependência e armazenamento em cache de dados que a estrutura Flutter não fornece nativamente.
Embora seja usado principalmente para gerenciamento de estado, o Riverpod pode ser usado apenas para injeção de dependência se o seu projeto estiver usando outra biblioteca para gerenciar estados, como BLoC
.
Riverpod é melhor usado como um pacote completo, pois pode realizar tudo, desde gerenciamento de estado até cache de dados e não apenas injeção de dependência.
Cada dependência que queremos injetar é envolvida por um objeto chamado Provider
. No Riverpod, um Provider
é um objeto imutável usado para obter uma dependência. Ele agrupa a dependência necessária para que possa ser acessada em qualquer lugar do seu aplicativo que esteja dentro de um ProviderScope
.
Instalação
Configure seu projeto incluindo a dependência do Flutter Riverpod.
flutter pub add flutter_riverpod
Como obtemos dependências com Riverpod?
Vamos usar um exemplo mais realista (um aplicativo meteorológico simples) para demonstrar como usar o Riverpod para injeção de dependência.
Existem três blocos principais no Riverpod com os quais precisamos estar familiarizados; Provider
, Ref
(ou WidgetRef
em widgets Flutter) e o ProviderScope
. Cada um desses blocos é importante para fazer o Riverpod
funcionar.
1. Provider
O primeiro bloco que já conhecemos é o Provider
que pode nos dar a dependência que precisamos. Por exemplo, se tivermos um WeatherRepository
para obter a previsão do tempo de hoje, podemos construir um provedor para esta dependência da seguinte forma:
// Weather Repository para obter o clima de hoje class WeatherRepository { final WeatherDataSource _dataSource = OpenWeatherApiDataSource(); Future<String> todayWeather() { return _dataSource.getWeatherToday(); } } // Weather Repository provider final Provider<WeatherRepository> weatherRepositoryProvider = Provider<WeatherRepository>( (ref) { return WeatherRepository(); }; );
O weatherRepositoryProvider
pode ser acessado e usado em nosso aplicativo para obter uma dependência WeatherRepository
.
2. ProviderScope
Cada dependência agrupada por um Provider
só pode ser acessada dentro de um ProviderScope
. O ProviderScope
define o ambiente do nosso aplicativo para que o estado de nossas dependências esteja contido neste escopo.
void main() { runApp( const ProviderScope( child: WeatherApp(), ), ); }
A melhor abordagem para definir o escopo dos provedores é agrupar todo o aplicativo em um ProviderScope
. Geralmente isso é feito no widget pai superior do aplicativo para que todas as dependências possam ser acessadas em qualquer lugar dentro desse escopo.
3a. Injeção de dependência em provedores usando Ref
O objeto Ref
é a chave para localizar nossos provedores no escopo definido. Se quisermos injetar uma fonte de dados meteorológicos para nosso WeatherRepository
, podemos fazer isso primeiro criando um Provider
para esta fonte de dados.
// Interface de fonte de dados meteorológicos abstract class WeatherDataSouce { Future<String> getWeatherToday(); } // Fonte de dados meteorológica fictícia OpenWeatherApi class OpenWeatherApiDataSource extends WeatherDataSouce { @override Future<String> getWeatherToday() async { await Future.delayed(const Duration(seconds: 2)); return "10C"; } } // Provedor de fonte de dados meteorológicos final Provider<WeatherDataSouce> openWeatherDataSourceProvider = Provider<WeatherDataSouce>( (ref) { return OpenWeatherApiDataSource(); }, };
Agora que temos um Provider
para nossa fonte de dados meteorológicos, podemos injetá-lo em nosso WeatherRepository
. Primeiro, refatoramos o repositório para aceitar uma fonte de dados ao construir o objeto. Em seguida, usamos o método watch
do objeto Ref no provedor de repositório meteorológico para obter a fonte de dados.
O método watch
localiza o estado mais recente do provedor que está sendo monitorado. Em nosso exemplo, será a instância OpenWeatherApiDataSource
.
// Usando ref para encontrar uma fonte de dados meteorológicos final Provider<WeatherRepository> weatherRepositoryProvider = Provedor<WeatherRepository>( (ref) { final weatherDataSource = ref.watch(openWeatherDataSourceProvider); return WeatherRepository(weatherDataSource: weatherDataSource); }, );
Em vez de acoplar firmemente a fonte de dados meteorológicos abertos ao WeatherRepository
, melhoramos o código injetando essa dependência.
Agora que sabemos como obter dependências usando o objeto Ref
, vamos continuar com WidgetRef
.
3b. Injeção de dependência em widgets usando WidgetRef
O Ref
é usado para obter dependências de provedores dentro de outros provedores. Por outro lado, WidgetRef
é usado para localizar provedores dentro de um widget Flutter.
A abordagem mais comum para obter um WidgetRef
é fazer com que seu widget se estenda da classe ConsumerWidget
, se for um StatelessWidget
, ou de ConsumerStatefulWidget
, se for um StatefulWidget
.
Aqui está um exemplo de widget sem estado usando ConsumerWidget
.
// Usando o widget ref para encontrar uma fonte de dados meteorológicos class WeatherScreen extends ConsumerWidget { const WeatherScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final weatherRepository = ref.watch(weatherRepositoryProvider); return FutureBuilder( future: weatherRepository.todaysWeather(), builder: (context, snapshot) { if (snapshot.hasData) { return Text(snapshot.data!); } else { return const CircularProgressIndicator(); } }, ); } }
Usamos a instância WidgetRef
na função de construção do widget para obter a dependência de que precisamos. No exemplo WeatherScreen
, obtemos a dependência WeatherRepository para obter o clima de hoje.
Quaisquer widgets que estendam ConsumerWidget
ou ConsumerStatefulWidget
que estejam dentro do ProviderScope
podem acessar o WeatherRepository
usando ref.watch()
.
Definir provedores como variáveis globais é algo ruim?
Há uma conotação ruim no uso de variáveis globais em seu código devido aos perigos de mutabilidade, testabilidade e escalabilidade. Variáveis globais tendem a não ser testáveis devido ao seu escopo global e à capacidade de serem modificadas em qualquer lugar do código.
Os provedores no Riverpod estão seguros dessa maneira porque os estados que eles possuem estão contidos no ProviderScope
. Eles também são sempre declarados como finais
, em que as variáveis que apontam para eles não podem ser atualizadas ou substituídas.
Como substituímos dependências ao executar testes?
Devemos ser capazes de restringir o ambiente e o escopo do nosso aplicativo ao executar testes. Dependências como repositórios de dados devem ser fáceis de substituir por instâncias simuladas ou stubadas à medida que você escreve seus testes. Aqui está como você faz isso com Riverpod.
Vamos colocar todos os nossos provedores em um arquivo separado chamado providers.dart
, que será mais ou menos assim:
//providers.dart final Provider<WeatherDataSouce> openWeatherDataSourceProvider = Provider<WeatherDataSouce>( (ref) { return OpenWeatherApiDataSource(); }, }; final Provider<WeatherRepository> weatherRepositoryProvider = Provedor<WeatherRepository>( (ref) { final weatherDataSource = ref.watch(openWeatherDataSourceProvider); return WeatherRepository(weatherDataSource: weatherDataSource); }, );
Se quisermos executar nosso aplicativo, geralmente executamos o main.dart
, que deve ficar assim:
// Codigo de Produção import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'weather_app.dart'; void main() { runApp( const ProviderScope( child: WeatherApp(), ), ); }
No entanto, se quisermos injetar versões falsas de nossas dependências em testes, podemos substituí-las usando o argumento overrides
no ProviderScope
do nosso arquivo de teste.
//Código de Teste import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod_di/app.dart'; import 'package:flutter_riverpod_di/providers.dart'; import 'package:flutter_riverpod_di/services.dart'; import 'package:flutter_test/flutter_test.dart'; class FakeWeatherDataSource extends WeatherDataSouce { @override Future<String> getWeatherToday() { return Future.value('Fake value'); } } void main() { testWidgets( 'Use a fake weather data source', (tester) async { await tester.pumpWidget( ProviderScope( override: [ openWeatherDataSourceProvider.overrideWithValue( FakeWeatherDataSource(), }, ], child: const WeatherApp(); ), ); }, ); }
No código de exemplo acima, substituímos a fonte de dados meteorológicos aberta por uma falsa enquanto usamos o WeatherRepository
real. Para fazer isso, precisamos usar o método overrideWithValue
do provedor e fornecer a instância desejada.
O fato de as variáveis serem globais não causou nenhum problema porque o escopo ainda é controlado ao executar nossos testes de widget e foi substituído usando o parâmetro overrides do ProviderScope
. Mantemos nosso código de produção como está e, ao mesmo tempo, podemos eliminar nossas dependências com dependências falsas.
Exemplo de aplicativo meteorológico
Colocando em prática tudo o que aprendemos, vamos implementar o aplicativo meteorológico completo.
A primeira coisa a fazer é certificar-se de que nosso aplicativo meteorológico esteja agrupado em um ProviderScope.
// Codigo de Produção import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'weather_app.dart'; void main() { runApp( const ProviderScope( child: WeatherApp(), ), ); }
Em seguida, declaramos os diferentes serviços que utilizaremos no projeto. Definimos a interface WeatherRepository
e WeatherDataSource
e sua implementação.
abstract class WeatherDataSouce { Future<String> getWeatherToday(); } class OpenWeatherApiDataSource extends WeatherDataSouce { @override Future<String> getWeatherToday() async { await Future.delayed(const Duration(seconds: 2)); return "10C"; } } class WeatherRepository { const WeatherRepository({required this.weatherDataSource}); final WeatherDataSouce weatherDataSource; Future<String> todaysWeather() { return weatherDataSource.getWeatherToday(); } }
A seguir, definimos os provedores que usaremos para obter nossas dependências em um arquivo chamado provedores.dart
. Precisamos de um WeatherRepository
para buscar o clima de hoje usando uma fonte de dados WeatherDataSource
. Em nossa implementação, nossa fonte de dados será codificada para um valor que inclui um atraso para simular o progresso do carregamento.
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'services.dart'; final Provider<WeatherDataSouce> openWeatherDataSourceProvider = Provider<WeatherDataSouce>( (ref) { return OpenWeatherApiDataSource(); }, }; final Provider<WeatherRepository> weatherRepositoryProvider = Provedor<WeatherRepository>( (ref) { final weatherDataSource = ref.watch(openWeatherDataSourceProvider); return WeatherRepository(weatherDataSource: weatherDataSource); }, );
Por fim, só falta escrever nossos widgets para exibir a previsão do tempo. Nosso WeatherApp
se estende do ConsumerWidget
para que possa acessar as dependências necessárias para obter os dados meteorológicos.
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'providers.dart'; import 'services.dart'; class WeatherApp extends ConsumerWidget { const WeatherApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final weatherRepository = ref.watch(weatherRepositoryProvider); return MaterialApp( theme: ThemeData( colorScheme: ColorSheme.fromSeed(seedColor: Colors.blueGray), useMaterial3: true, ), debugShowCheckedModeBanner: false; home: Scaffold( floatingActionButton: FloatingActionButton.extended( onPressed: () { ref.invalidate(weatherRepositoryProvider); }, label: const Text('Refresh'), ), body: Center( child: FutureBuilder<String>( future: weatherRepository.todaysWeather(), builder: (context, snapshot) { if ((snapshot.hasData) && (snapshot.connectionState == ConnectionState.done)){ return Text(snapshot.data!, style: Theme.of(context).textTheme.headlineMedium)); } else { return const CircularProgressIndicator(); } }, ), ), ), ); } }
Quando o widget WeatherApp
for criado, ele obterá uma referência do repositório meteorológico e obterá o valor Future
do clima de hoje. Usando um widget FutureBuilder
, ele aguardará o término da chamada antes de exibir o resultado.
Também adicionei um botão para simular a obtenção de dados meteorológicos atualizados. O ref.invalidate(provider)
é a forma de limpar o cache de dados armazenado pelo Riverpod. Quando chamamos esse método, o provedor fornecerá um estado novo e atualizado para acesso de nossos dependentes. Em nosso exemplo, ele tentará buscar novos dados do WeatherDataSource
.
O aplicativo meteorológico de amostra foi escrito para demonstrar a injeção de dependência de serviços, como fontes de dados e repositórios. Ao usar Riverpod, é muito melhor usar provedores como
FutureProvider
ouStreamProvider
para exibir dados em cache.
Resumo
A injeção de dependência é importante na arquitetura do seu aplicativo, pois mantém sua base de código sustentável e escalonável. Riverpod é uma ótima estrutura para usar não apenas para gerenciamento de estado, mas também para implementar injeção de dependência.