BLoC x Riverpod: Comparando abordagens de gerenciamento de estado no Flutter

Tempo de leitura: 6 minutes

BLoC vs. Riverpod – Este artigo faz parte de uma série que explica por que Riverpod é uma opção melhor para gerenciamento de estado em Flutter do que BLoc. Neste artigo, veremos os princípios do Riverpod e do BLoc e demonstraremos como eles podem ser usados para corrigir problemas em aplicativos básicos. Este artigo é um excelente ponto de partida se você não estiver familiarizado com esses pacotes ou quiser comparar seus principais recursos e habilidades.

A principal diferença entre Riverpod e BLoC
Esta seção mostrará como esses dois pacotes funcionam em cenários simples. Nós vamos demonstrar isso para você.

  • Como construir suas classes lógicas (em provedores Riverpod e BLoc/Cubit em BLoc)?
  • Injeção de dependência.
  • Como seus widgets no Flutter podem consumir seus estados?

Provedores e BLOC/Cubit

Considere o cenário em que desejamos criar uma página de lista de filmes para nosso aplicativo. Podemos usar um repositório intuitivo e uma classe de serviço para recuperar títulos de filmes e remover filmes.

A classe de serviço de filme exibida abaixo faz consultas HTTP para um endpoint da API usando o pacote Dio. Enquanto o método removeMovie recebe um nome de filme como entrada e o remove da API, o método fetchMovieNames retorna um Future com uma lista de nomes de filmes.

import 'package:dio/dio.dart';

class MovieService {
  final Dio _dio = Dio();

  Future<list<string>> fetchMovieNames() async {
    try {
      Response response = await _dio.get('https://someapi.url');

      return (response.data) as List<string>;
    } catch (error, stacktrace) {
      // ignorar: evitar_print
      print("Exception occured: $error stackTrace: $stacktrace");

      adicione seu tratamento de erros
      rethrow;
    }
  }

  Future<void> removeMovie(String movieName) async {
    try {
      await _dio.delete('https://someapi.url/$movieName');
    } catch (error, stacktrace) {
      // ignorar: evitar_print
      print("Exception occured: $error stackTrace: $stacktrace");

      // adicione seu tratamento de erros
      rethrow;
    }
  }
}

Cubit e BLoc

Esta é a classe BLoc que podemos utilizar para gerenciar o estado atual da página da lista de filmes. O MovieBloc define dois manipuladores de eventos: _onFetchMovie e _onRemoveMovie, e estende a classe BLoc do pacote bloc.

import 'package:flutter_bloc/flutter_bloc.dart';

class MovieBloc extends Bloc<movieevent, moviestate=""> {
  final MovieRepository movieRepository;

  MovieBloc({required this.movieRepository}) : super(MovieInitial()) {
    on<fetchmovie>(_onFetchMovie);
    on<removemovie>(_onRemoveMovie);
  }

  void _onFetchMovie(FetchMovie event, Emitter<moviestate> emit) async {
    emit(MovieLoading());
    try {
      final movieList = await movieRepository.fetchMovieNames();
      emit(MovieLoaded(movieList));
    } catch (e) {
      emit(MovieError(e.toString()));
    }
  }

  void _onRemoveMovie(RemoveMovie event, Emitter<moviestate> emit) async {
    try {
      emit(MovieLoading());
      await movieRepository.removeMovie(event.movieName);
      add(FetchMovie());
    } catch (e) {
      emit(MovieError(e.toString()));
    }
  }
}

Podemos usar Cubit em vez de BLoc em casos de uso menos complexos. Foi assim que implementamos a classe MovieCubit:

import 'package:flutter_bloc/flutter_bloc.dart';

class MovieCubit extends Cubit<moviestate> {
  final MovieRepository movieRepository;

  MovieCubit({required this.movieRepository}) : super(MovieInitial());

  void onFetchMovie() async {
    emit(MovieLoading());
    try {
      final movieList = await movieRepository.fetchMovieNames();
      emit(MovieLoaded(movieList));
    } catch (e) {
      emit(MovieError(e.toString()));
    }
  }

  void onRemoveMovie(String movieName) async {
    try {
      emit(MovieLoading());
      await movieRepository.removeMovie(movieName);
      onFetchMovie();
    } catch (e) {
      emit(MovieError(e.toString()));
    }
  }
}

