Flutter Bloc : A Complete Guide
What is Bloc and its Architecture? What is BlocBuilder, BlocProvider, BlocListener, BlocConsumer, RepositoryProvider? Folder Structure For Bloc.

Search for a command to run...
What is Bloc and its Architecture? What is BlocBuilder, BlocProvider, BlocListener, BlocConsumer, RepositoryProvider? Folder Structure For Bloc.

Thanks Noel Rivero. Glad you liked it ๐
Lessons from Code, Content, and Consistency

Reliability basics: Outbox pattern + why it matters

Introducing Kafka/Redpanda + move to event-driven workflow

Connect Orders โ Inventory (first working service-to-service flow)

Inventory service + gRPC + proto contracts



Bloc. Bloc is not just a state management, but it's also an architectural design pattern which helps us to build production-level applications.



GET, POST, DELETE, etc methods inside this class. For example, To get the raw data from OpenWeatherMap API we can do something like// weather_data_provider.dart
class WeatherDataProvider {
Future<http.Response> getRawWeatherData(String city) async {
http.Response rawWeatherData = await http.get(
Uri.parse("Url"),
);
return rawWeatherData;
}
}
//Sample Raw Data looks like this
{
"weather": [
{
"id": 800,
"main": "Clear",
"description": "clear sky",
"icon": "01d"
}
],
"main": {
"temp": 282.55,
"feels_like": 281.86,
"temp_min": 280.37,
"temp_max": 284.26,
"pressure": 1023,
"humidity": 100
},
"name": "Mountain View",
}
raw data returned by the Data Provider in this layer. (For ex: converting the raw into some kind of Model). Bloc communicates with this layer when the user requests the data. This layer requests raw data from the Data Provider and after that, this layer performs some kind of transformation. For example, converting raw weather data to WeatherModel.// weather_repository.dart
class WeatherRepository {
late final WeatherDataProvider weatherDataProvider;
Future<Weather> getWeather(String location) async {
final http.Response rawWeather =
await weatherDataProvider.getRawWeatherData(location);
final json = jsonDecode(rawWeather.body);
final Weather weather = Weather.fromJson(json);
return weather;
}
}

State it receives from the Bloc. For example, there could be different kinds of states - LoadingState - Will Show Progress Indicator
LoadedState - Will Show Actual widget with data
ErrorState - Will show an error that something went wrong.
Note: If you don't know about
Streamsthen please learn it first as it is the base of the bloc, I'll not cover it in this article as I've already covered it in my previous article, So make sure you check it.
flutter create bloc_example
main.dartvoid main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: WeatherApp(),
);
}
}
weather_app.dartclass WeatherApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Weather App"),
centerTitle: true,
),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextField(
decoration: InputDecoration(
hintText: "Enter city name",
border: OutlineInputBorder(),
),
),
Column(
children: [
Image.asset(
"assets/images/sun.png",
height: 200,
width: 200,
),
SizedBox(
height: 20,
),
Text(
"New York",
style: Theme.of(context).textTheme.headline3,
),
SizedBox(
height: 5,
),
Text(
"38",
style: Theme.of(context).textTheme.headline4!.copyWith(
fontWeight: FontWeight.bold, color: Colors.black),
),
],
),
ElevatedButton(
onPressed: () {},
child: Text("Get Weather"),
)
],
),
),
);
}
}

pubspec.yaml and add flutter_bloc, bloc, and http packages inside dependenciesdependencies:
flutter:
sdk: flutter
flutter_bloc: ^7.1.0
bloc: ^7.1.0
http: ^0.13.3
bloc fileslib folder of your application, here you'll see Bloc: New Bloc option if you've installed the bloc extension that I've mentioned earlier.

