Learn what is BloC Cubit by building a weather app

By Faheem Ahmed - January 05,2024
Learn What Is BloC Cubit By Building A Weather App

Introduction

BLoC stands for Business Logic Component, It is a state management solution for Flutter which helps separate the business logic and presentation layers of an application. It's a part of the flutter_bloc package.

BLoC-Cubit has two main components:

  1. BLoC: It holds the business logic of the application and manages the state . It receives events from the user interface and processes them, updating the state accordingly. It can be seen as a middle layer between the UI and the data layer.
  2. Cubit: It is a simplified version of BLoC that reduces the boilerplate code needed for state management. We mostly use this when the business logic is simple. It handles events and updates the state accordingly, without the need for multiple streams.

Reactive and Event-Driven: BloC uses streams to manage states, making it reactive and event-driven.

Separation of Concerns: Using this pattern helps with better separation of UI and functionality.

Testability: With clear separation of concerns in this pattern, testing is much easier and therefore you can test business logic independent of the UI.

Code Reusability: The business logic components can be used across different parts of the projects.

UI Responsiveness: In this pattern, the UI components can listen to changes in the state and behave based on these updates accordingly, which makes the UI and state work in sync.

Let’s understand with a example

We will create a simple weather application in Flutter using BloC Cubit, that shows a text field where you type your city and the press enter, and the current temperature of your location will be shown along with the other details.

Now that you know what we are going to build, I want you to focus and understand each step as this will help you understand how state and cubit work together to update the UI.

Things You Need

  • Basic Understanding of Flutter Framework
  • IDE like VSCODE or Android Studio
  • Mobile device for testing

Getting Started

Set up your Flutter project:

First, make sure you have Flutter and Dart installed on your machine. If not, you can follow the official Flutter installation guide here: https://docs.flutter.dev/get-started/install

Create a new Flutter project using the following command in your VS code Terminal: flutter create flutter_weather_app_bloc_ cubit

In “main.dart” file, remove old code and replace with the below code:

Define the structure of your project

This structure will help us implement the BloC pattern for state management.

In main.dart remove old code and replace with the following:

				
					void main() {
 runApp(const MyApp());
}

class MyApp extends StatelessWidget {
 const MyApp({super.key});

 @override
 Widget build(BuildContext context) {
   return MaterialApp(
     home: BlocProvider(
       create: (context) => WeatherCubit(),
       child: MyHomePage(),
     ),
   );
 }
}
				
			

Here, BlocProvider is providing a Bloc to its children via BuildContext.

It is used as a dependency injection (DI) widget so that a single instance of a bloc can be provided to multiple widgets within a subtree.

And in our code, BlocProvider is creating an instance of WeatherCubit and providing it to MyHomePage widget. By doing this MyHomePage and its children can access the WeatherCubit instance and react to its state changes.

Getting API key from open weather

Get your API key by logging into https://home.openweathermap. org/api , and generate an API Key.

Adding key to project

Make a new file named api_services.dart and save copied key in a string as well as the api URL so we can call this key and the url for API anywhere in our project.

Adding required packages to project

Let’s first add some required packages to our project:

				
					 equatable: ^2.0.5
				
			
				
					 flutter_bloc: ^8.1.3
 http: ^1.1.2
				
			

You may need to update these packages to the latest versions, as these packages are regularly updated. Find these packages on pub.dev

Making a model for the API consumption

In order to make the model easily, we first need a response from API and based on that response we can generate a model. So, get the response, goto PostMan and type your full API GET URL as follows:

				
					https://api.openweathermap.org/data/2.5/weather?q=rawalpindi&appid=81d30d113e22d1e27faf2999efddfe0c
				
			

Here you have to change the key to your own key and the city to your own city.


You will get a response like this:

Now it’s time to copy this and goto the following website:

https://app.quicktype.io/

Paste this JSON data and select dart, A model will be generated for you to use in you project.

Now copy this model to your flutter Project( you can find this full code in our repo in the link provided)

Defining weather cubit

				
					Class WeatherCubit extends Cubit<WeatherState> {
 WeatherCubit() : super(WeatherState.initial());

 WeatherModel weatherData = WeatherModel();

 void getWeather(String city) async {
   emit(WeatherState.loading());

   final apiUrl = '$url$city&appid=$apiKey';

   try {
     final response = await http.get(Uri.parse(apiUrl));
     if (response.statusCode == 200) {
       final weatherData = response.body;
       debugPrint('weatherData: $weatherData');
       weatherData = WeatherModel.fromJson(jsonDecode(weatherData));
       emit(WeatherState.success(weatherData));
     } else {
       debugPrint('Error weatherData: ${response.body}');
       emit(WeatherState.error('Failed to fetch weather data'));
     }
   } catch (e) {
     emit(WeatherState.error('$e'));
   }
 }
}
				
			

Now let’s understand this code:

The WeatherCubit is a class extending Cubit with the state of WeatherState and WeatherState.initial() initializes the state.

Then, we create an instance of WeatherModel named weatherData and create a method getWeather for fetching the weather data for a given city. It first emits a loading state and creates a string named apiURL that has a full url for api including the saved key and url.

