Um mergulho profundo em Riverpod vs Bloc

Tempo de leitura: 4 minutes

Ultimamente, Riverpod, uma “versão melhor do provider” criada por ninguém menos que o criador original do provider Remi Rousselet, tem sido um tema quente na comunidade Flutter. Ele resolve muitos problemas de pacote de provedores e adiciona recursos adicionais que o provedor não era capaz de fazer, mas como ele se compara a um bloc? Como ele gerencia o estado? Que tal injeção de dependência? É testável da mesma forma que um bloc? Tentarei responder a tudo isso enquanto aprofundo como o riverpod funciona em comparação com o bloc.

Este artigo se concentra no riverpod. Observe que não explicarei como o bloco funciona. Portanto, seria melhor se você tivesse pelo menos conhecimento básico de bloc antes de ler isto.

 

Gerenciamento de estado

Ok, vamos começar com um exemplo simples de busca de dados Cubit como este:

class LatestRaceWinnersCubit extends Cubit<LatestRaceWinnersState> {
  LatestRaceWinnersCubit(this.repository) : super(const LatestRaceWinnersInitial());

  final WinnersRepository repository;

  Future<void> load() async {
    emit(const LatestRaceWinnersLoading());
    try {
      final winners = await repository.getLatestRaceWinners();
      emit(LatestRaceWinnersLoaded(winners));
    } catch (e) {
      emit(const LatestRaceWinnersFailure());
    }
  }
}

Riverpod tem algo muito semelhante ao Cubit, chamado StateNotifier. Quando você olha para o código, é quase idêntico:

class LatestRaceWinnersNotifier extends StateNotifier<LatestRaceWinnersState> {
  LatestRaceWinnersNotifier(
    this.repository,
  ) : super(const LatestRaceWinnersInitial());

  final WinnersRepository repository;

  Future<void> load() async {
    state = const LatestRaceWinnersLoading();
    try {
      final winners = await repository.getLatestRaceWinners();
      state = LatestRaceWinnersLoaded(winners);
    } catch (e) {
      state = const LatestRaceWinnersFailure();
    }
  }
}

StateNotifier é apenas um Cubit. A implementação de ambos é muito semelhante e ambos são baseados no StreamController, um conceito já comprovado em aplicações do mundo real.

Ok, mas e quanto ao nível do widget? Qual é a diferença aí?

Bem, pouca ou nenhuma diferença, o riverpod oferece métodos .watch e .read semelhantes. Ele também fornece um modificador semelhante a .select. Ok, chega de conversa, vamos ver o widget:

class MainPageRiverpodContent extends ConsumerWidget {
  const MainPageRiverpodContent({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(latestRaceWinnersNotifierProvider);

    if (state is LatestRaceWinnersLoading) {
      return const LoadingWidget();
    } else if (state is LatestRaceWinnersFailure) {
      return const FailureWidget();
    } else if (state is LatestRaceWinnersLoaded) {
      return WinnersPodiumWidget(
        winnersModel: state.raceWinners,
        onRefresh: () {
          ref.read(latestRaceWinnersNotifierProvider.notifier).load();
        },
      );
    } else {
      return const SizedBox.shrink();
    }
  }
}

* ConsumerWidget atua como um StatelessWidget, com uma maneira prática de acessar seus provedores com WidgetRef.

Há também o Consumer como um widget que você pode usar dentro de sua árvore de widgets (semelhante ao BlocBuilder). Com ele, você pode decidir mais facilmente quais partes da UI devem ser reconstruídas ou usadas dentro de um StatefulWidget.

Assim como o BlocListener, existe um método .listen que escuta as mudanças de estado sem reconstruir o widget, apenas sem aninhar outro widget na sua árvore de widgets.

ref.listen(latestRaceWinnersNotifierProvider, (previous, next) {
  if (next is LatestRaceWinnersLoaded) {
    print('Vencedores da corrida carregados com sucesso');
  }
});

Você também pode agrupar futuros, fluxos ou até mesmo valores singulares em provedores e usá-los em seus widgets com a mesma lógica WidgetRef, o que pode ser muito útil, especialmente se você quiser testá-los.

 

Injeção de dependência

Bloc, altamente inspirado em um provedor, usa um DI simples. Consiste nos mesmos problemas que o riverpod tenta resolver. Riverpod substitui BlocProviders e RepositoryProviders por, você adivinhou, “provedores”.

final winnersRepositoryProvider = Provider((ref) {
  return WinnersRepository();
});

Para que funcione, você precisa agrupar seu aplicativo com ProviderScope e pronto. ProviderScope funciona exatamente como RepositoryProvider, ele cria objetos quando eles são ouvidos.

Como o ProviderScope está localizado no topo da árvore de widgets, não é fácil descartar as dependências. Normalmente, você não deseja manter o estado de um widget específico durante sua vida útil e deseja descartá-lo automaticamente quando for fechado. Você pode fazer isso adicionando um modificador ao StateNotifierProvider assim:

final latestRaceWinnersNotifierProvider =
    StateNotifierProvider.autoDispose<LatestRaceWinnersNotifier, LatestRaceWinnersState>((ref) {
  return LatestRaceWinnersNotifier(ref.watch(winnersRepositoryProvider))..load();
});

Vale ressaltar também que ao criar um provedor você tem acesso ao ref, com o qual pode acessar outras dependências como no exemplo acima. Se não forem inicializados no ponto de acesso, serão inicializados então.

Testes unitários

Testar o notificador de estado é bastante simples. Embora não forneça uma biblioteca adicional para testes como bloc, nada mais do que a biblioteca padrão de testes de flutter e mockito/mocktail deve ser suficiente.

when(() => mock.getLatestRaceWinners()).thenAnswer((_) async => winnersModel);

expectLater(
  notifier.stream,
  emitsInOrder([
    isA<LatestRaceWinnersLoading>(),
    isA<LatestRaceWinnersLoaded>(),
  ]),
);

await notifier.load();

 

 

Widget ou testes de ouro

Os testes de widget e de ouro são outra história porque dependem da sua abordagem. No AppUnite, geralmente tentamos simular a camada mais baixa possível, por isso garantimos testar apenas a IU e não tudo o que está entre elas. Também nos dá mais flexibilidade. E aqui está o problema – você não pode zombar do estado dos notificadores estaduais (ou pelo menos no momento em que escrevo este artigo, mais informações aqui).

Você pode simular diferentes camadas, como no exemplo acima, e testar seus widgets dessa forma, mas é imperfeito porque você não controla completamente o que está exibindo.

Criando uma classe wrapper para StateNotifier e definindo o estado desejado diretamente, haveria outra solução.

 

Resumo

Para finalizar, o riverpod é um ótimo pacote de gerenciamento de estado que, em muitas ocasiões, é mais simples que o bloc. Ele fornece um nível semelhante de documentação e muitas pessoas estão começando a aprendê-lo em suas aplicações do mundo real. A injeção de dependência é de um padrão muito mais elevado do que o do bloc. A sintaxe e a sensação do riverpod são muito melhores do que as do bloc e, depois de aprendê-lo, será uma alegria escrever o código.

Mas nem tudo é perfeito. Os testes de ouro são uma grande parte do nosso desenvolvimento e utilizá-los em todo o seu potencial é muito importante para nós. Se não for valioso para você, não há muito a dizer contra o Riverpod.