Riverpod vs Bloc: Comparison of basic features
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 ConsumerStatefulWidget
and 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 FutureProvider
in 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: