We are able to divide our application into three layers by using the bloc library:
- Presentation
- Business Logic
- Data
- Repository
- Data Provider
Data layer
This layer is the lowest level of the application, interacting with databases, network requests, and other asynchronous data sources.
There are two components to the data layer:
- Data Provider
The data provider typically exposes simple APIs to perform CRUD operations. Our data layer may have the createData, readData, updateData, and deleteData methods.
class DataProvider {
Future<RawData> readData() async {
// Read from DB or make network request etc...
}
}
Repository
The repository layer is a wrapper around one or more data providers with which the Bloc Layer communicates.
class Repository {
final DataProviderA dataProviderA;
final DataProviderB dataProviderB;
Future<Data> getAllDataThatMeetsRequirements() async {
final RawDataA dataSetA = await dataProviderA.readData();
final RawDataB dataSetB = await dataProviderB.readData();
final Data filteredData = _filterData(dataSetA, dataSetB);
return filteredData;
}
}
Business Logic Layer
The business logic layer’s responsibility is to respond to input from the presentation layer with new states.
class BusinessLogicComponent extends Bloc<MyEvent, MyState> {
BusinessLogicComponent(this.repository) {
on<AppStarted>((event, emit) {
try {
final data = await repository.getAllDataThatMeetsRequirements();
emit(Success(data));
} catch (error) {
emit(Failure(error));
}
});
}
final Repository repository;
}
Presentation Layer
It is responsible for ui change based on the state changes.
Bloc Concepts
Bloc Widgets
BlocBuilder
It is a Flutter widget that requires bloc and a builder function. It builds a widget in response to a state change.
BlocBuilder<BlocA, BlocAState>(
builder: (context, state) {
// return widget here based on BlocA's state
},
);
To provide a bloc that will be only accessible in a particular widget and is not accessible via parent BlocProvider and BuildContext.
BlocBuilder<BlocA, BlocAState>(
bloc: blocA, // provide the local bloc instance
builder: (context, state) {
// return widget here based on BlocA's state
},
);
For controlling when to build, it provides buildWhen.It takes in pthe revious and current states and returns boolean .If it returns true ,then the builder will be called.
BlocBuilder<BlocA, BlocAState>(
buildWhen: (previousState, state) {
// return true/false to determine whether or not
// to rebuild the widget with state
},
builder: (context, state) {
// return widget here based on BlocA's state
},
);
BlocSelector
It is similar to BlocBuilder, but it allows you to filter updates by selecting a new value based on the current bloc state. It is used to reduce the number of rebuilds.
BlocSelector<BlocA, BlocAState, SelectedState>(
selector: (state) {
// return selected state based on the provided state.
},
builder: (context, state) {
// return widget here based on the selected state.
},
);
BlocProvider
It is a widget which provides bloc to its children via BlocProvider.of<T>(context).It is used as dependency injection so that a single instance of bloc can be provided to multiple widget within same subtree.
In most cases, BlocProvider should be used to create new blocs which will be made available to the rest of the subtree. In this instance, BlocProvider will take care of closing the bloc automatically because it is in charge of creating it.
BlocProvider(
create: (BuildContext context) => BlocA(),
child: ChildA(),
);
BlocProvider will by default generate the bloc lazily, which means that when the bloc is sought up using BlocProvider.of(context), create will be invoked. To get around this:
BlocProvider(
lazy: false,
create: (BuildContext context) => BlocA(),
child: ChildA(),
);
In certain situations, an existing bloc can be sent to a new section of the widget tree using BlocProvider. In those cases, it will not be auto-closed.
BlocProvider.value(
value: BlocProvider.of<BlocA>(context),
child: ScreenA(),
);
Then, from screenA or childA, we can retrieve BlocA with
// with extensions
context.read<BlocA>();
// without extensions
BlocProvider.of<BlocA>(context);
MultiBlocProvider
Several BlocProvider widgets can be combined into a single Flutter widget.
MultiBlocProvider(
providers: [
BlocProvider<BlocA>(
create: (BuildContext context) => BlocA(),
),
BlocProvider<BlocB>(
create: (BuildContext context) => BlocB(),
),
BlocProvider<BlocC>(
create: (BuildContext context) => BlocC(),
),
],
child: ChildA(),
);
BlocListener
BlocListener is a Flutter widget that takes a BlocWidgetListener and an optional Bloc and invokes the listener in response to state changes in the bloc.
Listener is only called once for each state change (NOT including the initial state) unlike builder in BlocBuilder, and is a void function.
BlocListener<BlocA, BlocAState>(
listener: (context, state) {
// do stuff here based on BlocA's state
},
child: const SizedBox(),
);
To provide a bloc that will be only accessible in a particular widget and is not accessible via parent BlocProvider and BuildContext.
BlocListener<BlocA, BlocAState>(
bloc: blocA,
listener: (context, state) {
// do stuff here based on BlocA's state
},
child: const SizedBox(),
);
For controlling when to build, it provides listenWhen.It takes in the previous and current states and returns a boolean. If it returns true, then the listener will be called.
BlocListener<BlocA, BlocAState>(
listenWhen: (previousState, state) {
// return true/false to determine whether or not
// to call listener with state
},
listener: (context, state) {
// do stuff here based on BlocA's state
},
child: const SizedBox(),
);
MultiBlocListener
Flutter widget that merges multiple BlocListener widgets into one.
MultiBlocListener(
listeners: [
BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
),
BlocListener<BlocB, BlocBState>(
listener: (context, state) {},
),
BlocListener<BlocC, BlocCState>(
listener: (context, state) {},
),
],
child: ChildA(),
);
BlocConsumer
It is a combination of both BlocBuilder and BlocListener.It should only be used when both UI rebuild and executing a function are required on bloc state change. It takes all the parameters taken by both BlocBuilder and BlocListener, like buildWhen,listenWhen, bloc, etc.
RepositoryProvider
A Flutter widget that provides a repository to its children via RepositoryProvider.of<T>(context).It is used as a dependency injection widget to provide a single instance of the repository to the entire subtree.
While RepositoryProvider should only be used for repositories, BlocProvider should be used to offer blocs.
RepositoryProvider(
create: (context) => RepositoryA(),
child: ChildA(),
);
The repository can then be obtained from Child A:
// with extensions
context.read<RepositoryA>();
// without extensions
RepositoryProvider.of<RepositoryA>(context)
MultiRepositoryProvider
Flutter widget that merges multiple RepositoryProvider widgets
MultiRepositoryProvider(
providers: [
RepositoryProvider<RepositoryA>(
create: (context) => RepositoryA(),
),
RepositoryProvider<RepositoryB>(
create: (context) => RepositoryB(),
),
RepositoryProvider<RepositoryC>(
create: (context) => RepositoryC(),
),
],
child: ChildA(),
);
BlocProvider Usage
First, we need to create a bloc. Here, I am creating the counter bloc :
sealed class CounterEvent {}
final class CounterIncrementPressed extends CounterEvent {}
final class CounterDecrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
on<CounterDecrementPressed>((event, emit) => emit(state - 1));
}
}
Now to supply this bloc to counterPage, we can write the provider as follows:
void main() => runApp(CounterApp());
class CounterApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: BlocProvider(
create: (_) => CounterBloc(),
child: CounterPage(),
),
);
}
}
Now, to make the CounterPage to react to state changes :
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Counter')),
body: BlocBuilder<CounterBloc, int>(
builder: (context, count) {
return Center(
child: Text(
'$count',
style: TextStyle(fontSize: 24.0),
),
);
},
),
floatingActionButton: Column(
crossAxisAlignment: CrossAxisAlignment.end,
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () => context.read<CounterBloc>().add(CounterIncrementPressed()),
),
),
Padding(
padding: EdgeInsets.symmetric(vertical: 5.0),
child: FloatingActionButton(
child: Icon(Icons.remove),
onPressed: () => context.read<CounterBloc>().add(CounterDecrementPressed()),
),
),
],
),
);
}
}
RepositoryProvider Usage
First, we need to create a repository :
class WeatherRepository {
WeatherRepository({
WeatherApiClient? weatherApiClient
}) : _weatherApiClient = weatherApiClient ?? WeatherApiClient();
final WeatherApiClient _weatherApiClient;
Future<Weather> getWeather(String city) async {
final location = await _weatherApiClient.locationSearch(city);
final woeid = location.woeid;
final weather = await _weatherApiClient.getWeather(woeid);
return Weather(
temperature: weather.theTemp,
location: location.title,
condition: weather.weatherStateAbbr.toCondition,
);
}
}
Now we will pass our WeatherRepository instance to our app through the constructor :
import 'package:flutter/material.dart';
import 'package:flutter_weather/app.dart';
import 'package:weather_repository/weather_repository.dart';
void main() {
runApp(WeatherApp(weatherRepository: WeatherRepository()));
}
Now, to inject this repository into our widget tree, we can use RepositoryProvider or MultiRepositoryProvider(in case of multiple repositories) :
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:weather_repository/weather_repository.dart';
class WeatherApp extends StatelessWidget {
const WeatherApp({Key? key, required WeatherRepository weatherRepository})
: _weatherRepository = weatherRepository,
super(key: key);
final WeatherRepository _weatherRepository;
@override
Widget build(BuildContext context) {
return RepositoryProvider.value(
value: _weatherRepository,
child: BlocProvider(
create: (_) => ThemeCubit(),
child: WeatherAppView(),
),
);
}
}
Now to expose our repositories to a cubit/bloc :
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_weather/weather/weather.dart';
import 'package:weather_repository/weather_repository.dart';
class WeatherPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => WeatherCubit(context.read<WeatherRepository>()),
child: WeatherView(),
);
}
}
Read More: Flutter State Management with BLoC Explained