ref – https://pub.dev/packages/flutter_bloc
Events
Events are triggered due to some user action from the UI layer. Events can be represented via hierarchy by using abstract classes.
For example, in the app we are going to build today, there are three events, FetchWeatherEvent, ResetWeatherEvent, RefreshWeatherEvent. They all extend from WeatherEvents.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
abstract class WeatherEvents extends Equatable { const WeatherEvents(); } class FetchWeather extends WeatherEvents { final String city; FetchWeather({@required this.city}) : assert(city != null); @override List<Object> get props => [city]; } class RefreshWeather extends WeatherEvents { final String city; const RefreshWeather({@required this.city}) : assert(city != null); @override List<Object> get props => [city]; } class ResetWeather extends WeatherEvents { @override List<Object> get props => null; } |
They are triggered in WeatherView. In our UI, say we have a handler called onRefresh. When the refresh button is clicked, it will execute it like so:
1 2 3 4 5 6 7 8 9 10 11 |
... ... onPressed: () { if (_formKey.currentState.validate()) { BlocProvider.of<WeatherBloc>(context) .add(FetchWeather(city: weatherCityController.text)); } }, ... ... ) |
Notice that it uses BlocProvider to add our Event to the event stream’s sink. This is essentially what’s happening when we call BlockProvider.of
RefreshWeather extends from WeatherEvents, as are other classes.
Thus, on the UI layer, we put events on the event stream in such ways.
When the event arrives on the event stream, we have BlocObserver.
The idea is that we pass the Bloc and Event into our BlocObserver.
BlocObserver will then call weatherBloc’s mapEventToState with this particular event.
1 2 3 4 5 6 7 8 9 10 11 12 |
class SimpleBlocObserver extends BlocObserver { @override void onEvent(Bloc bloc, Object event) { print('simpleBlocObserver.dart - event:'); print(event); // FetchWeather, an Event print('simpleBlocObserver.dart - bloc:'); print(bloc); // instance of WeatherBloc. Will give the yield...etc super.onEvent(bloc, event); } ... } |
mapEventToState is an abstract function in our weatherBloc, which yield states to this particular event. Yielding state means we are sending the state in the counter stream back to the UI layer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class WeatherBloc extends Bloc<WeatherEvents, WeatherStates> { ... ... ... @override Stream<WeatherStates> mapEventToState(WeatherEvents event) async* { print('weatherBloc.dart - incoming event is:'); print(event); if (event is FetchWeather) { print('weatherBloc.dart - YIELD WeatherLoading instance'); yield WeatherLoading(); try { final Weather weather = await weatherRepository.getWeather(event.city); yield WeatherLoaded(weather: weather); } catch (error) { print(error); yield WeatherError(); } } } // mapEventToState } // class |
Notice that we await a weather repo to get the weather from a web API. Then we have received it, we just yield the state class WeatherLoaded with the weather data. Of course we can yield instances of other state classes that implements the State abstract class, which is used to put up say an indicator or stop an indicator.
weatherState.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
abstract class WeatherStates extends Equatable { const WeatherStates(); @override List<Object> get props => []; } class WeatherEmpty extends WeatherStates {} class WeatherLoading extends WeatherStates {} class WeatherLoaded extends WeatherStates { final Weather weather; WeatherLoaded({@required this.weather}) : assert(weather != null); @override List<Object> get props => [weather]; } class WeatherError extends WeatherStates {} |
Notice WeatherLoaded has a weather property that holds on to this new data.
We also override props because our abstract class WeatherStates extends Equatable.
In order to see if this instance is equal to another, we need to put our weather object in the props array
so Equatable can do the comparison
Okay, so now from the WeatherBloc, we have yielded the WeatherLoaded instance along with the weather data, now what?
We come back to simpleBlocObserver and hit the onTransition.
SimpleBlocObserver.dart
1 2 3 4 5 6 7 8 |
@override void onTransition(Bloc bloc, Transition transition) { print('---simpleBlocObserver.dart - onTransition----'); print(bloc); // instance of WeatherBloc print('simpleBlocObserver.dart - onTransition, transition:'); print(transition); // { currentState: WeatherEmpty, event: FetchWeather, nextState: WeatherLoading } super.onTransition(bloc, transition); } |
So this means that we’re currently on the WeatherEmpty state
The event passed in is fetchWeather
Because Event FetchWeather, weatherBloc yields WeatherLoading…that is why our nextState is WeatherLoading.
When the data arrives the state will be WeatherLoaded. But first! Let’s see what happens to the UI when we our state is on WeatherLoading.
In our weather.dart, our BlocConsumer’s listener will be activated.
BlocConsumer
BlocConsumer exposes a builder and listener in order react to new states.
BlocConsumer is analogous to a nested BlocListener and BlocBuilder but reduces the amount of boilerplate needed.
BlocConsumer should only be used when it is necessary to both rebuild UI and execute other reactions to state changes in the bloc.
BlocConsumer takes a required BlocWidgetBuilder and BlocWidgetListener and an optional bloc, BlocBuilderCondition, and BlocListenerCondition.
If the bloc parameter is omitted, BlocConsumer will automatically perform a lookup using BlocProvider and the current BuildContext.
1 2 3 4 5 6 7 8 |
BlocConsumer<BlocA, BlocAState>( listener: (context, state) { // do stuff here based on BlocA's state }, builder: (context, state) { // return widget here based on BlocA's state } ) |
in our case, look aa builder property. In it, its if-else has a condition where if state is WeatherLoading, then it returns an instance of a Center Widget. In it, it has a CircularProgressIndicator which will then run on the UI.
weather.dart
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
BlocConsumer<WeatherBloc, WeatherStates>( listener: (context, WeatherStates state) { if (state is WeatherLoaded) { print('weather.dart - State is Weather Loaded'); _refreshCompleter?.complete(); _refreshCompleter = Completer(); } }, builder: (context, WeatherStates state) { if (state is WeatherEmpty) { return EnterCity(); } else if (state is WeatherLoading) { print('weather.dart - WeatherLoading state...return Indicator'); return Center( child: CircularProgressIndicator(), ); } else if (state is WeatherLoaded) { .... .. ) |
So now our circular indicator is running, we’re also fetching data from the web api.
weatherBloc.dart
1 2 3 4 5 6 7 8 9 10 |
if (event is FetchWeather) { yield WeatherLoading(); // indicator will appear in our UI try { final Weather weather = await weatherRepository.getWeather(event.city); yield WeatherLoaded(weather: weather); // indicator removed. Weather data gets displayed } catch (error) { print(error); yield WeatherError(); } } |
Once the data comes back, we yield WeatherLoaded with the data. The BlocConsumer’s state changed to WeatherLoaded and it simply updates the UI.
In our particular case, we update our refresh control, and give it a ListView where it calls DisplayWeather to put all the text and new data onto the screen.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
else if (state is WeatherLoaded) { final weather = state.weather; return RefreshIndicator( onRefresh: () { BlocProvider.of<WeatherBloc>(context) .add(RefreshWeather(city: weather.location)); return _refreshCompleter.future; }, child: ListView( children: <Widget>[ DisplayWeather( weather: weather, ), ], ), ); |