Bloc or Flutter Riverpod? I didn’t choose Riverpod for seven reasons.

Introduction

The first question we encounter when starting a new Flutter project is, “What state management method do I choose?” There are several solutions available, but two well-known ones at the moment are Bloc and Riverpod. Bloc is what I’ve personally been using ever since I discovered Flutter. I recently tried Riverpod, and I think it’s safe to say that I’ll remain with Bloc. I’ll tell you why.

Riverpod doesn’t go very well with Clean Architecture

Using CleanArchitecture and Bloc, I created a boilerplate project. My attempt to migrate it to Riverpod was unsuccessful due to Dependency Injection (DI).

Get_it and injectable were the two libraries I was using for DI, but I had to replace them with Riverpod’s Provider classes because Riverpod comes with a pre-built solution. A calamity is about to break out.

To clarify, with Clean Architecture, both the Data and Domain modules rely on an abstract class, such as this one, rather than the other way around.

We will need to create a Provider object for every class in order to leverage DI in Riverpod. For instance, the borrowMoneyUseCaseProvider in the class BorrowMoneyUseCase will look like this:

final borrowMoneyUseCaseProvider = Provider<BorrowMoneyUseCase>(
  (ref) => BorrowMoneyUseCase(
    ref.watch(userRepositoryProvider),
  ),
);

class BorrowMoneyUseCase {
  const BorrowMoneyUseCase(this._userRepository);

  final UserRepository _userRepository;
}
Dart

We also need to declare userRepositoryProvider because loginUseCaseProvider depends on it. But where should we make the announcement? In the Data module’s UserRepositoryImpl or the Domain module’s UserRepository?

It appears that userRepositoryProvider will need to be defined in the Data module in order to import and use those providers because it also depends on other providers from the Data module, such as appApiServiceProvider, appPreferencesProvider, and appDatabaseProvider.

// module data
final userRepositoryProvider = Provider<UserRepository>(
  (ref) => RepositoryImpl(
    ref.watch(appApiServiceProvider),
    ref.watch(appPreferencesProvider),
    ref.watch(appDatabaseProvider),
  ),
);

class UserRepositoryImpl {
}
Dart

The userRepositoryProvider from the Data module has to be imported into borrowMoneyUseCaseProvider from the Domain module next. However, this goes against the Dependency Rule of Clean Architecture, which states that no files from the Data module may be imported into the Domain module, nor may the Domain module rely on the Data module. This is the point at when everything seems out of order.

Does not natively support the Event Transformer

You don’t need to know RxDart to use Event Transformation functions like throttleTime, debounceTime, etc. thanks to my boilerplate project. For instance, we may use debounceTime to stop API request spam in order to build the live-search feature. To do this, we just need to use the following code:

on<KeyWordChanged>(
      _onKeyWordChanged,
      transformer: debounceTime(),
);
Dart

Or, when we want to use throttleTime for features like favoriting a post, we can simply do this:

on<FavoriteButtonPressed>(
      _onFavoriteButtonPressed,
      transformer: throttleTime(),
);
Dart

It’s hard to imagine how we can do the same with Riverpod.

Everything is Singleton by default

Bloc classes and ViewModel classes can be declared as Factory or Singleton when the DI is used with get_it and injectable.

Since ViewModels are typically specified as Factory, the application will include several instances of ViewModel.
In certain rare situations, we designate a ViewModel as a Singleton in order to generate a single instance with its state shared across all of the app’s displays.

@Injectable() // factory
class ItemDetailViewModel {}

@LazySingleton() // singleton
class AppViewModel {}
Dart

However, with Riverpod, all providers are Singleton by default. If we want to declare ViewModel as Factory, we can use the .family modifier.

final loginViewModelProvider =
    StateNotifierProvider.family<LoginViewModel, LoginState, int>(
  (ref, uniqueId) => LoginViewModel(uniqueId),
);

class LoginViewModel {
   LoginViewModel(this.uniqueId);
   
   final int uniqueId;
}
Dart

But, since we need to pass the uniqueId , what do we pass for LoginViewModel ? Are we going to generate a random ID to use it?

Riverpod is too complicated

Riverpod is indeed a really complicated library with things like:

  • Provider
  • StateNotifierProvider
  • ChangeNotifierProvider
  • StateProvider
  • Notifier
  • AsyncNotifier
  • FutureProvider
  • StreamProvider
  • autoDispose
  • family
  • ref, watch, read, listen
  • ConsumerWidget
  • ConsumerStatefulWidget
  • ProviderScope
  • ProviderContainer

Meanwhile, with Bloc, we only need to know a few classes:

  • BlocBuilder
  • BlocListener
  • BlocConsumer
  • BlocSelector
  • BlocProvider
  • Bloc/Event/State

To make matters worse, the material provided by Riverpod is inadequate for educational purposes, making learning even more difficult.

 Global scope variables can easily break your Architecture

Although the developer claims that declaring Global Scope variables is acceptable, I think you should use caution when using them because of how easy your design can be destroyed by their flexibility and ease of access.

You have the ability to obtain any provider you desire using only one variable, Ref ref. There is a circular dependence because we can access the class Repository from ViewModel and vice versa.

final loginViewModelProvider = Provider((ref) => LoginViewModel(ref));

class LoginViewModel {
  final Ref ref;

  LoginViewModel(this.ref);

  void login() {
    ref.read(repositoryProvider).login();
  }

  void updateNewState(String newState) {
    // do something
  }
}
Dart

final repositoryProvider = Provider((ref) => Repository(ref));

class Repository {
  final Ref ref;

  Repository(this.ref);

  void login() {
     ref.read(loginViewModelProvider).updateNewState('newState');
  }
}
Dart

We can also get Navigator , and show dialogs right inside ApiService .

class ApiSerivce {
  final Ref ref;
  
  ApiSerivce(this.ref);

  void request() {
    // if error
    ref.read(navigatorProvider).push(NewPage);
  }
}
Dart

Doesn’t support ‘mounted’

The classes StateNotifierProvider and ChangeNotifierProvider are no longer in use as of right now. Instead, we employ AsyncNotifier. AsyncNotifier does not, however, provide a mounted variable to determine whether the Provider object has been disposed of. This is crucial because there will be an exception if the new state is updated for a provider who has already been disposed of. You may check for Bloc by using the isClosed variable.

@override
  void add(E event) {
    if (!isClosed) {
      super.add(event);
    } else {
      Log.e('Cannot add new event $event because $runtimeType was closed');
    }
  }
Dart

Bloc is not perfect, either

It is not to argue that Bloc is without flaws, though. Its longest weakness is by far its length. Compared to Bloc, Riverpod will require far less work to implement a basic screen. However, we are able to quickly generate code because of VSCode Extensions like Bloc VSCode Extension and tools like mason_cli. We could even create the gadget ourselves!

2 thoughts on “Bloc or Flutter Riverpod? I didn’t choose Riverpod for seven reasons.”

Comments are closed.