How to build a bottom navigation bar in Flutter

Mobile applications often have various categories of content to offer. The Google Play Store app, for example, presents its content in categories such as games, apps, movies, and books. In Flutter apps, the BottomNavigationBar widget enables users to see any one category as the app starts and quickly look at the others with just the tap of a finger.

Flutter Logo

In this tutorial, we’ll tell you everything you need to know about BottomNavigationBar in Flutter. We’ll demonstrate how it works, walk through some use cases, and show you how to customize the BottomNavigationBar widget in your Flutter app.

Here’s what we’ll cover:

What is BottomNavigationBar in Flutter?

BottomNavigationBar is a widget that displays a row of small widgets at the bottom of a Flutter app. Usually, it’s used to show around three to five items. Each item must have a label and an icon. BottomNavigationBar allows you to select one item at a time and quickly navigate to a given page.

Now let’s walk through the process of creating a simple BottomNavigationBar step by step. The finished product will look as follows:

Finished Product

Showing BottomNavigationBar

The BottomNavigationBar widget is given to the bottomNavigationBarproperty of Scaffold:

Scaffold(
  appBar: AppBar(
    title: const Text('BottomNavigationBar Demo'),
  ),
  bottomNavigationBar: BottomNavigationBar(
    items: const <BottomNavigationBarItem>[
      BottomNavigationBarItem(
        icon: Icon(Icons.call),
        label: 'Calls',
      ),
      BottomNavigationBarItem(
        icon: Icon(Icons.camera),
        label: 'Camera',
      ),
      BottomNavigationBarItem(
        icon: Icon(Icons.chat),
        label: 'Chats',
      ),
    ],
  ),
);

BottomNavigationBar has a required property called itemsitems accept a widget of a type BottomNavigationBarItemBottomNavigationBarItem is simply used to show the actual item inside BottomNavigationBar.

The above code just displays the BottomNavigationBar with the first item selected as the default. It does not change the selection yet as we click on the other items:

First Item Selected

Showing a selection of items

To show the selection of other items, we’ll use two properties: onTap and currentIndex.

int _selectedIndex = 0; //New
BottomNavigationBar(
  items: const <BottomNavigationBarItem>[
    ...
  currentIndex: _selectedIndex, //New
  onTap: _onItemTapped,         //New
)
//New
void _onItemTapped(int index) {
  setState(() {
    _selectedIndex = index;
  });
}

The _selectedIndex variable holds the value of the currently selected item. _selectedIndex is given to the currentIndex property.

The _onItemTapped() callback is assigned to onTap of BottomNavigationBar, which returns the index when the item is tapped. Simply assigning a currently selected item index to _selectedIndex and doing setState will show the item as selected in BottomNavigationBar.

Selecting Item

Displaying the page of the selected item

As of now, we don’t have any page to show based on the selected item. So let’s go ahead and great it:

//New
static const List<Widget> _pages = <Widget>[
  Icon(
    Icons.call,
    size: 150,
  ),
  Icon(
    Icons.camera,
    size: 150,
  ),
  Icon(
    Icons.chat,
    size: 150,
  ),
];
Scaffold(
  appBar: AppBar(
    title: const Text('BottomNavigationBar Demo'),
  ),
  body: Center(
    child: _pages.elementAt(_selectedIndex), //New
  ),
  bottomNavigationBar: BottomNavigationBar(
    ...
  ),
);

_pages hold a list of widgets. For simplicity, we’re just showing a big icon of the item itself.

Showing one page in the center of the screen from _pages based on the _selectedIndex of the item will do the rest of the magic.

Now we have BottomNavigationBar up and running:

Bottom Navigation Working

The illustration below shows how the code translates into the design:

Code Translated to Design

Customizing the BottomNavigationBar

BottomNavigationBar has a lot of options to customize it per your need. Let’s zoom in on some of the properties you can customize.

Background color

You may want to change the background color of the BottomNavigationBar to match your brand. You do that simply by using the backgroundColor property.


Over 200k developers use LogRocket to create better digital experiences

Learn more →

BottomNavigationBar(
  backgroundColor: Colors.blueAccent,
  items: const <BottomNavigationBarItem>[
    ...
  ],
)

Blue Accent

Elevation

By default, the BottomNavigationBar is set to elevate 8 points from the surface so that it appears on top of pages. You can set this property to any value:

BottomNavigationBar(
  backgroundColor: Colors.white10,
  elevation: 0,
  items: const <BottomNavigationBarItem>[
   ...
  ],
)

Elevation Value

Icon size

You can shrink or magnify the size of all the icons at once using iconSize property:

BottomNavigationBar(
  iconSize: 40,
  items: const <BottomNavigationBarItem>[
    ...
  ],
)

Icon Size Updated

Mouse cursor

When running on the web, you can customize the mouse cursor when it hovers over an item on the BottomNavigationBar:

BottomNavigationBar(
  mouseCursor: SystemMouseCursors.grab,
  items: const <BottomNavigationBarItem>[
    ...
  ],
)

Mouse Hovering Over Icons

Selected item

You can make the selected item appear different from an unselected one using the several selected properties of BottomNavigationBar:

BottomNavigationBar(
  selectedFontSize: 20,
  selectedIconTheme: IconThemeData(color: Colors.amberAccent, size: 40),
  selectedItemColor: Colors.amberAccent,
  selectedLabelStyle: TextStyle(fontWeight: FontWeight.bold),
  items: const <BottomNavigationBarItem>[
    ...
  ],
)

Yellow Highlighted Icons

Unselected items

You may also want to change the look and feels of unselected items. BottomNavigationBar has a few unselected properties that you can use:

BottomNavigationBar(
  unselectedIconTheme: IconThemeData(
    color: Colors.deepOrangeAccent,
  ),
  unselectedItemColor: Colors.deepOrangeAccent,
  items: const <BottomNavigationBarItem>[
    ...
  ],
)

Different Icon Properties

Removing labels

If you want to get rid of the labels entirely, you can use showSelectedLabels and showUnselectedLabels:

BottomNavigationBar(
  iconSize: 40,
  showSelectedLabels: false,
  showUnselectedLabels: false,
  items: const <BottomNavigationBarItem>[
    ...
  ],
)

No Labels

Highlighting the selected item

You can emphasize the selected item by setting the BottomNavigationBar type to BottomNavigationBarType.shifting:

BottomNavigationBar(
  type: BottomNavigationBarType.shifting,
  selectedFontSize: 20,
  selectedIconTheme: IconThemeData(color: Colors.amberAccent),
  selectedItemColor: Colors.amberAccent,
  selectedLabelStyle: TextStyle(fontWeight: FontWeight.bold),
  items: const <BottomNavigationBarItem>[
    ...
  ],
)

Emphasized Select

How to preserve the state of pages

Although the basic version of BottomNavigationBar is working well, we have one problem: whatever action — e.g., searching, filtering, entering text, scrolling through a list, filling out a contact form, etc. — is being performed on the page will be lost upon selecting another item from the BottomNavigationBar:

State of Pages Being Lost

In the demo above, we’re trying to find a contact. When we switch to the camera section before we finish our search and then return to the chat section, the previously entered text is completely gone.

Fear not — the solution is pretty simple. Simply replace the existing widget with IndexedStack. The IndexedStack widget holds a stack of widgets but shows only one at a time. Since all the widgets stay in the stack, the state is preserved.

//Before
Center(
  child: _pages.elementAt(_selectedIndex),
)
//After
IndexedStack(
  index: _selectedIndex,
  children: _pages,
)

The index property is used to show one page from the _pages, which is given to the children property.

Index Property

How to include TabBar with BottomNavigationBar

Sometimes a single page is not enough to cover a wide range of subcategories within a parent category inside BottomNavigationBar. For example, the Google Play Store app has subcategories labeled For you, Top charts, Kids, etc. A scenario like this calls for the Flutter TabBar widget.

For demonstration purposes, let’s try to add TabBar for incoming, outgoing, and missed calls inside the calls section, as shown below:

Incoming, Outgoing, and Missed Calls Sections

The overall structure of BottomNavigationBar remains the same. You may need to create a separate class for the page in which you want to include a TabBar. For that purpose, the CallsPage is created and added to the list of pages.

static const List<Widget> _pages = <Widget>[
  CallsPage(),
  // Camera page
  // Chats page
];

The CallsPage looks like this:

DefaultTabController(
  length: 3,
  child: Scaffold(
    appBar: AppBar(
      flexibleSpace: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          TabBar(
            tabs: [
              Tab(
                text: 'Incoming',
              ),
              Tab(
                text: 'Outgoing',
              ),
              Tab(
                text: 'Missed',
              ),
            ],
          )
        ],
      ),
    ),
    body: TabBarView(
      children: [
        IncomingPage(),
        OutgoingPage(),
        MissedPage(),
      ],
    ),
  ),
);

