ref –
- https://resocoder.com/2020/08/04/flutter-bloc-cubit-tutorial/
- https://medium.com/flutterando/cubit-a-simple-solution-for-app-state-management-in-flutter-66ab5279ef73
- http://chineseruleof8.com/code/index.php/2021/04/14/beginning-bloc-for-flutter/
- http://chineseruleof8.com/code/index.php/2021/04/15/working-with-flutter-bloc/
- https://github.com/irvine5k/cubit_movies_app/
A Cubit is the base for Bloc (in other words Bloc extends Cubit).
Cubit is a special type of Stream which can be extended to manage any type of state .
A functions is generally that something you create to do some orders as you want and this function will be achieved my orders when I would CALLING IT so it can’t achieve anything without any call from me . something like when I call KFC to order some food the function here is calling the restaurant to order my food because the restaurant can’t order food to me without calling it.
It replaces events in the bloc architecture. Originally, UI actions create events. Events get mapped to State. And we return certain state to update the data to be drawn in the UI.
However, instead of mutating individual fields, we emit whole new MyState objects. Also, the state is in a class separate from the one responsible for changing the state.
Importing Cubic
pubspec.yaml
1 2 3 4 5 |
dependencies: flutter: sdk: flutter bloc: ^6.0.1 flutter_bloc: ^6.0.1 |
State
A rule of thumb is to always build out the state class(es) first. So, what kind of WeatherState do we want to have?
We have the abstract class WeatherState to denote top of hierarchy.
We then create different Weather States that implements this abstraction. In our case:
WeatherInitial – This WeatherInitial state will indicate that no action has yet been taken by the user and that we should display an initial UI
WeatherLoading – we are waiting for the weather to arrive from its web api
WeatherLoaded – the data has arrived!
WeatherError – internet connection broke somehow.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
part of 'weather_cubit.dart'; @immutable abstract class WeatherState { const WeatherState(); } class WeatherInitial extends WeatherState { const WeatherInitial(); } class WeatherLoading extends WeatherState { const WeatherLoading(); } class WeatherLoaded extends WeatherState { final Weather weather; const WeatherLoaded(this.weather); @override bool operator ==(Object o) { if (identical(this, o)) return true; return o is WeatherLoaded && o.weather == weather; } @override int get hashCode => weather.hashCode; } class WeatherError extends WeatherState { final String message; const WeatherError(this.message); @override bool operator ==(Object o) { if (identical(this, o)) return true; return o is WeatherError && o.message == message; } @override int get hashCode => message.hashCode; } |
We also override annotation operator ==. This is because UI updates when state are different. Flutter will do a comparison between different states. If the states are different, update the UI. If they are the same, don’t do anything. Thus, for our Weather state, we need to override this to let flutter do the comparison.
1 2 3 4 |
bool operator ==(Object o) { if (identical(this, o)) return true; return o is WeatherLoaded && o.weather == weather; } |
If the two states are identical, then UI won’t do anything.
If the incoming object is of the same state (say WeatherLoaded) and the state’s weather object is the same, then we return true and UI won’t update.
But if either:
– the Object o is of another state
– the weather has changed
then we return false, to let Flutter know that the state have changed.
Always override equality of the state classes. Bloc will not emit two states which are equal after one another.
Thus, if we do not override equality, Bloc will ALWAYS update the UI and it degrades performance.
Cubit
Having finished our work on the state, let’s implement the WeatherCubit that will perform logic such as getting the weather from the WeatherRepository and emit states.
So we extend from Cubit
1 2 3 |
class WeatherCubit extends Cubit<WeatherState> { ... } |
We ctrl + click Cubit to see what’s going on underneath. We see that it’s an abstract class that uses a StreamController. This is mentioned in our earlier tutorials on basic blocs and stream:
- http://chineseruleof8.com/code/index.php/2021/04/14/beginning-bloc-for-flutter/
- http://chineseruleof8.com/code/index.php/2021/04/15/working-with-flutter-bloc/
1 2 3 4 5 6 7 |
abstract class Cubit<State> extends Stream<State> { Cubit(this._state); // notice this state final _controller = StreamController<State>.broadcast(); State _state; ... ... |
A stream can only take one type, and we see that Cubit takes on the state that we specify in the type bracket.
The StreamController instance then broadcasts it and it will be sink.add, or stream.listen accordingly. But when using Cubic, all that details are hidden away.
Do take note of the constructor. It takes a state as parameter.
Hence that is why in our initialization code, we put WeatherInitial() instance because it assigns the initial state to Cubit.
1 2 3 4 5 |
class WeatherCubit extends Cubit<WeatherState> { final WeatherRepository _weatherRepository; WeatherCubit(this._weatherRepository) : super(WeatherInitial()); ... } |
We then have our a weatherRepository instance that gets the data from a web api.
Notice that we don’t need to override mapEventToState here like we do in bloc architecture.
That is because in Cubit, there is no events. We only have functions that trigger state change.
Hence, in our case, we call getWeather to asynchronously get data. But before it does, we first emit a WeatherLoading state instance. This state is to trigger a circular indicator in our UI, as will be shown later.
1 2 3 4 5 6 7 8 9 |
Future<void> getWeather(String cityName) async { try { emit(WeatherLoading()); final weather = await _weatherRepository.fetchWeather(cityName); emit(WeatherLoaded(weather)); } on NetworkException { emit(WeatherError("Couldn't fetch weather. Is the device online?")); } } |
Then we do the async operation and get the weather data. After receiving this data, we emit that state to be loaded, along with the weather data itself.
The states then get reflected on the UI through BlocBuilder:
weather_search_page.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
Container( padding: EdgeInsets.symmetric(vertical: 16), alignment: Alignment.center, child: BlocBuilder<WeatherCubit, WeatherState>( builder: (context, state) { print('state: $state'); if (state is WeatherInitial) { print( 'weather_search_page.dart - WeatherInitial state --> returning buildInitialInput() UI'); return buildInitialInput(); } else if (state is WeatherLoading) { print( 'weather_search_page.dart - WeatherLoading state --> returning buildLoading() UI'); return buildLoading(); } else if (state is WeatherLoaded) { print( 'weather_search_page.dart - WeatherLoaded state --> returning buildColumnWithData() UI'); return buildColumnWithData(state.weather); } else { // (state is WeatherError) print('weather_search_page.dart - ERROR! go back to initial UI'); return buildInitialInput(); } }, // builder ), // BlockBuilder ) // Container |
Running the App
In the very beginning, we have this:
1 2 3 4 5 6 7 |
class WeatherSearchPage extends StatefulWidget { @override _WeatherSearchPageState createState() { return _WeatherSearchPageState(); } } |
It’s a class that extends StatefulWidget. We need to override StatefulWidget’s abstract method createState. createState returns State. Thus, we need to create a class that extends from State. We name it WeatherSearchPageState. The type of State is WeatherSearchPage.
1 2 3 |
class _WeatherSearchPageState extends State<WeatherSearchPage> { ... } |
We then override the build function, which is used to build the UI.
When we run the app, we get the log messages like so:
flutter: weather_search_page.dart – WeatherSearchPage – createState
flutter: weather_search_page.dart – state: Instance of ‘WeatherInitial’
flutter: weather_search_page.dart – WeatherInitial state –> returning buildInitialInput() UI
We get WeatherInitial state because in WeatherCubit, we initialized state it WeatherInitial():
1 |
WeatherCubit(this._weatherRepository) : super(WeatherInitial()); |
The way how the UI is implemented is. Our UI is organized in such a way that whenever a state is such, we return the proper UI widgets.
Then when we put in a city for weather, we start at the cubit. It uses the weather repo to do its async operation. Once its done, it emits a state. This state then is caught in the UI where weather_search_page.ldart sees that its a WeatherLoading.
flutter: weather_cubit.dart – weather is loading…
flutter: weather_repository.dart – getting weather for shanghai
flutter: weather_search_page.dart – state: Instance of ‘WeatherLoading’
flutter: weather_search_page.dart – WeatherLoading state –> returning buildLoading() UI
Then few seconds later, in weather_cubit, we receive weather data. It will emit WeatherLoaded.
Now, Cubit will do a comparison between WeatherLoading and WeatherLoaded in its operator overloaded ==.
flutter: weather_cubit.dart – received weather data!
flutter: weather_state.dart – WeatherLoaded – comparing Instance of ‘WeatherLoading’ and Instance of ‘WeatherLoaded’
flutter: weather_state.dart – DIFFERENT state, update UI NOW!
When the states are different, we update UI.
flutter: weather_search_page.dart – state: Instance of ‘WeatherLoaded’
flutter: weather_search_page.dart – WeatherLoaded state –> returning buildColumnWithData() UI