DEV Community

Cover image for Cache API Integration with HydratedBLoC in Flutter (Source Codes Included)
Imran Sefat
Imran Sefat

Posted on

9 2

Cache API Integration with HydratedBLoC in Flutter (Source Codes Included)

Introduction 🎉

BLoC stands for Business Logic Controller. It was created by Google and introduced at Google I/O 2018. It is made based on Streams and Reactive Programming.

I can assure you that every intermediate-level Flutter developer in their development lifetime heard about Bloc or maybe tried to learn bloc. Bloc is one of the most popular state management choices among developers because it has rich documentation and is well maintained. But yes, there are some downsides as well, for example, a lot of boilerplate codes.

We will implement API integration at first, then persist the state so that when the user closes the app, it can maintain the state or load the data from the local device saved from the last API call to put it simply.

Note that after we kill the app, it starts right back where it left off. In addition, after it has loaded the previous (cached) state, the app requests the latest API data and updates seamlessly. Let’s get started!



I won

Steps 👣

1. Configuring the Flutter Project

2. Add the datamodel

3. Creating the bloc

4. Creating the bloc state and event

5. Creating the Bloc Repository

6. Implementing the bloc



I won

1. Configuring the Flutter project ⚙️

Let’s add the necessary packages that we’re going to use throughout the application.

Copy the dependencies to your Pubspec.yaml file. I am using the latest version available now at this moment.

dependencies:
bloc: ^7.2.1
build_runner: ^2.1.4
cupertino_icons: ^1.0.2
equatable: ^2.0.3
flutter:
sdk: flutter
flutter_bloc: ^7.3.1
http: ^0.13.4
hydrated_bloc: ^7.1.0
json_annotation: ^4.3.0
json_serializable: ^6.0.1
path_provider: ^2.0.5
dev_dependencies:
flutter_lints: ^1.0.0
flutter_test:
sdk: flutter
view raw pubspec.yaml hosted with ❤ by GitHub

Then we need to install it with:

flutter packages get
Enter fullscreen mode Exit fullscreen mode

You will get to understand everything as we go ahead.



Datamodel

2. Add the datamodel 📳

We will implement the “FREETOGAME API”. For this, we have to make a datamodel of the API’s response. I have used the following website to make the datamodel class. It’s pretty easy, copy the JSON response and paste it on the website. The website will generate a class for you.

Website Link: https://ashamp.github.io/jsonToDartModel/
Don’t forget to tick the Null Safety checkbox!

class FreeToGamesModel {
/*
{
"id": 1,
"title": "Dauntless",
"thumbnail": "https://www.freetogame.com/g/1/thumbnail.jpg",
"short_description": "A free-to-play, co-op action RPG with gameplay similar to Monster Hunter.",
"game_url": "https://www.freetogame.com/open/dauntless",
"genre": "MMORPG",
"platform": "PC (Windows)",
"publisher": "Phoenix Labs",
"developer": "Phoenix Labs, Iron Galaxy",
"release_date": "2019-05-21",
"freetogame_profile_url": "https://www.freetogame.com/dauntless"
}
*/
int? id;
String? title;
String? thumbnail;
String? shortDescription;
String? gameUrl;
String? genre;
String? platform;
String? publisher;
String? developer;
String? releaseDate;
String? freetogameProfileUrl;
FreeToGamesModel({
this.id,
this.title,
this.thumbnail,
this.shortDescription,
this.gameUrl,
this.genre,
this.platform,
this.publisher,
this.developer,
this.releaseDate,
this.freetogameProfileUrl,
});
FreeToGamesModel.fromJson(Map<String, dynamic> json) {
id = json["id"]?.toInt();
title = json["title"]?.toString();
thumbnail = json["thumbnail"]?.toString();
shortDescription = json["short_description"]?.toString();
gameUrl = json["game_url"]?.toString();
genre = json["genre"]?.toString();
platform = json["platform"]?.toString();
publisher = json["publisher"]?.toString();
developer = json["developer"]?.toString();
releaseDate = json["release_date"]?.toString();
freetogameProfileUrl = json["freetogame_profile_url"]?.toString();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data["id"] = id;
data["title"] = title;
data["thumbnail"] = thumbnail;
data["short_description"] = shortDescription;
data["game_url"] = gameUrl;
data["genre"] = genre;
data["platform"] = platform;
data["publisher"] = publisher;
data["developer"] = developer;
data["release_date"] = releaseDate;
data["freetogame_profile_url"] = freetogameProfileUrl;
return data;
}
}