Provedores de Riverpod

Vejamos como usar o Riverpod para desenvolver classes lógicas, agora que vimos como fazer isso com BLoC e Cubit. Dependendo de suas demandas exclusivas, Riverpod oferece uma variedade de tipos de provedores. Nessa situação, StateNotifier é uma escolha razoável porque faremos chamadas de API e retornaremos resultados.

Aqui está uma demonstração de como StateNotifier pode ser usado para construir um provedor Riverpod:

final movieNotifierProvider = StateNotifierProvider<movienotifier, moviestate="">(
  (ref) => MovieNotifier(ref.watch(movieRepositoryProvider)),
);

class MovieNotifier extends StateNotifier<moviestate> {
  MovieNotifier(
    this.movieRepository,
  ) : super(MovieInitial());

  final MovieRepository movieRepository;

  void fetchMovie() async {
    state = MovieLoading();
    try {
      final movieList = await movieRepository.fetchMovieNames();
      state = MovieLoaded(movieList);
    } catch (e) {
      state = MovieError(e.toString());
    }
  }

  void removeMovie(String movieName) async {
    try {
      state = MovieLoading();
      await movieRepository.removeMovie(movieName);
      fetchMovie();
    } catch (e) {
      state = MovieError(e.toString());
    }
  }
}

No entanto, você também pode utilizar um FutureProvider para este caso de aplicação simples. O AsyncValue que este provedor fornecerá tem estados de carregamento, carregamento e erro. Isso implica que você não precisará implementar vários estados de UI.

final movieListFutureProvider = FutureProvider<list<string>>((ref) async {
  final movieRepository = ref.watch(movieRepositoryProvider);
  return movieRepository.fetchMovieNames();
});

Basta dar uma olhada no código; tem apenas quatro linhas. Este caso de uso é relativamente fácil de entender, mas muitos aplicativos o utilizam porque é útil.

Examine todos os provedores listados na documentação.

Injeção de dependência BloC

O uso dos widgets BlocProvider e MultiBlocProvider no BloC é sugerido para injetar dependências em suas classes BloC. Com a ajuda desses widgets, você pode adicionar uma instância BLoc/Cubit à árvore de widgets que qualquer widget na subárvore pode usar.

Para fornecer uma única instância de um BloC/Cubit, use o BlocProvider. Aqui está uma ilustração de como você pode aplicá-lo:

BlocProvider(
  create: (context) => MyBloc(),
  child: MyWidget(),
)

Neste caso, estamos usando BlocProvider para dar à árvore de widgets acesso a uma instância do MyBloc. Qualquer descendente de MyWidget pode utilizar context.read() para obter a instância de MyBloc que o BlocProvider desenvolveu.

Por outro lado, MultiBlocProvider entrega muitas instâncias de Bloc/Cubit simultaneamente. Aqui está uma ilustração:

MultiBlocProvider(
  providers: [
    BlocProvider(create: (context) => BlocA()),
    BlocProvider(create: (context) => BlocB()),
  ],
  child: MyWidget(),
)

Para fornecer instâncias de Blocs/Cubits à árvore de widgets por meio de injeção de dependência, BlocProvider e MultiBlocProvider são utilizados. Eles possibilitam gerenciar as dependências do seu aplicativo de forma rápida e fácil.

 

Injeção de Dependência de Riverpod

Utilizando provedores no Riverpod, a injeção de dependência é possível. Classes chamadas provedores criam e mantêm objetos e podem ser usadas para injetar dependências em widgets ou outros provedores.

Riverpod tem diferentes tipos de provedores, incluindo Provider, FutureProvider, StreamProvider e ScopedProvider. Cada tipo de provedor tem um caso de uso específico e, ao fundi-los e compô-los, podem ser criadas dependências mais complicadas.

Aqui está um exemplo de como injetar uma dependência no Riverpod usando um provedor:

final movieRepositoryProvider = Provider((ref) => MovieRepository());

Da mesma forma, como uma instância do FutureProvider:

final movieListProvider = FutureProvider<list<string>>((ref) async {
  final movieRepository = ref.watch(movieRepositoryProvider);
  return movieRepository.fetchMovieNames();
});

