Injeção de dependência em Flutter usando Riverpod

Tempo de leitura: 8 minutes

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.

 

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 ou StreamProvider 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.