Mastering Flutter App Architecture with Bloc

Kiran S
by Kiran S 

When it comes to developing mobile applications, Flutter stands out as one of the most powerful and user-friendly frameworks available. It allows developers to create functional apps with ease. However, building an app, no matter how simple or complex, without a well-structured architecture is akin to constructing a house without a blueprint or plan – it’s bound to lead to chaos and confusion.

In the early stages of app development, you might not fully appreciate the importance of having a robust architecture. Small apps can often get away with disorganized code. However, as your project scales, incorporating proper architectural principles becomes critical. Picture this: a production-level application with numerous screens, animations, methods, classes, and other components. Without a well-defined architecture, it’s easy to lose track of how everything fits together and communicates.

This is where architectural patterns come into play. They provide the structure, organization, and guidelines necessary to maintain clean, readable, testable, and maintainable code. In the world of Flutter, one such architectural pattern that has gained significant popularity is the Bloc architecture.

What is Bloc?

Bloc, which stands for Business Logic Component, is more than just a tool for managing the state of an application. It’s actually an architectural design pattern that empowers developers to create strong, production-ready apps.

In the context of software development, business logic, or domain logic, refers to the part of the program that handles real-world business rules. These rules dictate how data can be created, stored, and modified, essentially defining the core functionality of the application.

Bloc Architecture Graphical Representation

Bloc architecture achieves a clear separation between the user interface (UI) and the business logic. In contrast to building an app without any architectural pattern, where you might find yourself writing logic directly within the UI components, Bloc encourages developers to isolate business logic in separate files. This separation makes it easier to manage, test, and comprehend the inner workings of a complex application.

Key Components of Bloc

The Bloc architecture comprises four primary layers, each with a specific role and responsibility:

1. UI (Presentation Layer):

This layer is where all the components and widgets that the user interacts with are defined. It includes everything that’s visible to the user, from buttons and forms to images and text.

2. Bloc (Business Logic Layer):

The Bloc layer acts as a mediator between the UI and the Data layer. It takes user-triggered events, such as button presses or form submissions, as input. Then, it processes these events, orchestrates the business logic, and responds to the UI with the relevant state changes.

3. Data Layer:

The Data layer is responsible for managing data sources, including databases, APIs, and local storage. It fetches, stores, and updates data according to the requirements of the business logic.

4. Repository Layer:

The Repository layer acts as a bridge between the Bloc and Data layers. It abstracts the data source interactions, providing a consistent and simplified API for the Bloc layer to access data. This abstraction allows you to switch between different data sources without affecting the business logic.

Event and State

In Bloc-based architecture, two crucial terms to understand are Event and State.

1. Event: 

An Event represents user actions, such as button clicks or form submissions, triggered in the UI. It encapsulates information about the action and delivers it to the Bloc for handling.

2. State:

 The UI updates based on the State it receives from the Bloc. Different states can represent various UI conditions, such as:

  • Loading State (displaying a progress indicator)
  • Loaded State (showing the actual widget with data)
  • Error State (indicating that something went wrong).

Bloc Pattern in Flutter - Event & State

Implementing Bloc:

Let us take an example of a project using the Weather App. Let’s integrate the bloc pattern inside it to illustrate how the bloc works on a project.

Step 1: Setting up your project

flutter create weather_app

Step 2: Installing Bloc

Go to your pubspec.yaml file and add the following dependencies.

flutter_bloc:
bloc:
equatable:

Step 3: Folder structure

The folder structure is like this:

weather_app/

|-- lib/

|   |-- bloc/

|   |   |-- weather_bloc/

|   |   |   |-- weather_bloc.dart

|   |   |   |-- weather_event.dart

|   |   |   |-- weather_state.dart

|   |-- data/

|   |   |-- repository/

|   |   |   |-- weather_repo.dart

|   |   |-- models/

|   |   |   |-- weather.dart

|   |-- presentation/

|   |   |-- constants/

|   |   |   |-- app_string.dart

|   |   |   |-- colors.dart

|   |   |   |-- image_assets.dart

|   |   |   |-- styles.dart

|   |   |-- screens/

|   |   |   |-- search_page.dart

|   |   |   |-- show_weather.dart

|   |   |-- widgets/

|   |   |   |-- column_data_widget.dart

|   |   |   |-- pwh.dart

|-- main.dart

Step 4: Setup Data layer

The data layer consists of the repository which calls an API to fetch the weather data based on city enter.

i) weather_repo.dart

class WeatherRepo {
  Future<WeatherModel> getWeather(String city) async {
   final result = await http.get(Uri.parse(weather_base_url));
   if (result.statusCode != 200) {
     throw Exception();
   }
   final response = json.decode(result.body);
   return WeatherModel.fromJson(response);
 }
}

ii) weather.dart

class WeatherModel {
 final dynamic temp;
 final dynamic icon;

 WeatherModel( {
     this.temp,
     this.icon,
   });

 factory WeatherModel.fromJson(Map<String, dynamic> json) {
   return WeatherModel(
     temp: json["main"]["temp"],
     icon: json["weather"][0]["icon"],
   );
 }
}

Step 5: Generate bloc files

i) weather_bloc.dart

class WeatherBloc extends Bloc<WeatherEvent, WeatherState> {
 final WeatherRepo weatherRepo;
 WeatherBloc({required this.weatherRepo}) : super(WeatherIsNotSearched()) {
   on<FetchWeather>(_onFetchWeather);
 }

 Future<void> _onFetchWeather(
   FetchWeather event,
   Emitter<WeatherState> emit,
 ) async {
   emit(WeatherIsLoading());
   try {
     final weather = await weatherRepo.getWeather(event.city);
     final city = event.city;

     emit(WeatherIsLoaded(weather, city));
   } catch (_) {
     emit(WeatherFailure());
   }
 }
}  

Let’s break down the provided code into points:

  • The WeatherBloc class is extending the Bloc class, and it’s designed to handle events of type WeatherEvent and manage states of type WeatherState.
  • In the constructor, it takes an instance of WeatherRepo as a required parameter. This repository likely handles data retrieval, such as fetching weather information.
  • Upon initialization of the WeatherBloc, it starts with an initial state of WeatherIsNotSearched().
  • The on method is used to listen to a specific event type, in this case, FetchWeather. When a FetchWeather event occurs, it will call the _onFetchWeather method to handle it.
  • Inside _onFetchWeather, the emit method is used to change the state to WeatherIsLoading(). This informs the UI that the app is currently in the process of fetching weather data.
  • It then attempts to fetch weather data from the weatherRepo by calling weatherRepo.getWeather(event.city) where event.city is the city for which the weather data is requested.
  • If the data is successfully retrieved, it sets the weather and city variables with the obtained data and the requested city.
  • It then updates the state to WeatherIsLoaded(weather, city). This indicates that the weather data has been successfully fetched and is ready for display in the UI.
  • If any errors occur during the data fetching process (caught by the catch block), it changes the state to WeatherFailure(). This state signifies that there was an issue while attempting to fetch the weather data.

ii) weather_state.dart

class WeatherState extends Equatable {
 const WeatherState();

 @override
 List<Object> get props => [];
}

class WeatherIsNotSearched extends WeatherState {}

class WeatherIsLoading extends WeatherState {}

class WeatherIsLoaded extends WeatherState {
 final dynamic _weather;
 final dynamic city;

 const WeatherIsLoaded(this._weather, this.city);

 WeatherModel get getWeather => _weather;

 @override
 List<Object> get props => [_weather,city];
}
class WeatherFailure extends WeatherState {}

