AsyncMemoizer: Caching Futures for Performance Optimization in Flutter

AsyncMemoizer: Caching Futures for Performance Optimization in Flutter

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. InitialiseAsyncMemoizer 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. CallrunOncemethod onAsyncMemoizerthat accepts ourfetchDataFromApifunction

  // 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. CallrunOnce 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 optimise 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.

Originally posted on medium.com