Now If you see in the lib directory the folder named bloc is created and it has 3 different file - weather_bloc, weather_event, and weather_state
weather_bloc.dart :import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:meta/meta.dart';
part 'weather_event.dart';
part 'weather_state.dart';
class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
WeatherBloc() : super(WeatherInitial());
@override
Stream<WeatherState> mapEventToState(WeatherEvent event) async* {
// TODO: implement mapEventToState
}
}
weather_bloc.dart class is a bridge between our UI and the Data class, In other words, this class will handle all the Events triggered by the User and sends the relevant State back to the UI.
WeatherBloc class with Bloc which takes two things WeatherEvent and WeatherState. As the name suggests, they handle the applications State and Events respectively.weather_event.dart and weather_state.dart respectively.constructor of our class is created. In this, we need to provide an initial state. It's not going to do anything. It simply represents that the app is now in its initial stage and nothing has happened yet.mapEventToState method. As the name suggests, It will Map the Events to State. In other words, what this is gonna do is, It's gonna take some kind of Event (ex: Increment Counter event, Get Weather event, Decrement Counter event, etc). And it's our responsibility to write the functionality of what suppose to happen after that event is triggered.state is going to result as an effect of that event.mapEventToState method.weather_event.dart :/*import statements*/
part of 'weather_bloc.dart';
@immutable
abstract class WeatherEvent {}
class WeatherRequest extends WeatherEvent {
final String cityName;
WeatherRequest({required this.cityName});
}
Get Weather button, the WeatherRequest event is triggered.cityName. It's nothing but a string inputted by the user in the search text field.weather_state.dart classweather_state.dart :/*import statements*/
part of 'weather_bloc.dart';
@immutable
abstract class WeatherState {}
class WeatherInitial extends WeatherState {}
class WeatherLoadInprogress extends WeatherState {}
class WeatherLoadSuccess extends WeatherState {
final Weather weather;
WeatherLoadSuccess({required this.weather});
}
class WeatherLoadFailure extends WeatherState {
final String error;
WeatherLoadFailure({required this.error});
}
Events and States. We can now go to weather_bloc.dart and can start implement mapEventToStatemethod.weather_bloc.dart:class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
final _weatherRepository = WeatherRepository();
WeatherBloc() : super(WeatherInitial());
@override
Stream<WeatherState> mapEventToState(WeatherEvent event) async* {
if (event is WeatherRequest) {
yield WeatherLoadInprogress();
try {
final weatherResponse =
await _weatherRepository.getWeather(event.cityName);
yield WeatherLoadSuccess(weather: weatherResponse);
} catch (e) {
yield WeatherLoadFailure(error: e.toString());
}
}
}
}
if-else if-else. You can use switch-case too.Event is WeatherRequest, then we have to first of all pass the WeatherLoadInProgress state that tells the app that data is currently fetching.bloc data in UI?.weather data and display it on the screen. And also we have to bind the Get Weather button.
main.dart.class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => WeatherBloc(),
child: MaterialApp(
home: WeatherApp(),
),
);
}
}
BlocProvider.of<WeatherBloc>(context).lazy parameter. By default, it's true. It is used to lazily load the bloc. It means whenever anyone tries to use the bloc then it will be initialized.lazy to false.main.dart?BlocProvider(
BlocProvider(
BlocProvider(
.....

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(create: (context) => WeatherBloc(),),
BlocProvider(create: (context) => NetworkBloc(),),
BlocProvider(create: (context) => StorageBloc(),),
],
child: MaterialApp(
home: WeatherApp(),
),
);
}
}
Get Weather button.GetWeather button we want to show a Circular Progress Indicator instead of what we have previously.bodybody: BlocBuilder<WeatherBloc, WeatherState>(
builder: (context, state) {
return ...
}
)
bloc, and state. builder function is required which takes two parameters. context and the state which is of type WeatherState in our case. And it should return Widget in response.bloc in BlocBuilder by passing the bloc inside bloc property of BlocBuilderBlocBuilder<WeatherBloc, WeatherState>(
bloc: blocA, // provide the local bloc instance
builder: (context, state) {
return ...
}
)
buildWhen parameter takes the previous bloc state and current bloc state and returns a boolean. If buildWhen returns true, builder will be called with state and the widget will rebuild. If buildWhen returns false, builder will not be called with state and no rebuild will occur.BlocBuilder<WeatherBloc, WeatherState>(
buildWhen: (previousState, state) {
},
builder: (context, state) {
return ...
}
)
state parameter. Let's conditionally render the widget of our weather app based on different states. And call the WeatherRequest event on the Get Weather button press.BlocBuilder<WeatherBloc, WeatherState>(
builder: (context, state) {
if (state is WeatherLoadInprogress)
return Center(
child: CircularProgressIndicator(),
);
else if (state is WeatherLoadFailure)
return Center(
child: Text("Something went wrong"),
);
else if (state is WeatherLoadSuccess)
return WeatherWidget()
else
return Container();
},
),
);
}
}
context.read<WeatherBloc>().add(Event());
// or
BlocProvider.of<WeatherBloc>(context)add(Event())
listener, which is called only once per state, not including the initial state.bloc parameter. Only specify the bloc if you wish to provide a bloc that is otherwise not accessible via BlocProvider and the current BuildContext.listenWhen parameter is the same as BlocBuilder's buildWhen but for Listener.body: BlocListener<WeatherBloc, WeatherState>(
listener: (context, state) {
if (state is WeatherLoadInprogress) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Loading"),
),
);
}
},
child : BlocBuilder<WeatherBloc, WeatherState>(
builder: // ...
)
)

BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
child: BlocListener<BlocB, BlocBState>(
listener: (context, state) {},
child: BlocListener<BlocC, BlocCState>(
listener: (context, state) {},
child: ChildA(),
),
),
)
MultiBlocListener(
listeners: [
BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
),
BlocListener<BlocB, BlocBState>(
listener: (context, state) {},
),
BlocListener<BlocC, BlocCState>(
listener: (context, state) {},
),
],
child: ChildA(),
)
BlocConsumer<BlocA, BlocAState>(
listener: (context, state) {
if (state is WeatherLoadInprogress) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Loading"),
),
);
}
},
builder: (context, state) {
// return widget
}
)
listenWhen and buildWhen can be implemented for more granular control over when listener and builder are called.bloc to its children whereas RepositoryProvider provides repositories to its children.RepositoryProvider(
create: (context) => WeatherRepository(),
child: ChildWidget(),
);
context.read<WeatherRepository>();
// or
RepositoryProvider.of<WeatherRepository>(context)
MultiRepositoryProvider(
providers: [
RepositoryProvider<RepositoryA>(
create: (context) => RepositoryA(),
),
RepositoryProvider<RepositoryB>(
create: (context) => RepositoryB(),
),
RepositoryProvider<RepositoryC>(
create: (context) => RepositoryC(),
),
],
child: ChildA(),
)
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
WeatherBloc()..add(WeatherRequest(cityName: "Ahmedabad")),
child: MaterialApp(
home: WeatherApp(),
),
);
}
}
WeatherRequest when calling WeatherBloc, because we want the initial city to show. here I've provided a static city name. You can take the user's current location and provide it here to show the initial location of the user.body inside BlocConsumer and add WeatherRequest Event on button click to fetch new city weather: class WeatherApp extends StatelessWidget {
final _cityController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text("Weather App"),
centerTitle: true,
),
body: BlocConsumer<WeatherBloc, WeatherState>(
listener: (context, state) {
if (state is WeatherLoadInprogress) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Loading"),
),
);
}
},
builder: (context, state) {
if (state is WeatherLoadInprogress)
return Center(
child: CircularProgressIndicator(),
);
else if (state is WeatherLoadFailure)
return Center(
child: Text("Something went wrong"),
);
else if (state is WeatherLoadSuccess)
return Padding(
padding: EdgeInsets.symmetric(horizontal: 12.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextFormField(
controller: _cityController,
decoration: InputDecoration(
hintText: "Enter city name",
border: OutlineInputBorder(),
),
),
Column(
children: [
Image.asset(
"assets/images/sun.png",
height: 200,
width: 200,
),
SizedBox(
height: 20,
),
Text(
state.weather.name,
style: Theme.of(context).textTheme.headline3,
),
SizedBox(
height: 5,
),
Text(
state.weather.main["temp"].toString(),
style: Theme.of(context).textTheme.headline4!.copyWith(
fontWeight: FontWeight.bold, color: Colors.black),
),
],
),
ElevatedButton(
onPressed: () {
if (_cityController.text.isNotEmpty) {
context.read<WeatherBloc>().add(
WeatherRequest(
cityName: _cityController.text,
),
);
}
},
child: Text("Get Weather"),
)
],
),
);
else
return Container();
},
),
);
}
}


business_logic folder you can provide all your bloc.data folder we have data_provider where you provide different providers, model where you define your data model, and repositories folder where you define all the repositories.presentation folder you can have screen folder, pages, widgets folder where you implement UI.