Riverpod and Bloc packages: comparison

Eugene M
Eugene M
5631 August, 1, 2022 12 min read
5631 August, 1, 2022 12 min read

Introduction

One of the most actively asked questions from Flutter beginners is “What is the best state management library?”. There are countless threads on Twitter and Reddit posts that pitch one solution against another or preach a pure solution of only using Flutter-provided classes like ChangeNotifier, but when it comes to real-life products, you have to decide on a stack of third parties and approaches that would be most beneficial to your team and your app.

Riverpod and Bloc are the most prominent packages which promote an immutable state (meaning that you cannot change the state of the app from the UI, so you eliminate the possibility of a rogue code behaving unpredictably).

Comparing Riverpod and Bloc is not strictly a correct approach. Both of these packages can be virtually split into two parts: dependency injection and state management. 

Bloc uses its own extensions of the Provider package for DI and Bloc or Cubit classes for storing state. Riverpod on the other hand is literally Provider 2.0 for DI and has a State Notifier package bundled with it for state management (and the name Riverpod is an anagram of the Provider for the reason that it’s next generation Provider).

Let’s start by comparing the state management packages.

Bloc/Cubit and State Notifier

First of all, we are going to throw out Cubit from this comparison. The reason being is that Cubit while being a bit simplified version of Bloc, looks and feels exactly the same way as State Notifier for a developer. As illustrated by Bloc authors, the workflow of Cubit looks the following way: 

UI - cubit - data

This approach is literally the same as State Notifier from a developer’s perspective. You can swap “cubit” in the picture above and nothing will change. The only difference is in the syntax of emitting state. Where Cubit uses emit(event) syntax, State Notifier uses state = event.

Bloc on the other hand relies on events instead of functions to get feedback from UI to Cubit. 

Events - response

Prior to version 8.0, using Bloc was a bit cumbersome due to the amount of boilerplate code you had to write. Apart from state class and a bloc itself, you needed a separate mapping of events to the state. This changed in 8.0 where Bloc authors introduced a concise way to map events to state directly in the constructor body of a bloc.

As you can see, there isn’t much of a difference between Bloc/Cubit and State Notifier. The main differences come from the way you inject your state management into UI and how you consume the respective package state.

Providing state to the widget tree

Both Bloc and Riverpod have satellite packages for use with Flutter - respectively flutter_bloc and flutter_riverpod. This is where the main difference lies.

Flutter_bloc relies on its own extension of Provider to allow Flutter widgets to access a Bloc instance. The provider relies on Flutter’s context, which is inseparable from the widget tree. This means that you have to add BlocProvider widgets into the widget tree on a level of the tree that is covered by a particular Bloc. Each BlocProvider stores a reference to a Bloc or Cubit that gets passed down the subtree of widgets.

On one hand, it makes it easier to limit a bloc only to the subtree that is governed by its state.

On the other hand in teams of mixed experience and in convoluted projects that have pressing deadlines, you can often have this in your widget tree:

bloc provider

Now, most of the Flutter devs on the internet will tell you that this is a bit too many global state providers, but again, for a lot of real-life projects, it’s much easier to implement and forget, especially if the customer is pressing deadlines.

Riverpod and its satellite flutter_riverpod do not rely on Flutter itself for dependency injection, because they don’t rely on context. The way it’s done is that Riverpod creates a container for all providers which exists separately from the tree. This means two things:

  1. You don’t need to inject state objects via widgets
  2. You can use dependency injection via Riverpod in any of the layers of your architecture

Riverpod providers are created as a global function:

final myProvider = Provider((ref) {

 return MyValue();

});

Now, the MyValue object here won’t be created until there is at least one listener that wants to subscribe to this provider. After that, the object is kept in memory.

Unlike Provider, a small downside of this is that a developer has to be careful about where he needs a certain provider and how to dispose of it properly. Luckily, Riverpod offers a modifier .autodispose, which gets rid of the object when there are no more listeners.

final userProvider = Provider.autoDispose<User>((ref) {

  Return User();

});