Now,If the request is successful, It Parses the JSON response into a WeatherModel object and assigns it to weatherData string and emits a success state with the fetched weather data, otherwise it emits an error state with a failure message.

Defining weather state

				
					import 'package:equatable/equatable.dart';

enum WeatherStatus { initial, loading, success, error }

class WeatherState extends Equatable {
 final String weatherData;
 final WeatherStatus status;
 final String error;

 const WeatherState({
   required this.weatherData,
   required this.status,
   required this.error,
 });

 factory WeatherState.initial() {
   return const WeatherState(
     weatherData: '',
     status: WeatherStatus.initial,
     error: '',
   );
 }

 factory WeatherState.loading() {
   return const WeatherState(
     weatherData: '',
     status: WeatherStatus.loading,
     error: '',
   );
 }

 factory WeatherState.success(String data) {
   return WeatherState(
     weatherData: data,
     status: WeatherStatus.success,
     error: '',
   );
 }

 factory WeatherState.error(String errorMsg) {
   return WeatherState(
     weatherData: '',
     status: WeatherStatus.error,
     error: errorMsg,
   );
 }

 @override
 List<Object?> get props => [weatherData, status, error];
}
				
			

The WeatherState class extends Equatable which allows value comparison.

We have an enum WeatherStatus with four values: (initial, loading, success, and error) and four factory methods to create instances of the class in different states.

And finally, Props getter is overridden to return a list of the properties, which Equatable uses to perform value comparisons.

Defining User Interface

				
					class MyHomePage extends StatelessWidget {
 final TextEditingController _cityController = TextEditingController();

 MyHomePage({super.key});

 @override
 Widget build(BuildContext context) {
   final weatherCubit = context.read<WeatherCubit>();
   return Scaffold(
     appBar: AppBar(
       title: const Text('Weather App with Cubit'),
     ),
body: Center(
    child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
        children: [
        const Text('Enter City:'),
        const SizedBox(height: 10),
        Padding(
        padding: const EdgeInsets.all(40.0),
    child: TextField(
               controller: _cityController,
    decoration: const InputDecoration(
                 hintText: 'City Name',
               ),
             ),
           ),
    const SizedBox(height: 20),
    ElevatedButton(
             onPressed: () {
    final city = _cityController.text;
    if (city.isNotEmpty) {
                 weatherCubit.getWeather(city);
               }
             },
    child: const Text('Get Weather'),
           ),
    const SizedBox(height: 20),
    BlocBuilder<WeatherCubit, WeatherState>(
    builder: (context, state) {
    if (state.status == WeatherStatus.loading) {
    return const CircularProgressIndicator();
    } else if (state.error.isNotEmpty) {
    return Text(
    'Error: ${state.error}',
     style: const TextStyle(color: Colors.red),
                 );
               } else {
                 return Column(
    children: <Widget>[
    Text(
    'City: ${weatherCubit.weartherData.name ?? 'N/A'}',
    style: const TextStyle(fontSize: 16),
                     ),
Text(
'Temperature: ${(weatherCubit.weartherData.main?.tempMin != null ?
(weatherCubit.weartherData.main!.tempMin! - 273.15).toStringAsFixed(2) : 'N/A')} °C',
style: const TextStyle(fontSize: 16),
                     ),
weatherCubit.weartherData.weather != null &&
weatherCubit.weartherData.weather!.isNotEmpty
                         ? Image.network(
'http://openweathermap.org/img/w/${weatherCubit.weartherData.weather?[0].icon}.png',
                             width: 100,
                             height: 100,
                           )
                         : Container(
                             width: 100,
                             height: 100,
                             color: Colors.grey,
                           ),
                   ],
                 );
               }
             },
           ),
         ],
       ),
     ),
   );
 }
}
				
			

Let’s understand the code. We have a text field for city input, a button to fetch weather data, and a BlocBuilder to build the UI based on the WeatherState.

The ElevatedButton's onPressed callback gets the city name from _cityController and calls getWeather method of weatherCubit if the city name is not empty.

The BlocBuilder rebuilds the UI based on the WeatherState. If the state is loading, it shows a CircularProgressIndicator. If there's an error, it shows the error message. Otherwise, it shows the weather data, including the city name, temperature, and an image representing the weather condition.

Also the temperature received as a response is in Kelvin, so we converted it to Celsius by subtracting 273.15.

And finally, the weather image is fetched from
'http://openweathermap.org/ img/w/${weatherCubit.wearthe rData.weather?[0].icon}.png'. If the weather data is null or empty, it shows a simple gray container.

UI Screenshots


Conclusion

Today you learned how easy it is to build your very own Flutter Application that has AI powers and generates a recipe for you based on the items you provide to it.

You first started out with a default flutter project, then you learned how to build a User Interface that is simple and clear.

You learned how to get the API key and learn using that API key how you can send a prompt to OpenAI and get a response in JSON and then made a model that helps with parsing data and how to display the response to the screen.

If this article has helped in any way and you have learned something new today, Kindly consider sharing the article so others can also benefit and learn how to build simple AI applications.

Subscribe to our website for more interesting articles on AI and other interesting fields of IT.

You can find the code for this here:

Repository Link:

Written by
Faheem Ahmed

wpChatIcon
wpChatIcon