Nesta ilustração, usamos um FutureProvider para recuperar uma lista de títulos de filmes usando o movieRepositoryProvider. O valor do provedor movieRepositoryProvider é recuperado e usado no FutureProvider usando o método ref.watch.

 

Construtores que os utilizam em widgets

Use o widget BlocBuilder no pacote BLoc para reconstruir seu widget com base nas alterações de estado do BLoc.

BlocBuilder é um widget oferecido pelo pacote Bloc que facilita a reconstrução rápida de uma árvore de widgets a partir do estado de um BLOC. Ele fica atento às mudanças no estado do Bloco e reconstrói a árvore de widgets sempre que elas ocorrem.

Aqui está uma ilustração de como o BlocBuilder é usado:

class MovieListBlocWidget extends StatelessWidget {
  const MovieListBlocWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<moviebloc, moviestate="">(
      bloc: MovieBloc(movieRepository: MovieRepository()),
      builder: (context, state) {
        if (state is MovieInitial) {
          return const Placeholder();
        } else if (state is MovieLoading) {
          return const CircularProgressIndicator();
        } else if (state is MovieLoaded) {
          return ListView.builder(
            itemCount: state.movieList.length,
            itemBuilder: (context, index) {
              return ListTile(
                title: Text(state.movieList[index]),
              );
            },
          );
        } else if (state is MovieError) {
          return Text(state.message);
        }

        return Container();
      },
    );
  }
}

Se desejar, o côvado pode ser usado no lugar do BLoc; mude MovieBloc para MovieCubit.

 

Consumer Widgets do Riverpod

Você pode usar ConsumerWidget ou ConsumerStatefulWidget e a função de observação que o objeto ref oferece para consumir um provedor Riverpod em um widget.

Lembre-se de nosso movieNotifierProvider, não é? StateNotifierProvider era exatamente o que era.

final movieNotifierProvider = StateNotifierProvider<movienotifier, moviestate="">(
  (ref) => MovieNotifier(ref.watch(movieRepositoryProvider)),
);

Podemos usar ref.watch(movieNotifierProvider) para obter o valor mais recente do provedor antes de usá-lo no método de construção do widget.

O estado do objeto movieList pode então ser verificado usando instruções condicionais e, com base em seu estado atual, vários widgets podem ser retornados.

class MovieListStateNotifierWidget extends ConsumerWidget {
  const MovieListStateNotifierWidget({super.key});

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

    if (movieList is MovieInitial) {
      return const Placeholder();
    } else if (movieList is MovieLoading) {
      return const CircularProgressIndicator();
    } else if (movieList is MovieLoaded) {
      return ListView.builder(
        itemCount: movieList.movieList.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text(movieList.movieList[index]),
          );
        },
      );
    } else if (movieList is MovieError) {
      return Text(movieList.message);
    }

    return Container();
  }
}

O código para usar FutureProvider em seu código é o seguinte.

class MovieListFutureProviderWidget extends ConsumerWidget {
  const MovieListFutureProviderWidget({super.key});

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

    return movieList.when(
      data: (data) {
        return ListView.builder(
          itemCount: data.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text(data[index]),
            );
          },
        );
      },
      loading: () => const CircularProgressIndicator(),
      error: (error, stackTrace) => Text(error.toString()),
    );
  }
}

Conclusão

Neste artigo, comparamos os componentes fundamentais dos pacotes de gerenciamento de estado Flutter BLOC e Riverpod. Investigamos as necessidades básicas de aplicativos simples que ambos os pacotes podem satisfazer.

Além disso, a decisão entre o BLoc e o Riverpod depende, em última análise, dos requisitos específicos de cada projeto e das preferências da equipe de desenvolvimento. A dimensão e a complexidade do projecto, bem como o grau de experiência e familiaridade com os dois quadros, devem ser tidos em conta. Os desenvolvedores frequentemente usam ambas as estruturas e demonstraram eficácia no gerenciamento do estado dos aplicativos Flutter. Independentemente da decisão, é essencial compreender os princípios de gestão do estado em sua essência e utilizar as melhores práticas ao desenvolver, testar e manter software. Com isso, os desenvolvedores Flutter da renomada empresa de desenvolvimento de aplicativos Flutter podem garantir uma experiência de usuário agradável e desenvolverão seus aplicativos com desempenho, escalabilidade e manutenção.