18. Bloc to Cubit

18. Bloc to Cubit

ยท

7 min read

Welcome back, Glad that you are here.

We're building a Movie App with the best coding practices and tools. In the previous tutorial, we worked on the splash screen and loading animation while making an API call.

In this tutorial, we will convert the bloc to the cubit. Cubit was announced in version 6 of the flutter bloc library. You'll learn about situations where we can use Cubit's maximum features and where we cannot.

Before you go further in this tutorial/video, it is good if you are already aware of what is Bloc and what are its main components. You can watch the complete bloc tutorials here.

Why Flutter Bloc 6.0.0?

I started with the latest version of the flutter bloc which is 7.0.0. But, this version is built for dart 2.12 and above to support null safety. If we use dart 2.12, we need to refactor the complete app to implement null safety. But here we are also using hive which depends on build_runner and this is a blocker for us.

If I have to put in a simple sentence, we are not ready for Flutter 2 at least for those apps which heavily rely on build_runner, because unfortunately at the time of writing this tutorial, build_runner is not null safe.

Hence, I chose to go to the first version when cubit was announced.

Cubit Interoperability with Bloc

Cubit is fully interoperable with Bloc. Because bloc in version 6.0.0 extends Cubit itself. So, if you are using flutter_bloc: >=6.0.0 <=7.0.0, then you are already using cubit. In version 7.0.0 the implementations of Bloc and Cubit may be different because they both implement a new BlocBase class.

Why Cubit?

Cubit helps in reducing some boilerplate that we have in the bloc. You don't need Events while using cubit. In some cases, you can get rid of the States as well.

Cubit in MovieApp

If you have followed the complete series of tutorials/videos on the movie app, you are aware that we have only used Bloc to manage the state of screens in this app. I recommend reading/watching the Carousel tutorial to see one of the bloc implementations.

We are using bloc to get rid of setState() which rebuilds the complete widget on which is called. Bloc on the other side, with the help of BlocBuilder, BlocListener and BlocConsumer helps in building only specific sections of the screen whenever data changes.

When we are replacing the bloc with a cubit, there are two different types of solutions. First, when we are taking data from an event and yielding the same data or simply yielding a state without data. Second, where we are making API calls using the event data and then yielding the states.

Let's see examples of the first one

Simple Blocs to Cubit

Open the LoadingBloc and look at the code.

class LoadingBloc extends Bloc<LoadingEvent, LoadingState> {
  LoadingBloc() : super(LoadingInitial());

  @override
  Stream<LoadingState> mapEventToState(
    LoadingEvent event,
  ) async* {
    if (event is StartLoading) {
      yield LoadingStarted();
    } else if (event is FinishLoading) {
      yield LoadingFinished();
    }
  }
}

In the mapEventToState(), we are checking for the event and returning a state. This is the minimal bloc that we have. And in the UI, based on the state we will show or hide the loader as explained in Splash & Loader tutorial Let's convert this to extend Cubit.

//1
class LoadingCubit extends Cubit<bool> {
  //2
  LoadingCubit() : super(false);
  //3
  void show() => emit(true);
  //3
  void hide() => emit(false);
}
  1. We rename the LoadingBloc to LoadingCubit to logically make it understandable. Instead of extending Bloc, now extend with Cubit. Because we only need a boolean value to decide whether to show or hide loader, we are changing the state to a bool value.
  2. As our initial state of this bloc, we will make it default to false.
  3. As we have removed Events in the Cubit, we will not use mapEventToState(). Instead, create 2 public methods that will be called now instead of dispatching events. These methods will return a bool value.

Let's see how to call these methods and how to listen to the values.

Open MovieCarouselBloc for instance.

//Old
loadingBloc.add(StartLoading());
loadingBloc.add(FinishLoading());

//New
loadingCubit.show();
loadingCubit.hide();
  1. Instead of dispatching the events, you'll now directly call the methods present in the bloc.

Open LoadingScreen to change the BlocBuilder

//1
BlocBuilder<LoadingCubit, bool>
builder: (context, shouldShow)

//3
if (shouldShow)
  1. Replace the State with bool and rename the variable to a meaningful name.
  2. Instead of comparing the state type, we will now check if the flag is true or false to render UI.

Let's check another example of the same sort. I will only show Bloc changes now because the screen and calls are straight-forward.

