Making Flutter and REST API Work Together — Efficient Data Merging (Part 8)

Alex Josef Bigler
Full Struggle Developer
6 min readApr 9, 2023

In modern applications, situations often arise when some data depends on others, and information must be obtained from different API methods. In our example, we have a list of albums, and for each album, additional data about the artist is required through a separate API request. However, a naive approach to executing separate requests for each album can lead to performance issues and inefficient use of resources.

In this article, we will explore various strategies and approaches to optimizing such dependent requests, as well as techniques for combining data to minimize delays and improve application performance. Together, we will find the optimal balance between efficiency and code quality to make your application run faster and more reliably.

The following posts are suggested reading for this post to get you started:

The Problem

Before we start optimizing, let’s first understand the problems that can arise with a naive approach to handling dependent API requests. Let’s say we have a list of albums, and for each album, we want to display information about the artist. If we send a separate request for each artist, this can lead to the following problems:

  • Delays: Sending individual requests for each artist can cause significant delays as each request will be processed sequentially, and the server response time may not be constant.
  • Performance issues: Performing a large number of requests can negatively impact the performance of your application and the server.
  • Limitations on the number of requests: Some APIs have limitations on the number of requests that can be made within a certain period of time. Sending too many requests can lead to reaching these limits, which will result in access to the API being restricted.

Now that we’ve identified the problems, let’s move on to possible optimization strategies:

  1. Batch processing of requests: Instead of sending individual requests for each artist, we can consider sending one request with an array of artist IDs. This will reduce the number of requests and decrease the overall processing time.
  2. Caching data: If artists or albums rarely change, we can use data caching to store information about them on the client side. This will help reduce the number of server requests and speed up data loading.
  3. Parallel requests: Instead of sending sequential requests, we can perform parallel requests to speed up data loading. In this case, requests are executed simultaneously, which reduces the overall processing time.
  4. Lazy loading: In some cases, information about artists can be loaded only when it is actually needed.

Of all the optimization approaches, we will be using batch request processing and lazy loading. Batch request processing allows us to reduce the number of requests to the server by grouping them into larger batches. Lazy loading allows us to load only the data that is needed at the moment, deferring the loading of other data until it is needed. Both of these approaches can significantly improve performance and reduce delays when working with APIs.

Do it

Our custom fake API allows making requests to retrieve information about artists using their IDs. One request can include multiple IDs, allowing to get information about all necessary artists at once:

GET /artists?id=..&id=..&id=..

This allows more efficient use of server resources and reduces network load.

If you want to deploy such a fake API server yourself, you can read my article here 👈.

To solve above issues, we can optimize our approach to retrieving artist data. First, we will retrieve albums as usual with pagination, and then using unique artist IDs, we will make a single request to load all artists. Then, we will associate artists with albums using their IDs. This will improve performance and reduce the number of API requests.

First, as usual, we add support for artist data from the API, which we will receive in the following format:

[
{
"id": 18,
"name": "Rick Astley",
"bio": "Nihil mollitia animi dolores eius unde provident. Reiciendis molestias blanditiis accusantium fuga modi omnis eveniet laudantium. Amet reprehenderit perferendis commodi ipsam. Recusandae repudiandae labore quidem perferendis reiciendis iusto sint accusamus porro. Animi in suscipit quae asperiores consequuntur maiores officiis sapiente consequuntur. Eum necessitatibus harum voluptatibus saepe expedita.",
"genres": [
"Folk"
],
"country": "Netherlands",
"yearsActive": 39,
"listens": 33988,
"image": "https://cloudflare-ipfs.com/ipfs/Qmd3W5DuhgHirLHGVixi6V76LhCkZUz6pnFt5AJBiyvHye/avatar/62.jpg"
}
]

To begin with, let’s create a class Artist, which will represent the data about the artist:

class Artist {
final int id;
final String name;
final String bio;
final List<String> genres;
final String country;
final int yearsActive;
final int listens;
final String image;

Artist({
required this.id,
required this.name,
required this.bio,
required this.genres,
required this.country,
required this.yearsActive,
required this.listens,
required this.image,
});

factory Artist.fromJson(Map<String, dynamic> json) {
return Artist(
id: json['id'],
name: json['name'],
bio: json['bio'],
genres: List<String>.from(json['genres']),
country: json['country'],
yearsActive: json['yearsActive'],
listens: json['listens'],
image: json['image'],
);
}
}