Here’s the output:

Calls Page Icons

Hiding BottomNavigationBar on scroll

When building a Flutter app, you always want to utilize the screen space as efficiently as possible. When a user is scrolling through a long list of items on one of the pages in your app, you can hide the BottomNavigationBar smoothly. This behavior improves the user experience because you’re showing only content that is required at that moment.

As of now, the BottomNavigationBar stays as it is while scrolling through the list of outgoing calls:

Bottom Navigation Stays

Let’s walk through the process of hiding the BottomNavigationBar step by step.

First, wrap your list view inside the NotificationListener widget. NotificationListener listens to the scroll notification happening on the ListView.

NotificationListener<ScrollNotification>(
  onNotification: _handleScrollNotification,
  child: Scaffold(
    body: Center(
      child: ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          return ListTile(
            title: Text('${items[index]}'),
          );
        },
      ),
    ),
  ),
);

Next, implement the _handleScrollNotification method to determine the scroll direction. Notify the page that hosts the BottomNavigationBar to hide it when the user scrolls down.

bool _handleScrollNotification(ScrollNotification notification) {
  if (notification.depth == 0) {
    if (notification is UserScrollNotification) {
      final UserScrollNotification userScroll = notification;
      switch (userScroll.direction) {
        case ScrollDirection.forward:
          widget.isHideBottomNavBar(true);
          break;
        case ScrollDirection.reverse:
          widget.isHideBottomNavBar(false);
          break;
        case ScrollDirection.idle:
          break;
      }
    }
  }
  return false;
}

Wrap the BottomNavigationBar inside the SizeTransition widget. SizeTransition animates the size of BottomNavigationBar.

AnimationController animationController =
    AnimationController(vsync: this, duration: Duration(milliseconds: 300));
SizeTransition(
  sizeFactor: animationController,
  axisAlignment: -1.0,
  child: BottomNavigationBar(
    items: const <BottomNavigationBarItem>[
      ...
    ],
  ),
)

Start hiding animation on receiving the callback from the page that has the ListView.

CallsPage(
  isHideBottomNavBar: (isHideBottomNavBar) {
    isHideBottomNavBar
        ? animationController.forward()
        : animationController.reverse();
  },
)

Here is the result:

Scrolling Outgoing Calls

That’s it! The full code for this Flutter BottomNavigationBar example can be found on GitHub.

Conclusion

In this tutorial, we showed how to integrate BottomNavigationBar and customize it. We also learned various use cases with examples that you’ll likely encounter while developing a full-fledged Flutter app. I hope the practical examples we examined in this article helped you understand these important concepts.

Mastering Flutter App Architecture with Bloc

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