BloC vs. RiverPod Comparing State Management Approaches in Flutter

BLoC vs. Riverpod: Comparing State Management Approaches in Flutter

BLoC vs. Riverpod – This article is part of a series explaining why Riverpod is a better option for state management in Flutter than BLoc. In this article, we’ll look at the principles of Riverpod and BLoc and demonstrate how they may be used to fix issues in basic apps. This article is an excellent place to begin if you’re unfamiliar with these packages or want to compare their primary features and abilities.

The main difference between Riverpod and BLoC
This section will show how these two packages function in simple scenarios. We’ll demonstrate it to you.

  • How to build your logic classes (in Riverpod providers and BLoc/Cubit in BLoc)?
  • Dependency Injection.
  • How your widgets in Flutter can consume their states?

Providers and BLoc/Cubit

Consider the scenario where we wish to design a movie list page for our app. We may use an intuitive repository and service class to retrieve movie titles and remove movies.

The movie service class displayed below makes HTTP queries to an API endpoint using the Dio package. While the removeMovie method takes a movie name as input and removes it from the API, the fetchMovieNames method returns a Future with a list of movie names.

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) {
      // ignore: avoid_print
      print("Exception occured: $error stackTrace: $stacktrace");

      // add your error handling
      rethrow;
    }
  }

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

      // add your error handling
      rethrow;
    }
  }
}

Cubit and BLoc

This is the BLoc class we may utilize to manage the movie list page’s current state. The MovieBloc defines two event handlers: _onFetchMovie and _onRemoveMovie, and it extends the BLoc class from the bloc package.

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()));
    }
  }
}

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

We can use Cubit rather than BLoc in less complex use cases. This is how we implemented the MovieCubit class:

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()));
    }
  }
}

Providers of Riverpod

Let’s look at how to use Riverpod to develop logic classes now that we’ve seen how to accomplish it with BLoc and Cubit. Depending on your unique demands, Riverpod offers a variety of providers kinds. In this situation, StateNotifier is a reasonable choice because we’ll be making API calls and returning results.

Here’s a demonstration of how StateNotifier can be used to build a Riverpod provider:

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());
    }
  }
}

However, you can also utilize a FutureProvider for this simple application case. The AsyncValue this provider will deliver has loading, loaded, and error states. This implies that you won’t need to implement several UI states.

final movieListFutureProvider = FutureProvider<List<String>>((ref) async {
  final movieRepository = ref.watch(movieRepositoryProvider);

  return movieRepository.fetchMovieNames();
});

Just have a glance at the code; it only has four lines. This use case is relatively easy to understand, yet many apps use it because it is helpful.

Examine every provider listed in the documentation.

BLoc Dependency Injection

Using the BlocProvider and MultiBlocProvider widgets in BLoc is suggested for injecting dependencies into your BLoc classes. With the help of these widgets, you may add a BLoc/Cubit instance to the widget tree that any widget in the subtree can use.

To provide a single instance of a BLoc/Cubit, use the BlocProvider. Here is an illustration of how you might apply it:

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

In this case, we’re using BlocProvider to give the widget tree access to an instance of MyBloc. Any descendant of MyWidget can utilize context.read() to obtain the instance of MyBloc that the BlocProvider developed.

On the other hand, MultiBlocProvider delivers many instances of Bloc/Cubit simultaneously. Here’s an illustration:

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

To give instances of Blocs/Cubits to the widget tree through dependency injection, BlocProvider and MultiBlocProvider are utilized. They make it possible to manage your app’s dependencies quickly and easily.

Also Read: A Quick Guide to Riverpod Data Caching and Provider Lifecycles

Riverpod’s Dependency Injection

Utilizing providers in Riverpod, dependency injection is possible. Classes called providers create and maintain objects and can be used to inject dependencies into widgets or other providers.

Riverpod has different kinds of providers, including Provider, FutureProvider, StreamProvider, and ScopedProvider. Each kind of provider has a particular use case, and by merging and composing them, more complicated dependencies can be made.