Now let’s add a method for loading artists from the API to the ApiProvider class:

  Future<List<Artist>> fetchArtists({List<int>? ids}) async {
String url = '$_baseUrl/artists';

if (ids != null && ids.isNotEmpty) {
final idParams = ids.map((id) => 'id=$id').join('&');
url += '?$idParams';
}

final response = await http.get(Uri.parse(url));
String responseBody = processResponse(response);

List jsonResponse = json.decode(responseBody);
return jsonResponse.map((artist) => Artist.fromJson(artist)).toList();
}

This method uses an optional parameter ids, which allows specifying a list of artist IDs to retrieve their data. If ids is not specified or empty, the method will return all available artists.

Then, to add information about the artist to the widget with albums, we need to first load data about the artists. Let’s add a new method _fetchArtists to the class _AlbumListWidgetState, which calls fetchArtists from ApiProvider:

Future<void> _fetchArtists(List<Album> albums) async {
List<int> artistIds = albums.map((album) => album.artistId).toSet().toList();
// Here will be the code to link artists with albums
}

Now let’s modify the _fetchAlbums method to call _fetchArtists with the artist IDs after loading the albums:

  _fetchAlbums() async {
setState(() {
_isLoading = true;
});

List<Album> newAlbums = await _apiProvider.fetchAlbums(_currentPage, _pageSize);

// Load artists and link them with albums
await _fetchArtists(newAlbums);

setState(() {
_isLoading = false;
_albums.addAll(newAlbums);
_currentPage++;
});
}

During the loading of artists, we will be adding them to albums using artist IDs. To do this, we will add a new property artist to the Album class and set it in the constructor:

class Album {
final int id;
final String title;
final int year;
final String cover;
final String color;
final String label;
final int artistId;
final int listens;
Artist? artist; // <-- Here

Album({
required this.id,
required this.title,
required this.year,
required this.cover,
required this.color,
required this.label,
required this.artistId,
required this.listens,
this.artist, // <-- And here
});

factory Album.fromJson(Map<String, dynamic> json) {
return Album(
id: json['id'],
title: json['title'],
year: json['year'],
cover: json['cover'],
color: json['color'],
label: json['label'],
artistId: json['artistId'],
listens: json['listens'],
);
}
}

Now let’s modify the _fetchArtists method to link artists with albums:

  Future<void> _fetchArtists(List<Album> albums) async {

// Get unique artist IDs from albums
List<int> artistIds = albums.map((album) => album.artistId).toSet().toList();

// Fetch artists data from API by their IDs
List<Artist> artists = await _apiProvider.fetchArtists(ids: artistIds);

// Create a Map with artist IDs as keys and Artist objects as values
Map<int, Artist> artistMap = {};
for (Artist artist in artists) {
artistMap[artist.id] = artist;
}

// Link artists with their albums using artist IDs
for (Album album in albums) {
album.artist = artistMap[album.artistId];
}
}

Now we have artist information linked to each album. You can display the artist name in the ListTile widget in the album list 🧐:

ListTile(
title: Text(album.title),
subtitle: Text("${album.year} - ${album.artist?.name ?? ''}"),
leading: CachedNetworkImage(
imageUrl: album.cover,
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),
trailing: Text("Listens: ${album.listens}"),
)

Let’s see the result:

Noice.

Summary

We have used optimization in the form of loading artists only for those albums that were loaded on the current page. This helped reduce the number of API requests and load artists only when they are actually needed for display on the page.

In the upcoming articles, we will explore other aspects of optimizing applications related to working with APIs. We will cover how to implement search and filtering of data, as well as how to use debouncing for more efficient handling of requests.

Stay tuned!

--

--

Alex Josef Bigler
Full Struggle Developer

Enthusiast of new technologies, entrepreneur, and researcher. Writing about IT, economics, and other stuff. Exploring the world through the lens of data.