It can still be a bit more complicated to beginners than just injecting BlocProvider into a sub-tree and making sure you only listen to its changes under that sub-tree, but when you get used to it, it makes the widget tree much less cumbersome to work with.

Additionally as mentioned above, you can use Riverpod to inject dependencies on all of the layers. Consider the following simple layer structure:

repository - state - ui

For a state to have access to the repository object, in flutter_bloc you need to provide a repository object as a reference in a bloc object. This means that on UI when you want to add BlocProvider, you need to specify a repository object to instantiate it. For that, flutter_bloc has a separate widget called RepositoryProvider, which provides a repository object to underlying BlocProviders. But if you need multiple repositories in one state object, it becomes really cumbersome.

Of course, you could use a third-party solution like GetIt to help with that, but Riverpod provides a solution out of the box.

Remember that single container where Riverpod keeps all the providers? If you create a provider for your repository object, instead of declaring it as an argument for state class, you pass the reference of the container to state and read the repository provider when needed.

Moreover, you can watch the repository provider for changes if needed, and act accordingly.

Consuming state in widgets

Consuming state in widgets is very close between Bloc and Riverpod, but there are differences, which we can cover in 3 different scenarios:

Rebuilding widget on state change

Bloc uses the BlocBuilder widget to react to state changes from a bloc.BlocBuilder goes through the widget tree looking for the closest bloc of a certain type and subscribes to its state changes.

BlocBuilder<AuthCubit, AuthState>(

  builder: (context, state) {

    if (state.user == null) return Login();

    return Home();

  },

),

Riverpod uses the Consumer widget to do the same. Since we don’t have our state notifier in the tree, we cannot declare the type as with Bloc, but we do have our trusty container of providers and Consumer widget gives the container reference:

Consumer(

  builder: (context, ref, child) {

    final user = ref.watch(authControllerProvider);

    if (state.user == null) return Login();

    return Home();

    },

  )

Here we are explicitly subscribing to authControllerProvider which handles state changes.

The biggest difference here is that flutter_riverpod provides an ability to use Consumer on a user-written widget level instead of within a widget build method. While using flutter_riverpod you can use ConsumerWidget instead of StatelessWidget and ConsumerStatefulWidget instead of StatefulWidget.

Some might argue that it’s a departure from flutter principles. My humble opinion is that it’s a minuscule change for a developer which can be very useful in the code.

Rebuilding widget on partial state change

If we have a screen governed by one state object, which consists of multiple widgets representing a part of that state, we do not want the whole screen to rebuild when only a part of the state changes.

Bloc has a BlocSelector widget that allows you to listen to the changes of a part of the state.

BlocSelector<AuthCubit, AuthState, String>(

  selector: (state) => state.user.id

 

  builder: (context, state) {

    if (state.user == null) return Login();

    return Home();

  },

      ),

With Riverpod you have to change the code which subscribes to the state a bit, but you are still using the Consumer widget:

Consumer(

  builder: (context, ref, child) {

    final user = ref.watch(authControllerProvider.select((state) =>

       state.user.id));

    if (state.user == null) return Login();

    return Home();

    },

  )

Reacting to state without rebuilding

Bloc supplies a BlocListener widget which allows you to listen to changes in state and perform some action (e.g. showing a snackbar) without rebuilding the underlying widget:

 BlocListener<AuthCubit, AuthState>(

    listener: (context, state) {

      if (state.user == null) {

        ScaffoldMessenger.of(context).showSnackBar();

      }

    },

    child: Container(),

  ),

Riverpod provides similar functionality by using ref.listen instead of ref.watch, wherever container reference ref is available (either inside the builder of a Consumer widget or inside the build method of a ConsumerWidget). A bonus is that you can also monitor partial state change here.

ref.listen<User?>(

    authControllerProvider.select((value) => value.period),

   (previous, next) {

      if (next != null) {

         ScaffoldMessenger.of(context).showSnackBar();

       }

   });

