Hello everyone,
In this blog, I wanted to share the English version of my blog discussing how to create an effective state management structure using BloC and Freezed.
Let’s Get Started!
In the screenshot above, I will demonstrate the BloC and Freezed structure by writing the application.
First, let’s add the necessary packages to our pubspec.yaml file.
The packages we need to add under _dependencies_
are flutter_bloc and freezed_annotation. The packages we need to add under _dev_dependencies_
are build_runner, freezed, and json_serializable. Also, don’t forget to add the dio package under dependencies for our API requests.
To generate code on the Freezed side, I am installing the build runner extension for VS Code. Of course, you can run it directly in the script as well.
flutter pub run build_runner build
The structure of my project is as follows:
How to Create a Model with Freezed?
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:flutter/foundation.dart';
part 'coin_model.freezed.dart';
part 'coin_model.g.dart';
@freezed
class CoinModel with _$CoinModel {
const factory CoinModel({
required final String id,
required final String rank,
required final String symbol,
required final String name,
required final String supply,
final String? maxSupply,
required final String marketCapUsd,
required final String volumeUsd24Hr,
required final String priceUsd,
required final String changePercent24Hr,
final String? vwap24Hr,
}) = _CoinModel;
factory CoinModel.fromJson(Map<String, dynamic> json) =>
_$CoinModelFromJson(json);
}
By wrapping this model with Freezed, we’ve added some basic features. We also added the JsonSerialize methods this way. The critical part of creating this model is running the build runner after adding the paths, which will generate two new files in the same directory.
Let’s Write a Simple API Service
import 'package:coin_example/core/model/coin_model.dart';
import 'package:dio/dio.dart';
abstract class ICoinService {
final Dio dio;
ICoinService(this.dio);
Future<List<CoinModel>> fetchCoins();
}
class CoinService extends ICoinService {
CoinService({required Dio dio}) : super(dio);
@override
Future<List<CoinModel>> fetchCoins() async {
final response = await dio.get('${Api.baseUrl}${Api.assets}');
final data = response.data['data'];
return data.map<CoinModel>((json) => CoinModel.fromJson(json)).toList();
}
}
enum Api {
baseUrl,
assets,
}
extension ApiExtension on Api {
String get url {
switch (this) {
case Api.baseUrl:
return '[URL]';
case Api.assets:
return 'assets';
}
}
}
We will use the dio package we added to the project to write a service. By creating a service class, we communicate our requests through it.
Now let’s see how we use our service on the BloC side.
To add Bloc files, I’m using the bloc extension. If you’re using VS Code, you can create bloc files by right-clicking and selecting new bloc.
Let’s Create the BloC Structure with Freezed
The BloC structure consists of State, Event, and the main bloc section where functions are called.
part of 'home_bloc.dart';
@freezed
class HomeState with _$HomeState {
const factory HomeState.loading() = _Loading;
const factory HomeState.loaded({required final List<CoinModel> items}) =
_Loaded;
}
I defined two different states: the loading state that will be active during loading and the loaded state that will return when finished, along with the CoinModel list. As we write the code, files will be regenerated, and errors will be resolved.
part of 'home_bloc.dart';
@freezed
class HomeEvent with _$HomeEvent {
const factory HomeEvent.load() = _Load;
}
I created an event called Load. This event allows us to call functions in the bloc.
import 'package:bloc/bloc.dart';
import 'package:coin_example/core/service/coin_service.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../core/model/coin_model.dart';
part 'home_event.dart';
part 'home_state.dart';
part 'home_bloc.freezed.dart';
class HomeBloc extends Bloc<HomeEvent, HomeState> {
final CoinService coinService;
HomeBloc(this.coinService) : super(const _Loading()) {
on<_Load>(_onLoad);
}
Future<void> _onLoad(_Load event, Emitter<HomeState> emit) async {
final val = await coinService.fetchCoins();
emit(HomeState.loaded(items: val));
}
}
We call our functions using on(eventFunction). Here, our onLoad function fetches our data from the service and sends it to the interface. Also, the initial state is set as Loading.
Finally, Let’s Look at the User Interface
We wrap the part of the interface that needs to be aware of the states with BlocProvider.
HomeBloc(CoinService(dio: Dio()))..add(const HomeEvent.load())
By using ..add in the create parameter of BlocProvider, we initiated the data-fetching process by executing the onLoad function. Finally, we receive the Loaded state and our CoinModel list.
The BlocBuilder section is triggered during state refresh events and allows this part to be rebuilt.
In BlocBuilder, by calling state.when, we see one of the best features provided by Freezed. We can see the states defined on the HomeState side and provide the widgets to be displayed in those states.
BlocBuilder<HomeBloc, HomeState>(
builder: (context, state) {
return state.when(
loading: () => LoadProgressStateWidget(),
loaded: (_models) => LoadSuccessStateWidget(_models),
);
},
)
Yes, it’s that simple. It definitely has a more effective usage than normal and has added many features to our classes. Normally, we would have to write extra code for these features.
Happy Coding to Everyone!