Here is an example of how to inject a dependency in Riverpod using a Provider:

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

Likewise, as an instance of the FutureProvider:

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

In this illustration, we use a FutureProvider to retrieve a list of movie titles using the movieRepositoryProvider. The value of the movieRepositoryProvider provider is retrieved and used in the FutureProvider using the ref.watch method.

Builders who use them in widgets

Use BlocBuilder Widget in BLoc Package to rebuild your widget based on BLoc state changes.

BlocBuilder is a widget offered by the Bloc package that facilitates quickly reconstructing a widget tree from a BLoc’s state. It keeps an eye out for changes in the Bloc’s state and rebuilds the widget tree whenever one occurs.

Here’s an illustration of how BlocBuilder is used:

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();
      },
    );
  }
}


If you like, cubit can be used in place of BLoc; change MovieBloc to MovieCubit.

Consumer Widgets by Riverpod

You can use the ConsumerWidget or ConsumerStatefulWidget and the watch function the ref object offers to consume a Riverpod provider in a widget.

Keep in mind our movieNotifierProvider, did you? StateNotifierProvider was exactly what it was.

final movieNotifierProvider = StateNotifierProvider<MovieNotifier, MovieState>(
  (ref) => MovieNotifier(ref.watch(movieRepositoryProvider)),
);

We can use ref.watch(movieNotifierProvider) to obtain the provider’s most recent value before using it in the build method of the widget.

The movieList object’s state may then be checked using conditional statements, and based on its present state, various widgets can be returned.

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();
  }

The code to use FutureProvider in your code is as follows.

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();
  }
}

The code to use FutureProvider in your code is as follows.

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()),
    );
  }
}

Flutter Developers from Flutter Agency

Conclusion

In this article, we compared the fundamental components of the Flutter state management packages BLoc and Riverpod. We investigated the basic needs of simple applications that both packages can satisfy.

In addition, the decision between BLoc and Riverpod ultimately comes down to each project’s specific requirements and the development team’s preferences. The size and complexity of the project, as well as the degree of experience and familiarity with the two frameworks, should all be taken into account. Developers frequently use both frameworks and have shown effectiveness in managing the state of Flutter apps. Regardless of the decision, it is essential to comprehend the state management principles at its core and to use best practices when developing, testing, and maintaining software. With this, Flutter developers from the reputed Flutter app development company can ensure a pleasant user experience and will develop your apps performant, scalable, and maintainable by doing.

Frequently Asked Questions (FAQs)

1. What makes Riverpod preferable to the provider?

On the other side, Riverpod created as a rewrite of the provider, offers a state management API that is more declarative and concise. The Scoped Model concept, which supports Riverpod’s strategy, enables developers to manage the state of a specific component of their program without affecting other aspects.

2. What distinguishes Riverpod and BLoc from one another?

Packages from Riverpod and BLoc are compared. The syntax of the emitting state is the only variation. State Notifier uses the syntax state = event instead of Cubit’s emit(event) syntax. On the contrary, BLoc uses events rather than functions to communicate input from the UI to Cubit.

3. Why does Flutter require state management?

One of the key ideas behind Flutter state management is managing the state independently of the properties. By maintaining the state separately, you may obtain more efficient updates and better app performance. When changes are made to the state, Flutter merely rebuilds the impacted widgets rather than the entire app.

Book Your Flutter Developer Now

Abhishek Dhanani

Written by Abhishek Dhanani

Abhishek Dhanani, a skilled software developer with 3+ years of experience, masters Dart, JavaScript, TypeScript, and frameworks like Flutter and NodeJS. Proficient in MySQL, Firebase, and cloud platforms AWS and GCP, he delivers innovative digital solutions.

Leave a comment

Your email address will not be published. Required fields are marked *


ready to get started?

Fill out the form below and we will be in touch soon!

"*" indicates required fields

✓ Valid number ✕ Invalid number