Good architecture is very important in every good building as well as a good application. When building an application without proper architecture you will end up in a state where everything is messed up. If you don’t follow a proper architecture for your application you will lose control of your application like maintainability, test-ability, and scalability. In this guide, we will talk about Blocs. Bloc is not just a state management system, it’s also an architectural design pattern that helps you to build production-level applications.
Good architecture is very important in every good building as well as a good application. When building an application without proper architecture you will end up in a state where everything is messed up. If you don’t follow a proper architecture for your application you will lose control of your application like maintainability, test-ability, and scalability. In this guide, we will talk about Blocs. Bloc is not just a state management system, it’s also an architectural design pattern that helps you to build production-level applications.
There are many state management solutions and deciding which one to use can be a daunting task. There is no one perfect state management solution! What’s important is that you pick the one that works best for your team and your project.
Using the bloc library allows us to separate our application into three layers:
The presentation layer in Flutter is the layer that is responsible for rendering the UI and responding to user input. It is the layer that is closest to the user and is responsible for making the app look and feel good.
The presentation layer is made up of widgets, which are small, reusable components that can be combined to create complex interfaces. Widgets are responsible for drawing themselves on the screen and responding to user input.
The business logic layer is the bridge between the user interface (presentation layer) and the data layer. The business logic layer is notified of events/actions from the presentation layer and then communicates with the repository to build a new state for the presentation layer to consume.
class BusinessLogicComponent extends Bloc < MyEvent, MyState > {
BusinessLogicComponent(this.repository) {
on((event, emit) {
try {
final data = await repository.getAllDataThatMeetsRequirements();
emit(Success(data));
} catch (error) {
emit(Failure(error));
}
});
}
final Repository repository;
}
This layer is the lowest level of the application and interacts with databases, network requests, and other asynchronous data sources.
The data layer can be split into two parts:
These providers store data in memory. They are easy to use and fast, but they can only store a limited amount of data.
These providers store data in files. They can store a large amount of data, but they are not as fast as in-memory providers.
These providers store data in a database. They can store a very large amount of data and are very fast, but they can be more complex to use than other types of providers.
These providers fetch data from an external web service. They are easy to use and can be used to fetch data from any web service, but they can be slower than other types of providers.
class DataProvider {
Future readData() async { // Read from DB or make network request etc. }
}
This layer contains one or more than one Data Providers. Actually, 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 getAllDataThatMeetsRequirements() async {
final RawDataA dataSetA = await dataProviderA.readData();
final RawDataB dataSetB = await dataProviderB.readData();
final Data filteredData = _filterData(dataSetA, dataSetB);
return filteredData;
}
}
A stream is a sequence of data elements that are continuously generated or received over time.If you’re unfamiliar with Streams just think of a pipe with water flowing through it. The pipe is the Stream and the water is the asynchronous data.
Events are often used to trigger actions in a computer program. For example, when a user clicks a button in UI, an event is generated. This event can then be used to call a function or to change the state of the program.
The state is updated whenever an event is dispatched. The BLoC then emits the new state to all of its subscribers. This allows the subscribers to update their UI in response to changes in the state of the application.
In this tutorial, we are going to build a weather application (following) flutter bloc. Our weather app will pull live weather data from the public OpenMeteo API and demonstrate how to separate our application into layers (data, repository, business logic, and presentation).
Open the terminal and write the command
flutter create weather_flutter
Go to pubspec.yaml and add flutter_bloc, bloc, http, equatable,hydrated_bloc, and path_provider packages inside dependencies. Equatable overrides == and hashCode for you so you don’t have to waste your time writing lots of boilerplate code.
flutter_bloc: ^ 8.1 .3
http: ^ 0.13 .5
equatable: ^ 2.0 .5
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(WeatherApp(weatherRepository: WeatherRepositoryImpl(), ), );
}
class WeatherApp extends StatelessWidget {
final WeatherRepositoryImpl _weatherRepository;
const WeatherApp({
required WeatherRepositoryImpl weatherRepository,
super.key
}): _weatherRepository = weatherRepository;
@override
Widget build(BuildContext context) {
return RepositoryProvider.value(value: _weatherRepository, child: const WeatherAppView(), );
}
}
class WeatherAppView extends StatelessWidget {
const WeatherAppView({
super.key
});
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
return const MaterialApp(home: WeatherScreen(), );
}
}
class WeatherScreen extends StatelessWidget {
const WeatherScreen({
Key ? key
}): super(key: key);
@override
Widget build(BuildContext context) {
return BlocProvider(create: (context) => WeatherBloc(context.read()), child: const WeatherView(), );
}
}
class WeatherView extends StatefulWidget {
const WeatherView({
Key ? key
}): super(key: key);
@override
State createState() => _WeatherViewState();
}
class _WeatherViewState extends State {
Future getRefresh() async {}
@override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(title: const Text('Flutter Weather'),
actions: [
IconButton(icon: const Icon(Icons.search),
onPressed: () async {
var result = await Navigator.of(context).push(SearchScreen.route(), );
if (result != null && mounted) {
context.read().add(GetWeather(city: result));
}
}, ),
], ), body: SafeArea(child: Center(child: BlocConsumer < WeatherBloc, WeatherState > (listener: (context, state) {}, builder: (context, state) {
switch (state.status) {
case Status.initial:
return const WeatherEmpty();
case Status.failed:
return const WeatherError();
case Status.loading:
return const WeatherLoading();
case Status.success:
return WeatherPopulated(weather: state.weather!, units: state.temperatureUnits, onRefresh: () {
return getRefresh();
}, );
}
}, ), ), ), );
}
}
class WeatherEmpty extends StatelessWidget {
const WeatherEmpty({
super.key
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(mainAxisSize: MainAxisSize.min, children: [
const Text('Cityscape Emoji', style: TextStyle(fontSize: 64)),
Text('Please Select a City!', style: theme.textTheme.headlineSmall, ),
], );
}
}
class WeatherError extends StatelessWidget {
const WeatherError({
super.key
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(mainAxisSize: MainAxisSize.min, children: [
const Text('See No Evil Monkey Emoji', style: TextStyle(fontSize: 64)),
Text('Something went wrong!', style: theme.textTheme.headlineSmall, ),
], );
}
}
class WeatherLoading extends StatelessWidget {
const WeatherLoading({
super.key
});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(mainAxisSize: MainAxisSize.min, children: [
const Text('⛅', style: TextStyle(fontSize: 64)),
Text('Loading Weather', style: theme.textTheme.headlineSmall, ),
const Padding(padding: EdgeInsets.all(16), child: CircularProgressIndicator(), ),
], );
}
}
class WeatherPopulated extends StatelessWidget {
const WeatherPopulated({
required this.weather,
required this.units,
required this.onRefresh,
super.key,
});
final Weather weather;
final TemperatureUnits units;
final ValueGetter < Future < void >> onRefresh;
// final Function<void> onRefresh;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Stack(children: [
_WeatherBackground(),
RefreshIndicator(onRefresh: onRefresh, child: SingleChildScrollView(physics: const AlwaysScrollableScrollPhysics(),
clipBehavior: Clip.none,
child: Center(child: Column(children: [
const SizedBox(height: 48),
_WeatherIcon(condition: weather.condition),
Text(weather.location, style: theme.textTheme.displayMedium?.copyWith(fontWeight: FontWeight.w200, ), ),
Text(weather.formattedTemperature(units), style: theme.textTheme.displaySmall?.copyWith(fontWeight: FontWeight.bold, ), ),
// Text(
// '''Last Updated at ${TimeOfDay.fromDateTime(weather.).format(context)}''',
// ),
], ), ), ), ),
], );
}
}
class _WeatherIcon extends StatelessWidget {
const _WeatherIcon({
required this.condition
});
static
const _iconSize = 75.0;
final WeatherCondition condition;
@override
Widget build(BuildContext context) {
return Text(condition.toEmoji, style: const TextStyle(fontSize: _iconSize), );
}
}
extension on WeatherCondition {
String get toEmoji {
switch (this) {
case WeatherCondition.clear:
return 'Sun Emoji';
case WeatherCondition.rainy:
return 'Rainy Cloud Emoji';
case WeatherCondition.cloudy:
return 'Cloud Emoni';
case WeatherCondition.snowy:
return 'Cloud with Snow ';
case WeatherCondition.unknown:
return '❓';
}
}
}
class _WeatherBackground extends StatelessWidget {
@override
Widget build(BuildContext context) {
final color = Theme.of(context).primaryColor;
return SizedBox.expand(child: DecoratedBox(decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topCenter, end: Alignment.bottomCenter, stops: const [0.25, 0.75, 0.90, 1.0], colors: [
color,
color.brighten(),
color.brighten(33),
color.brighten(50),
], ), ), ), );
}
}
extension on Color {
Color brighten([int percent = 10]) {
assert(1 <= percent && percent <= 100, 'percentage must be between 1 and 100', );
final p = percent / 100;
return Color.fromARGB(alpha, red + ((255 - red) * p).round(), green + ((255 - green) * p).round(), blue + ((255 - blue) * p).round(), );
}
}
extension on Weather {
String formattedTemperature(TemperatureUnits units) {
return ''
'${temperature.toStringAsPrecision(2)}°${units.isCelsius ? '
C ' : '
F '}'
'';
}
}
class SearchScreen extends StatefulWidget {
const SearchScreen({
Key ? key
}): super(key: key);
static Route < String > route() {
return MaterialPageRoute(builder: (_) =>
const SearchScreen());
}
@override
State < SearchScreen > createState() => _SearchScreenState();
}
class _SearchScreenState extends State < SearchScreen > {
final TextEditingController _textController = TextEditingController();
String get _text => _textController.text;
@override
void dispose() {
_textController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(appBar: AppBar(title: const Text('City Search')), body: Row(children: [
Expanded(child: Padding(padding: const EdgeInsets.all(8),
child: TextField(controller: _textController, decoration: const InputDecoration(labelText: 'City', hintText: 'Chicago', ), ), ), ),
IconButton(key: const Key('searchPage_search_iconButton'),
icon: const Icon(Icons.search, semanticLabel: 'Submit'),
onPressed: () => Navigator.of(context).pop(_text), )
], ), );
}
}
Data: Retrieve raw weather data from the API
We’ll be focusing on two endpoints:
Open https://geocoding-api.open-meteo.com/v1/search?name=chicago&count=1 in your browser to see the response for the city of Chicago. We will use the latitude and longitude in the response to hit the weather endpoint.
The latitude/longitude for Chicago is 41.85003/-87.65005. Navigate to https://api.open-meteo.com/v1/forecast?latitude=43.0389&longitude=-87.90647¤t_weather=true in your browser and you’ll see the response for weather in Chicago which contains all the data we will need for our app.
The goal of our repository layer is to abstract our data layer and facilitate communication with the bloc layer. In doing this, the rest of our code base depends only on functions exposed by our repository layer instead of specific data provider implementations. This allows us to change data providers without disrupting any of the application-level code.
weather_repository.dart
Here we just declared the method
abstract class WeatherRepository {
Future getWeather(String city);
}
Now Implement the WeatherRepository abstract class on weather_repository_impl
class WeatherRepositoryImpl extends WeatherRepository {
final ApiClient _apiClient;
WeatherRepositoryImpl({
ApiClient ? apiClient
}): _apiClient = apiClient ?? ApiClient();
@override
Future < Weather > getWeather(String city) async {
final location = await _apiClient.locationSearch(city);
final weather = await _apiClient.getWeather(latitude: location.locationData!.first.latitude!, longitude: location.locationData!.first.longitude!, );
return Weather(temperature: weather.temperature!, location: location.locationData!.first.name!, condition: weather.weatherCode!.toInt().toCondition, );
}
}
extension on int {
WeatherCondition get toCondition {
switch (this) {
case 0:
return WeatherCondition.clear;
case 1:
case 2:
case 3:
case 45:
case 48:
return WeatherCondition.cloudy;
case 51:
case 53:
case 55:
case 56:
case 57:
case 61:
case 63:
case 65:
case 66:
case 67:
case 80:
case 81:
case 82:
case 95:
case 96:
case 99:
return WeatherCondition.rainy;
case 71:
case 73:
case 75:
case 77:
case 85:
case 86:
return WeatherCondition.snowy;
default:
return WeatherCondition.unknown;
}
}
}
We are using three model current_weather.dart, location.dart and weather.dart
class CurrentWeather {
double ? temperature;
double ? windSpeed;
String ? time;
int ? weatherCode;
CurrentWeather({
this.temperature,
this.windSpeed,
this.time
});
CurrentWeather.fromJson(Map < String, dynamic > json) {
temperature = json['temperature'];
windSpeed = json['windspeed'];
time = json['time'];
weatherCode = json['weathercode'];
}
}
class Location {
List < LocationData > ? locationData;
Location({
this.locationData
});
Location.fromJson(Map < String, dynamic > json) {
if (json['results'] != null) {
locationData = <LocationData>[];
json['results'].forEach((v) {
locationData!.add(LocationData.fromJson(v));
});
}
}
}
class LocationData {
int? id;
String? name;
double? latitude;
double? longitude;
LocationData({this.id, this.name, this.latitude, this.longitude});
LocationData.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
latitude = json['latitude'];
longitude = json['longitude'];
}
}
enum WeatherCondition {
clear,
rainy,
cloudy,
snowy,
unknown,
}
enum TemperatureUnits {
fahrenheit,
celsius
}
extension TemperatureUnitsX on TemperatureUnits {
bool get isFahrenheit => this == TemperatureUnits.fahrenheit;
bool get isCelsius => this == TemperatureUnits.celsius;
}
class Weather extends Equatable {
final String location;
final double temperature;
final WeatherCondition condition;
const Weather({
required this.location,
required this.temperature,
required this.condition,
});
@override
List < Object ? > get props => [location, temperature, condition];
}
Bloc: The weather_bloc.dart class is a bridge between our UI and the Data layer(weather_repository_impl.dart), In other words, this class will handle all the Events triggered by the User and send the relevant State back to the UI.
We are extending our WeatherBloc class with Bloc which takes two things WeatherEvent and WeatherState. As the name suggests, they handle the applications State and Events respectively.
These two classes are implemented in the weather_event.dart and weather_state.dart respectively.
enum WeatherCondition {
clear,
rainy,
cloudy,
snowy,
unknown,
}
enum TemperatureUnits {
fahrenheit,
celsius
}
extension TemperatureUnitsX on TemperatureUnits {
bool get isFahrenheit => this == TemperatureUnits.fahrenheit;
bool get isCelsius => this == TemperatureUnits.celsius;
}
class Weather extends Equatable {
final String location;
final double temperature;
final WeatherCondition condition;
const Weather({
required this.location,
required this.temperature,
required this.condition,
});
@override
List < Object ? > get props => [location, temperature, condition];
}
_getWeather(String? city) uses our weather repository to try and retrieve a weather object for the given city
abstract class WeatherEvent extends Equatable {
const WeatherEvent();
}
class GetWeather extends WeatherEvent {
final String city;
const GetWeather({
required this.city
});
@override
List < Object ? > get props => [];
}
enum Status {
loading,
success,
failed,
initial
}
class WeatherState extends Equatable {
final Status status;
final Weather ? weather;
final TemperatureUnits temperatureUnits;
const WeatherState({
this.status = Status.initial,
this.weather,
this.temperatureUnits = TemperatureUnits.celsius
});
WeatherState copyWith({
Status ? status,
Weather ? weather,
TemperatureUnits ? temperatureUnits
}) {
return WeatherState(status: status ?? this.status, weather: weather ?? this.weather, temperatureUnits: temperatureUnits ?? this.temperatureUnits);
}
@override
List < Object ? > get props => [status, weather, temperatureUnits];
}
In this class, we define only one state but if you want then you create a abstract class like this abstract class WeatherState {} and extend this class like
class WeatherInitial extends WeatherState {}
class WeatherLoadInprogress extends WeatherState {}
There are four states our weather app can be in:
The WeatherStatus enum will represent the above.
You need a context bloc and event that you trigger. In this WeatherBloc is our bloc and GetWeather is our event for getting weather and it needs a field city. We input the city dhaka.
context.read().add(GetWeather(City: "texas"));
Widget build(BuildContext context) {
return BlocProvider(create: (context) => WeatherBloc(context.read()), child: const WeatherView(), );
}
MultiBlocProvider(providers: [
BlocProvider(create: (BuildContext context) => BlocA(), ),
BlocProvider(create: (BuildContext context) => BlocB(), ),
BlocProvider(create: (BuildContext context) => BlocC(), ),
], child: ChildA(), )
BlocBuilder < BlocA, BlocAState > (builder: (context, state) {
// return widget here based on BlocA's state
})
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 < BlocA, BlocAState, SelectedState > (selector: (state) { // return selected state based on the provided state. }, builder: (context, state) { // return widget here based on the selected state. }, )
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 > (listenWhen: (previous, current) {
// return true/false to determine whether or not
// to invoke listener with state
}, listener: (context, state) {
// do stuff here based on BlocA's state
}, buildWhen: (previous, current) {
// 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
})
RepositoryProvider(create: (context) => RepositoryA(), child: ChildA(), );
// with extensions
context.read();
RepositoryProvider.of(context);
MultiRepositoryProvider(providers: [
RepositoryProvider(create: (context) => RepositoryA(), ),
RepositoryProvider(create: (context) => RepositoryB(), ),
RepositoryProvider(create: (context) => RepositoryC(), ),
], child: ChildA(), )
Tags : Flutter, Mobile app development, technology