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);
}
- We rename the
LoadingBloc
toLoadingCubit
to logically make it understandable. Instead of extendingBloc
, now extend withCubit
. Because we only need a boolean value to decide whether to show or hide loader, we are changing the state to abool
value. - As our initial state of this bloc, we will make it default to
false
. - As we have removed
Events
in theCubit
, we will not usemapEventToState()
. Instead, create 2 public methods that will be called now instead of dispatching events. These methods will return abool
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();
- Instead of dispatching the events, you'll now directly call the methods present in the bloc.
Open
LoadingScreen
to change theBlocBuilder
//1
BlocBuilder<LoadingCubit, bool>
builder: (context, shouldShow)
//3
if (shouldShow)
- Replace the
State
withbool
and rename the variable to a meaningful name. - Instead of comparing the state type, we will now check if the flag is
true
orfalse
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)),
));
}
}
- We change the
Bloc
toCubit
with keeping theState
. - 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),
));
}
}
- First, you'll remove
State
and use theLocale
. - Next, set a default locale in the
super
as the initial locale. - Now, in the
loadPreferredLanguage()
, you'll return null or the default locale. If you returnnull
, UI logic has to change drastically. But, if you have some default value to return then nothing should break on UI. - For success, instead of returning
State
, return theLocale
.
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,
),
);
}
)
- 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.