Another datamodel that will contain the list of the games refers to the code below.


import 'package:equatable/equatable.dart';
import 'package:free_to_game_bloc/model/free_to_games.dart';
import 'package:json_annotation/json_annotation.dart';
part 'gamelist_model.g.dart';
@JsonSerializable()
class Gamelist extends Equatable {
final List<dynamic> allGames;
const Gamelist({
required this.allGames,
});
factory Gamelist.fromJson(Map<String, dynamic> json) =>
_$GamelistFromJson(json);
Map<String, dynamic> toJson() => _$GamelistToJson(this);
@override
List<Object?> get props => throw UnimplementedError();
}


The above code will show you some errors, as you can see that the 5th line contains a code that indicates that this file is part of another file that needs to be generated. Another thing, look at the 7th line, it is indicating that we will serialize so that we can save the response for later use.


Open a terminal and run the below code.

flutter packages pub run build_runner build



bloc

3. Creating the bloc

import 'package:flutter/foundation.dart';
import 'package:free_to_game_bloc/model/gamelist_model.dart';
import 'package:free_to_game_bloc/repository/game_repository.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:meta/meta.dart';
part 'gamelist_event.dart';
part 'gamelist_state.dart';
class GamelistBloc extends HydratedBloc<GamelistEvent, GamelistState> {
final FreeGamesRepository freeGamesRepository;
GamelistBloc(this.freeGamesRepository) : super(const GamelistLoading()) {
on<GetGames>((event, emit) => _onGameLoading);
}
_onGameLoading(GamelistLoading event, Emitter<int> emit) async* {
try {
yield const GamelistLoading();
final gamelist = await freeGamesRepository.fetchFreeGames();
yield GamelistLoaded(gamelist);
} catch (e) {
// print(e.toString());
yield GamelistError(e.toString());
}
}
@override
GamelistState? fromJson(Map<String, dynamic> json) {
try {
final gamelist = Gamelist.fromJson(json);
return GamelistLoaded(gamelist);
} catch (_) {
return null;
}
}
@override
Map<String, dynamic>? toJson(GamelistState state) {
if (state is GamelistLoaded) {
return state.gamelist.toJson();
} else {
return null;
}
}
}



It contains the logic behind the main bloc. Now we have to make the event and state as well.



bloc

4. Creating the bloc state and event

import 'package:flutter/foundation.dart';
import 'package:free_to_game_bloc/model/gamelist_model.dart';
import 'package:free_to_game_bloc/repository/game_repository.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:meta/meta.dart';
part 'gamelist_event.dart';
part 'gamelist_state.dart';
class GamelistBloc extends HydratedBloc<GamelistEvent, GamelistState> {
final FreeGamesRepository freeGamesRepository;
GamelistBloc(this.freeGamesRepository) : super(const GamelistLoading()) {
on<GetGames>((event, emit) => _onGameLoading);
}
_onGameLoading(GamelistLoading event, Emitter<int> emit) async* {
try {
yield const GamelistLoading();
final gamelist = await freeGamesRepository.fetchFreeGames();
yield GamelistLoaded(gamelist);
} catch (e) {
// print(e.toString());
yield GamelistError(e.toString());
}
}
@override
GamelistState? fromJson(Map<String, dynamic> json) {
try {
final gamelist = Gamelist.fromJson(json);
return GamelistLoaded(gamelist);
} catch (_) {
return null;
}
}
@override
Map<String, dynamic>? toJson(GamelistState state) {
if (state is GamelistLoaded) {
return state.gamelist.toJson();
} else {
return null;
}
}
}


There can be three(3) states.

  1. Game list is loading -> GamelistLoading
  2. Game list loaded -> GamelistLoaded
  3. Game list cannot be loaded -> GamelistError



loading

5. Creating the Bloc Repository👾

import 'dart:convert';
import 'package:free_to_game_bloc/model/free_to_games.dart';
import 'package:free_to_game_bloc/model/gamelist_model.dart';
import 'package:http/http.dart' as http;
class FreeGamesRepository {
Future<Gamelist> fetchFreeGames() async {
List<FreeToGamesModel> games = [];
final response =
await http.get(Uri.parse('https://www.freetogame.com/api/games'));
if (response.statusCode == 200) {
// List<dynamic> allGames = jsonDecode();
List<dynamic> data = jsonDecode(response.body);
for (var game in data) {
final freeGame = FreeToGamesModel.fromJson(game);
games.add(freeGame);
}
Gamelist allGames = Gamelist(allGames: games);
return allGames;
} else {
Gamelist allGames = const Gamelist(allGames: []);
return allGames;
}
}
}

We are calling the API using the HTTP package form from this file or class.



loading

6. Implementing the bloc🛠

import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:flutter/material.dart';
import 'package:free_to_game_bloc/bloc/gamelist_bloc.dart';
import 'package:free_to_game_bloc/model/free_to_games.dart';
import 'package:free_to_game_bloc/model/gamelist_model.dart';
import 'package:free_to_game_bloc/repository/game_repository.dart';
import 'package:path_provider/path_provider.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
HydratedBloc.storage = await HydratedStorage.build(
storageDirectory: await getTemporaryDirectory(),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.red,
),
home: BlocProvider(
create: (context) => GamelistBloc(FreeGamesRepository()),
child: const MyHomePage(title: 'Flutter Demo Home Page'),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: BlocBuilder<GamelistBloc, GamelistState>(
builder: (context, state) {
if (state is GamelistLoading) {
return const GameloadingScreen();
} else if (state is GamelistLoaded) {
return GamelistWidget(gamelist: state.gamelist);
} else {
return const Text("Error");
}
},
),
);
}
}
class GameloadingScreen extends StatelessWidget {
const GameloadingScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final gamelistBloc = BlocProvider.of<GamelistBloc>(context);
gamelistBloc.add(GetGames(gamelistBloc.freeGamesRepository));
return const Center(child: CircularProgressIndicator());
}
}
class GamelistWidget extends StatelessWidget {
const GamelistWidget({
Key? key,
required this.gamelist,
}) : super(key: key);
// final List<FreeToGamesModel> gamelist;
final Gamelist gamelist;
@override
Widget build(BuildContext context) {
return ListView.builder(
shrinkWrap: true,
itemCount: gamelist.allGames.length,
itemBuilder: (BuildContext context, int index) {
FreeToGamesModel game = gamelist.allGames[index];
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 6),
child: Container(
decoration: BoxDecoration(
color: Colors.blue[50],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image(
// fit: BoxFit.cover,
// image: NetworkImage(game.thumbnail),
// ),
Text(game.title!),
Text(game.publisher!)
],
),
),
);
},
);
}
}
view raw main.dart hosted with ❤ by GitHub



This portion contains the UI and the Bloc implementation. You can check the main function. It is instantiating the hydrated bloc in the temporary directory.
Note that after we kill the app it starts right back where it left off. In addition, after it has loaded the previous (cached) state, the app requests the latest API data and updates seamlessly.



Congratulations! 🎊

You just integrated an API with HydratedBloc that has Caching.



success

Contact Me🌎

YouTube Channel: Coding with Imran
Twitter: @ImranSefat
LinkedIn: MD. Al Imran Sefat
Facebook Page: Coding with Imran

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (2)

Collapse
 
nombrekeff profile image
Keff

Interesting, I quite enjoyed this post. I have not used the bloc pattern much in flutter so I'm a bit of a newby.

I have a question though, where exactly is the cache implemented or what part handles the caching? does "HydratedBloc" cache stuff for you automatically? Or have I missed something?

I also see you setup HydrateBlock.storage, am I correct in asuming that the the state/data is also persistent in disk? Or is the storage just to store the cache?

Collapse
 
imransefat profile image
Imran Sefat

hydrated_bloc exports a Storage interface which means it can work with any storage provider. Out of the box, it comes with its own implementation: HydratedStorage.

HydratedStorage is built on top of hive for a platform-agnostic, performant storage layer. See the complete example for more details.

Copied from pub.dev

See the details here: pub.dev/packages/hydrated_bloc

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more