AsyncMemoizer: Caching Futures for Performance Optimization in Flutter

Etornam Sunu Bright
4 min readMar 4, 2024

Tired of redundant API calls and sluggish performance? Flutter developers, rejoice! AsyncMemoizer is your secret weapon for optimizing app speed and efficiency. This powerful tool caches the results of asynchronous functions, ensuring smooth performance and minimal resource usage. This article delves into all the important details of this powerful helper, explaining its purpose, functionalities, and effective use in your Flutter projects.

What is AsyncMemoizer?

AsyncMemoizer is a class that helps you cache the results of asynchronous functions. It ensures that the function runs only once for a given set of inputs, even if it's called multiple times. This can significantly improve performance by avoiding redundant network calls or expensive calculations.

Key Features of AsyncMemoizer:

  • Caching: Stores the result of the first successful execution for future calls with the same arguments.
  • Thread-safety: Handles concurrent calls safely, preventing race conditions and unexpected behaviour.
  • Error Handling: Propagates errors from the original function, allowing proper response to failures.
  • Customisation: Options like clearing cache, setting key generation functions, and providing error handlers offer flexibility.

Benefits of Using AsyncMemoizer:

  • Performance Improvement: Reduces redundant computations and network calls, leading to faster app responsiveness.
  • Efficiency: Minimises resource usage by avoiding unnecessary work.
  • Maintainability: Simplifies code by handling caching logic internally.

When to Use AsyncMemoizer:

  • API calls with static data: If the API response doesn’t change frequently, cache it to avoid unnecessary network requests.
  • Expensive calculations: Memoize the results of complex calculations to avoid recalculating them for the same inputs.
  • Data fetching in widgets: Use it with FutureBuilder or similar widgets to prevent redundant data fetching on rebuilds.

How to Use AsyncMemoizer:

  1. Import the package:
import 'package:async/async.dart';
import 'package:http/http.dart';
import 'dart:developer';

2. Create a function that makes a network call:

Future<String> fetchDataFromApi() async {
// Simulate an API call that takes 2 seconds
final response = await get(Uri.parse('https://catfact.ninja/fact'));

if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Failed to load data from API');
}
}

3. Initialise AsyncMemoizer and Stopwatch

 // Create an AsyncMemoizer to cache the result of fetchDataFromApi
final memoizer = AsyncMemoizer<String>();

// Create a stopwatch to measure time
// We are using the Stopwatch to measure the time taken to fetch the data.
final stopwatch = Stopwatch()..start();

4. Call runOnce method on AsyncMemoizer that accepts our fetchDataFromApi function

  // Call the memoizer to fetch data
String data1 = await memoizer.runOnce(fetchDataFromApi);

log('data1: $data1',name: 'memoizer'); // Output: cats facts


final firstCallTime = stopwatch.elapsed;

log("First call: $data1 (took ${firstCallTime.inMilliseconds}ms)",name: 'memoizer');

// Reset the stopwatch for the second call
stopwatch.reset();

5. Call runOnce method on AsyncMemoizer that accepts our fetchDataFromApi function and returns as data2

  // Call the memoizer again, but it will use the cached result
String data2 = await memoizer.runOnce(fetchDataFromApi);

log('data2: $data2',name: 'memoizer'); // Output: Data from API (almost instantaneous)

final secondCallTime = stopwatch.elapsed;

log("Second call: $data2 (took ${secondCallTime.inMilliseconds}ms)",name: 'memoizer');

// Check if the data is the same
log('data1 == data2: ${data1 == data2}', name: 'memoizer'); // Output: true

The logs will look something like this:

[memoizer] data1: {"fact":"A female cat is called a queen or a molly.","length":42}
[memoizer] First call: {"fact":"A female cat is called a queen or a molly.","length":42} (took 723ms)
[memoizer] data2: {"fact":"A female cat is called a queen or a molly.","length":42}
[memoizer] Second call: {"fact":"A female cat is called a queen or a molly.","length":42} (took 0ms)
[memoizer] data1 == data2: true

You will notice the second API call took 0ms to complete.

Important Considerations:

  • Cache Invalidation: If the cached data becomes outdated, consider mechanisms to clear the cache or update it automatically.
  • Thread Safety: Ensure multi-threaded environments access the memoizer safely, especially when using custom key generation functions.
  • Memory Usage: Large caches can consume memory, so use it judiciously and consider eviction strategies.

The complete code below:

import 'dart:developer';
import 'package:async/async.dart';
import 'package:http/http.dart';

Future<String> fetchDataFromApi() async {
final response = await get(Uri.parse('https://catfact.ninja/fact'));

if (response.statusCode == 200) {
return response.body;
} else {
throw Exception('Failed to load data from API');
}
}

void main() async {
// Create an AsyncMemoizer to cache the result of fetchDataFromApi
final memoizer = AsyncMemoizer<String>();

// Create a stopwatch to measure time
final stopwatch = Stopwatch()..start();

// Call the memoizer to fetch data
String data1 = await memoizer.runOnce(fetchDataFromApi);
log('data1: $data1',
name: 'memoizer'); // Output: Data from API (takes 2 seconds)

final firstCallTime = stopwatch.elapsed;

log("First call: $data1 (took ${firstCallTime.inMilliseconds}ms)",
name: 'memoizer');

// Reset the stopwatch for the second call
stopwatch.reset();

// Call the memoizer again, but it will use the cached result
String data2 = await memoizer.runOnce(fetchDataFromApi);
log('data2: $data2',
name: 'memoizer'); // Output: Data from API (almost instantaneous)

final secondCallTime = stopwatch.elapsed;

log("Second call: $data2 (took ${secondCallTime.inMilliseconds}ms)",
name: 'memoizer');

// Check if the data is the same
log('data1 == data2: ${data1 == data2}', name: 'memoizer'); // Output: true
}

By understanding AsyncMemoizer and its capabilities, you can optimize your Flutter applications by efficiently caching asynchronous operations and enhancing performance. Remember to use it strategically, considering thread safety, cache invalidation, and memory usage for optimal results.

I hope this comprehensive article provides valuable insights for Flutter developers/Engineers and anyone looking to improve their app's performance using AsyncMemoizer.

And that’s all for now. If you have any specific topic you would like me to write on, leave your suggestions in the comment section and also if you need any clarifications on this topic, do well to reach out to me on X(Twitter) @_iamEtornam.

--

--

Etornam Sunu Bright

Mobile and Backend engineering. Flutter Africa Community Co-organizer 🌍.