Open MovieBackdropBloc and see the code

//OLD
class MovieBackdropBloc extends Bloc<MovieBackdropEvent, MovieBackdropState> {
  MovieBackdropBloc() : super(MovieBackdropInitial());

  @override
  Stream<MovieBackdropState> mapEventToState(
    MovieBackdropEvent event,
  ) async* {
    yield MovieBackdropChanged((event as MovieBackdropChangedEvent).movie);
  }
}

//NEW
class MovieBackdropCubit extends Cubit<MovieEntity> {
  MovieBackdropCubit() : super(null);

  void backdropChanged(MovieEntity movie) {
    emit(movie);
  }
}

Here as well, we get rid of state and event classes. And emit the data the same as in entity.

Little Complex Blocs to Cubit

I am calling this complex because here we will not be able to delete the State classes. Let's consider LanguageBloc.

class LanguageBloc extends Bloc<LanguageEvent, LanguageState> {

  //.... Constructor and class variables

  @override
  Stream<LanguageState> mapEventToState(
    LanguageEvent event,
  ) async* {
    if (event is ToggleLanguageEvent) {
      await updateLanguage(event.language.code);
      add(LoadPreferredLanguageEvent());
    } else if (event is LoadPreferredLanguageEvent) {
      final response = await getPreferredLanguage(NoParams());
      yield response.fold(
        (l) => LanguageError(),
        (r) => LanguageLoaded(Locale(r)),
      );
    }
  }
}

We cannot replace the State classes here because we have two different sets of data that this bloc yields - LanguageError and LanguageLoaded, because we have organized our app to use Either. So, let's see the solution with having State returned from Cubit.

//1
class LanguageCubit extends Cubit<LanguageState> {

  //.... Constructor and class variables

  //2
  void toggleLanguage(LanguageEntity language) async {
    await updateLanguage(language.code);
    loadPreferredLanguage();
  }

  void loadPreferredLanguage() async {
    final response = await getPreferredLanguage(NoParams());
    emit(response.fold(
      (l) => LanguageError(),
      (r) => LanguageLoaded(Locale(r)),
    ));
  }

}
  1. We change the Bloc to Cubit with keeping the State.
  2. We then create 2 methods to handle two different types of events. Here as well you'll emit the state.

Let's see the alternative solution which will change the way we emit data from the bloc.

//1
class LanguageCubit extends Cubit<Locale> {
  final GetPreferredLanguage getPreferredLanguage;
  final UpdateLanguage updateLanguage;

  LanguageCubit({
    @required this.getPreferredLanguage,
    @required this.updateLanguage,
  }) : 
  //2
  super(
          Locale(Languages.languages[0].code),
        );

  void toggleLanguage(LanguageEntity language) async {
    await updateLanguage(language.code);
    loadPreferredLanguage();
  }

  void loadPreferredLanguage() async {
    final response = await getPreferredLanguage(NoParams());
    //3
    emit(response.fold(
      (l) => Locale(Languages.languages[0].code),
      (r) => Locale(r),
    ));
  }
}
  1. First, you'll remove State and use the Locale.
  2. Next, set a default locale in the super as the initial locale.
  3. Now, in the loadPreferredLanguage(), you'll return null or the default locale. If you return null, UI logic has to change drastically. But, if you have some default value to return then nothing should break on UI.
  4. For success, instead of returning State, return the Locale.

Now, open MovieApp and change the way we are using BlocBuilder by using Locale instead of State.

BlocBuilder<LanguageCubit, Locale>(
  builder: (context, locale) {
    return WiredashApp(
      //1
      languageCode: locale.languageCode,
      child; MaterialApp(
        //1
        locale: locale,
      ),
    );
  }
)
  1. When we return null for Locale, we will have to handle that case in UI. Luckily, for language-like cases, we can have some default values. But, this solution cannot be easily applied to Blocs that return a List of movies, videos, cast, etc.

For all these, we will have to handle the NULL cases or you can say error cases.

I believe I have tried my best to teach you about migration from Bloc to Cubit with all possible usage and caveats. It has to be done case to case basis for existing blocs and new applications Cubit is a nice and clean alternative.

I will see you at the next tutorial. Thanks for watching/reading. Goodbye.

Did you find this article valuable?

Support Prateek Sharma by becoming a sponsor. Any amount is appreciated!

ย