Riverpod vs Bloc: Comparison of basic features

Payam Zahedi
Snapp X
Published in
6 min readMay 16, 2023

In this article, we’ll explore the fundamentals of both Riverpod and Bloc and show you how they can solve problems in simple apps. Whether you’re new to these packages or just looking for a comparison of their basic features and capabilities, this article is a great place to start.

If you want to know more about this topic, you can follow me on Twitter.

https://twitter.com/payamzahedi95

Before we begin, you can access the complete code examples on GitHub:

Basic comparison of Riverpod and BloC

In this section, we will demonstrate how these two packages work in basic scenarios. We will show you

  • how to create your logic classes (providers in Riverpod and Bloc/Cubit in Bloc)
  • Dependency Injection
  • how to consume their states in your widgets.

Bloc/Cubit and Providers

Let’s say we want to create a movie list page for our app. To fetch movie names and remove movies, we can use a simple repository and service class.

The movie service class, shown below, utilizes the Dio package to perform HTTP requests to an API endpoint. The fetchMovieNames method returns a Future containing a list of movie names, while the removeMovie method takes a movie name as input and deletes it from the API.

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

Bloc and Cubit

This is the Bloc class we can use to manage the state of our movie list page. The MovieBloc extends the Bloc class from the bloc package and defines two event handlers: _onFetchMovie and _onRemoveMovie.


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

You may know that for simpler use cases, we can use Cubit instead of Bloc. Here’s our implementation of the MovieCubit class:


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

Riverpod Providers

Now that we’ve seen how to create logic classes with Bloc and Cubit, let’s explore how to do it with Riverpod. In Riverpod, there are multiple provider types to choose from based on your specific needs. In this case, since we’ll be making API calls and returning values, StateNotifier is a good option to use.

Here’s an example of how we can create a Riverpod provider using StateNotifier:


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

That’s it. However, for this basic use case, you can also use a FutureProvider. This provider will return an AsyncValue, which has loading, loaded, and error states. This means you won't need to implement different states for your UI.

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

return movieRepository.fetchMovieNames();
});

Just look at the code, it’s only four lines long. I know this use case is very simple, but it is also very useful and common in many apps.

Take a look at all Providers in the Documentation.

Dependency Injection in Bloc

In Bloc, the recommended way to inject dependencies into your Bloc classes is by using the BlocProvider and MultiBlocProvider widgets. These widgets allow you to provide an instance of a Bloc/Cubit to the widget tree so that it can be consumed by any widget in the subtree.

BlocProvider is used to provide a single instance of a Bloc/Cubit. Here's an example of how you would use it:

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

In this example, we are providing an instance of MyBloc to the widget tree using BlocProvider. Any descendant of MyWidget can use context.read<MyBloc>() to get the instance of MyBloc that was created by the BlocProvider.

MultiBlocProvider, on the other hand, is used to provide multiple instances of Bloc/Cubit at once. Here's an example:

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

In summary, BlocProvider and MultiBlocProvider are used for dependency injection in Bloc to provide instances of Blocs/Cubits to the widget tree. They allow for easy and efficient management of the dependencies in your app.

Dependency Injection in Riverpod

In Riverpod, you can achieve dependency injection using providers. Providers are classes that create and manage objects, and they can be used to inject dependencies into other providers or widgets.

There are different types of providers in Riverpod, such as Provider, FutureProvider, StreamProvider, and ScopedProvider, among others. Each type of provider has a specific use case, and they can be combined and composed to create more complex dependencies.

Here’s an example of how to use a Provider to inject a dependency in Riverpod:

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

and this is an example of the FutureProvider:

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

In this example, we are using the movieRepositoryProvider inside a FutureProvider to fetch a list of movie names. The ref.watch method is used to retrieve the value of the movieRepositoryProvider provider and use it in the FutureProvider.

Usage in Widgets — Builders

In Bloc Package, if you want to rebuild your widget base on the Bloc state changes, you should use BlocBuilder Widget.

BlocBuilder is a widget provided by the bloc package that helps to efficiently rebuild a widget tree based on the state of a Bloc. It listens for state changes in the Bloc and rebuilds the widget tree whenever the state changes.

Here’s an example of how to use BlocBuilder:


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

You can also use Cubit instead of Bloc, You just need to change MovieBloc to MovieCubit.

Riverpod Consumer Widgets

To consume a Riverpod provider in a widget, you can use the ConsumerWidget or ConsumerStatefulWidgetand the watch method provided by the ref object.

Can you remember our movieNotifierProvider ? It was a StateNotifierProvider.

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

To use this provider in the build method of the widget, we can use ref.watch(movieNotifierProvider) to get the current value of the provider.

We can then check the state of the movieList object using conditional statements and return different widgets based on its current state.


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

And this is the code for using FutureProviderin your code.

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

Conclusion

In this article, we compared the basic features of Bloc and Riverpod packages for state management in Flutter. We explored how both packages can fulfill the basic needs of simple applications.

Stay updated by following me on Twitter for the latest updates and Flutter-related content:

--

--

Payam Zahedi
Snapp X

I’m a Software Engineer, Flutter Developer, Speaker & Open Source Lover. find me in PayamZahedi.com