Additionally, Bloc supplies the BlocConsumer widget which serves as a combination of BlocBuilder and BlocListener. Since it’s just syntax sugar, there is no point in including it in the comparison.

Changing state from UI

As mentioned above, Bloc utilizes events to send signals to state objects for it to apply business logic and make changes to the state. This means that a developer needs to create a number of event classes (abstracted from an event interface) that will be processed by the bloc object.

In Riverpod, you need to access the state object from the UI via reading it from the container. So you will be using ref.read(object) to call functions inside of it that make changes to the state.

ref.read(authControllerProvider.notifier).signOut();

This function is generally used for one-time actions like pressing a button.

Communication between state objects

Here is where I think Riverpod shines a lot compared to Bloc from my humble perspective.

Bloc asks the developer to subscribe to the streams of the state that are emitted from bloc objects to establish communication between them. This means that one bloc object has to be dependent on another bloc object, which again adds to the layers of dependency injection.

In Riverpod, we can rebuild a state class based on the changes of the other state class inside the provider declaration, because we have our container reference and we can subscribe to its changes. 

This enables us to create providers that are derived from other providers with ease. Consider having a state which reflects elements in a list displayed on the screen. The customer comes in and would like to search this list. We can derive a provider that takes a filtering parameter, applies it to the main list provider, and produces the filtered list. Since the derived provider listens to changes of the main provider, everything will happen in a chain.
List provider updated -> Filter applied -> UI updated.

This is an example from Riverpod’s documentation:

  final characters = Provider.autoDispose.family<List<Character>,String>((ref, filter) async {

  return fetchCharacters(filter: filter);

});

Of course, you can do the same thing with Bloc, by subscribing from FilteredItemsBloc to ItemsBloc. But it adds a layer of boilerplate, which no one needs.

Additional utility 

Riverpod

Riverpod doesn’t give you much in terms of additional utility but what it gives you is quite useful.

AsyncValue

AsyncValue is a class supplied with Riverpod. It offers a very convenient way to differentiate between different states of a Future: loading, data, error, and since Riverpod 2.0 - refreshing. In many cases, these states are all you need to build a UI.

It also provides short and concise syntax to use these states. Instead of having to write a number of if-conditions to process state, you can write a shorthand  .when

return itemList.when(

      data: (items) {

        return ItemListContents(

          items: items,

        );

      },

      error: (error, _) {

        if (itemList.hasValue) {

          return ItemListContents(

            objectives: itemList.value ?? [],

          );

        }

        return Center(child: Text((error as AppException).message(context)));

      },

      loading: () => const Center(child: AppLoadingIndicator()),

    );

AsyncValue provides more methods to handle a lot of common scenarios when working with Futures. It can even combine data and error states into one, which gives a lot of granular control over how the UI should react to this.

Integration with flutter_hooks

Flutter hooks is a package from the same author, so it makes sense that the two of them would work great together. If you use hooks_riverpod instead of flutter_riverpod , you can use two new widgets HooksConsumerWidget and HooksConsumerStatefulWidget, which blend together widgets from flutter_hooks and flutter_riverpod.

Bloc

Bloc provides additional utility in form of packages that you can install in addition to it, namely:

  • hydrated_bloc provides automatic persistence and storing of state. Basically, any state will be stored inside the local storage and restored when needed. Can be pretty useful for the offline mode of an app
  • replay_bloc provides an ability to undo/redo state changes. This is useful for exactly this purpose - having an undo-redo functionality without developing it from scratch
  • bloc_test gives a simple and concise way to test your blocs and their behavior. State notifier has a package based on bloc_test called state_notifier_test, but unlike bloc_test, it’s a third party to Riverpod, so using it always poses more risk.

 

Get a free consultation

Contact our team and receive a free approach plan and estimation for your future project

contact us

Conclusion

I specifically avoided using the word ‘versus’ in this article because choosing a package to manage your app states is up to one’s preference and experience. It’s possible even to go pure Flutter  and do it with its own dependency injection and state management mechanisms.

Thus this article is for purely informational purposes. It never hurts anyone to be aware of all the possibilities out there.