Let’s break down each part:

  • WeatherState is a class that extends Equatable. It’s the base class for all the different states that the BLoC can have, and it’s used to compare state objects for equality.
  • WeatherIsNotSearched is a subclass of WeatherState. This state represents the initial state of the app, indicating that the user hasn’t searched for weather information yet. It’s used to set the BLoC’s initial state when the app starts.
  • WeatherIsLoading is another subclass of WeatherState. This state is used to indicate that the app is currently in the process of fetching weather data. It’s set when the BLoC is awaiting the response from a data source.
  • WeatherIsLoaded is a more complex subclass of WeatherState. This state represents that weather data has been successfully fetched. It holds two private variables, _weatherandcity`, which store the actual weather data and the city for which it was fetched, respectively.
    • The constructor takes _weather and city as parameters when it’s created.
    • There’s a getWeather method that allows you to access the weather data. It returns _weather.
  • WeatherFailure is yet another subclass of WeatherState. This state is used when there’s an error or failure in fetching weather data. It’s set if an exception occurs during the data retrieval process.

iii) weather_event.dart

class WeatherEvent extends Equatable {
 const WeatherEvent();

 @override
 List<Object> get props => [];
}
class FetchWeather extends WeatherEvent {
 final String city;
 const FetchWeather({required this.city});

  dynamic get getCity => city;

 @override
 List<Object> get props => [city];
}

class ResetWeather extends WeatherEvent {}

Let’s break down each part:

  • WeatherEvent is a base class that extends Equatable. It serves as the parent class for all the different events that can trigger state changes in the BLoC. Equatable is used to compare event objects for equality.
  • FetchWeather is a subclass of WeatherEvent. This event represents the action of requesting weather data for a specific city. It contains a city parameter in its constructor, indicating the city for which the weather data is being requested.
    • The constructor takes the city as a required parameter when creating the event.
    • There’s a getCity method that allows you to access the city parameter.
    • The props list in the FetchWeather class is overridden to include a city in the list, enabling the comparison of two FetchWeather events based on their cities.

Step 6: Adding an event to the bloc

To compute a bloc you have to add an event to the bloc. Such that there is logic included for each event is triggered.

child: ElevatedButton(
         style: buttonStyle,
         onPressed: () {
                if (_formKey.currentState!.validate()) {
                    _formKey.currentState!.save();
                     weatherBloc.add(FetchWeather(city: cityController!.text));
                 }
              },
         child: const Text(
                  AppString.search,
                ),
         );

Step 7: Access bloc data to UI

Having crafted the BLoC and integrated all the necessary features, the next step involves making this BLoC accessible within the widget tree. This is essential for us to access and utilize the weather data, enabling its display on the screen. We also need to establish a connection with the “Get Weather” button.

Prior to this, it’s crucial to grasp the various widgets offered by the BLoC library:

  • BlocProvider: This widget facilitates providing access to the BLoC to the widget tree.
  • BlocBuilder: It’s a widget that listens to the BLoC’s state changes and rebuilds the UI accordingly.
  • BlocListener: This widget enables us to listen to state changes in the BLoC and execute specific actions based on those changes.
  • BlocConsumer: Similar to BlocBuilder, it also observes state changes in the BLoC but provides methods for both building and listening.
  • RepositoryProvider: A widget for providing repositories to the widget tree, allowing BLoCs to access data sources effectively.

i) Bloc Provider

BlocProvider is a widget that supplies a BLoC to its child widgets.

  • It’s used for dependency injection, ensuring the same BLoC instance is available to various widgets.
  • Place BlocProvider where all child widgets need access to the BLoC. In the case of a Flutter app, this often means wrapping it around the MaterialApp.
  • This enables us to access the BLoC using ** BlocProvider.of(context) ** .
  • By default, BlocProvider initializes the BLoC lazily, meaning it’s created when someone tries to use it. To change this, set the lazy parameter to false.
  • If you have multiple BLoCs, nest them inside one another for a hierarchical structure.
class MyApp extends StatelessWidget {
 const MyApp({Key? key}): super(key: key);

 @override
 Widget build(BuildContext context) {
   return BlocProvider(
     create: (context) => WeatherBloc(
       weatherRepo: WeatherRepo(),
     ),
     child: MaterialApp(
       debugShowCheckedModeBanner: false
       home: const WeatherPage(),
     ),
   );
 }
}

ii) Bloc Builder

  • BlocBuilder serves as a widget that facilitates updating the user interface in response to changes in the app’s state.
  • In our scenario, we aim to have the UI react to the user’s action of pressing the “Get Weather” button.
  • BlocBuilder automatically reconstructs the UI each time the state undergoes a change.
  • It’s crucial to position BlocBuilder around the specific widget that you want to refresh when the state changes.
  • While it’s possible to wrap the entire widget tree with BlocBuilder, this isn’t efficient. Imagine the processing resources and time required to rebuild the entire widget structure just to update a single Text widget.
  • Therefore, it’s advisable to enclose BlocBuilder around the widget that needs to be updated when the state changes.
  • In our case, the entire page should be updated because, when the user triggers the “Get Weather” button, we want to display a Circular Progress Indicator in place of the previous content.
  • As a result, we should incorporate BlocBuilder within the body of the widget.
BlocBuilder<WeatherBloc, WeatherState>(
             builder: (context, state) {
             if (state is WeatherIsLoading) {
                 return const Center(
                   child: CircularProgressIndicator());
               } else if (state is WeatherIsLoaded) {
                 return ShowWeather(weather:state.getWeather,city:state.city);
               } else {
                 return const Center(
                   child: Text("City not found"),
                 );
               }
             },
           );

iii) Bloc Listeners

  • BlocListener, as the name suggests, monitors state changes, much like BlocBuilder.
  • However, unlike BlocBuilder, it doesn’t construct the widget itself. Instead, it takes a single function called a “listener” which executes only once for each state change, excluding the initial state.
  • Its purpose is for actions like navigation, displaying a Snackbar, or showing a dialog.
  • It also includes a “bloc” parameter, which is only needed if you want to provide a BLoC that isn’t accessible through BlocProvider in the current context.
  • BlocListener’s “listenWhen” parameter works similarly to BlocBuilder’s “buildWhen.”
  • The primary role of BlocListener is not to build or update widgets, unlike BlocBuilder. It’s solely responsible for observing state changes and performing specific operations. These operations might include actions like navigating to other screens when the state changes or displaying a Snackbar in response to a particular state.
  • For instance, if you want to show a Snackbar when the app is in the “WeatherLoadInProgress” state, you can wrap the relevant content within BlocListener.
BlocListener<WeatherBloc, WeatherState>(listener: ((context, state) {
             if (state is WeatherFailure) {
               ScaffoldMessenger.of(context).showSnackBar(
                 const SnackBar(
                   content: Text("Something went wrong"),
                 ),
               );
             }
           })),

iii) Bloc Consumers

  • Currently, we’re utilizing BlocBuilder to create widgets and BlocListener to display Snack bars.
  • Is there a simpler way to merge these functionalities into a single widget? Absolutely!
  • BlocConsumer offers a solution that blends both BlocListener and BlocBuilder into one.
  • Instead of separately implementing BlocListener and BlocBuilder, we can now achieve this combination.
BlocConsumer<WeatherBloc, WeatherState>(
             listener: ((context, state) {
               if (state is WeatherFailure) {
                 ScaffoldMessenger.of(context).showSnackBar(
                   const SnackBar(
                     content: Text("Something went wrong"),
                   ),
                 );
               }
             }),
             builder: (context, state) {
               if (state is WeatherIsNotSearched) {
                 return weatherNotSearched(weatherBloc);
               } else if (state is WeatherIsLoading) {
                 return const Center(
                   child: CircularProgressIndicator(),
                 );
               } else if (state is WeatherIsLoaded) {
                 return ShowWeather(weather: state.getWeather, city: state.city);
               } else {
                 return const Center(
                   child: Text("City not found"),
                 );
               }
             },
           )

Benefits of using the Bloc architecture 

  • Separation of concerns: 

Bloc architecture separates the UI layer from the business logic layer. This makes the code more modular, reusable, and testable.

  • State management: 

Bloc architecture provides a centralized way to manage the state of the app. This makes it easier to reason about the app’s behavior and ensure that the UI is always in sync with the state.

  • Predictable behaviour: 

Bloc architecture makes the app’s behavior more predictable. This is because the UI is only updated when the state changes and the state is only changed in response to events.

  • Performance: 

Bloc architecture can improve the performance of the app by reducing the number of unnecessary rebuilds. This is because the UI is only rebuilt when the state changes and the state is only changed in response to events.

  • Testability: 

Bloc architecture makes the app more testable. This is because the business logic is isolated in the Bloc layer, which makes it easier to write unit tests.

Drawback:

  • Complexity:

 The Bloc architecture can be difficult to learn for developers who are new to Flutter or state management patterns. It requires an understanding of Reactive Programming and can take time to master.

  • Boilerplate Code: 

Setting up Bloc may require writing some boilerplate code, which can be seen as a drawback for small projects.

Overall, Bloc architecture is a powerful and flexible design pattern that can be used to build scalable and maintainable Flutter apps. It provides a number of benefits, including separation of concerns, state management, predictable behavior, performance, and testability.

Read more about Bloc Here

.………………………………………

From concept to deployment, Gurzu provides an end-to-end mobile development solution. Our application development prioritizes intuitive interfaces, seamless user experiences, and robust functionality. 

Are you looking to empower your business through user-centric mobile solution? Reach out to us for a free consulting call!

This article is based on a talk by Kiran about Bloc Pattern. Get the slide deck here.

You might also want to read “Adding Chat to a React Native App with Firebase” here