skip to content

In shared_preferences 2.5, how do SharedPreferencesAsync and SharedPreferencesWithCache differ, and why are both preferred over the legacy getInstance API?

level: middleimportance: should knowfreq 38%

answer

  1. one has no cache at all
  2. create() loads, then sync getters
  3. allowList: null, empty, missing key
  4. each isolate has its own cache
  5. legacy: flutter. prefix, to be deprecated

basics

~20 s

SharedPreferencesAsync has no cache, so every read is an awaited platform call that always sees the latest value. SharedPreferencesWithCache loads allow-listed keys once in create() and then reads synchronously. The legacy getInstance singleton is slated for deprecation.

solid answer

~40 s

Since 2.3.0 the package has three APIs. `SharedPreferencesAsync` keeps no local cache: every `get` is a `Future` that goes to the platform, so it always sees the latest value, even one written by another isolate, a background engine or native code. `SharedPreferencesWithCache` is created with `await SharedPreferencesWithCache.create(cacheOptions: ...)`, loads the keys in its `allowList` into memory, then offers synchronous getters; setters update the cache and the platform. An `allowList` of `null` caches everything, an empty set allows nothing, and touching a key outside it throws `ArgumentError`. The legacy `SharedPreferences.getInstance()` is a per-isolate singleton that caches every `flutter.`-prefixed key; the README says it will be deprecated in the future, so new code picks one of the two newer APIs.

code

dart · 21 lines
dart
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';

Future<void> demo() async {
  final prefs = await SharedPreferencesWithCache.create(
    cacheOptions: const SharedPreferencesWithCacheOptions(
      allowList: <String>{'languageCode', 'onboardingDone'},
    ),
  );

  final String? code = prefs.getString('languageCode'); // synchronous
  await prefs.setBool('onboardingDone', true);

  // Throws ArgumentError: the key is not in the allowList.
  // prefs.getString('authToken');

  final fresh = SharedPreferencesAsync();
  final bool? done = await fresh.getBool('onboardingDone'); // platform read
  await fresh.clear(allowList: <String>{'languageCode'}); // scoped clear
  debugPrint('$code $done');
}

go deeper

for a junior

Remember that the package has an async API, a cached API and a legacy getInstance, and that new code uses one of the first two.

for a middle

Explain the cache difference, what create() loads, the null versus empty allowList rule, and the ArgumentError for keys outside it.

for a senior

Diagnose stale reads across isolates and engines, scope clear() safely, and choose the API based on who else writes the store.

for a principal

Decide the team convention for preferences access — one wrapper, one API, explicit allowLists — so storage behaviour is predictable across features.

## Three APIs in one package Version 2.3.0 of `shared_preferences` added two new entry points alongside the original one. They share the same backing stores and the same five value types, but differ in **caching** and **scoping**. | | `SharedPreferencesAsync` | `SharedPreferencesWithCache` | `SharedPreferences` (legacy) | |---|---|---|---| | Obtain it | `SharedPreferencesAsync()` | `await SharedPreferencesWithCache.create(cacheOptions: ...)` | `await SharedPreferences.getInstance()` | | Local cache | none | allow-listed keys | every key with the prefix | | Getters | `Future<T?>` | synchronous `T?` | synchronous `T?` | | Setters | `Future<void>` | `Future<void>`, cache updated first | `Future<bool>`, cache updated first | | Key scoping | optional `allowList` per `getAll`, `getKeys`, `clear` | `allowList` fixed at creation | `flutter.` prefix, `setPrefix` | | Status | recommended | recommended | legacy, to be deprecated | ## SharedPreferencesAsync: always fresh `SharedPreferencesAsync` holds no copy of the data. Every call — `getString`, `getBool`, `containsKey` — crosses to the platform and awaits the answer. That is slower per read, but it can never be stale. It is the right choice when something other than this Dart instance may change the store: - another **isolate** in the same app; - another **engine**, such as the background context some plugins create for push-message handlers; - **native code** that reads or writes the same preferences. Its `clear()` takes an optional `allowList`. Without one it removes **everything** in the store — including values other packages or native code put there — and the source recommends always passing one. ## SharedPreferencesWithCache: fast synchronous reads `SharedPreferencesWithCache.create` awaits a `reloadCache()` that fetches the allow-listed keys, after which `getString`, `getBool` and friends return immediately. That makes it convenient to read values during `build` or while wiring up the app. Setters write into the cache first and then return the platform write's `Future`. The `allowList` in `SharedPreferencesWithCacheOptions` is central: 1. `null` — no filtering; every key in the store is cached. 2. An empty set — nothing can be read, written or cached. 3. A set of keys — only those keys; any getter, setter, `containsKey` or `remove` for another key throws an `ArgumentError`. Passing an explicit list is strongly recommended: it keeps start-up reads small and stops a stray key from other code leaking in. `clear()` here clears only the allow-listed keys. If the store may change behind the cache's back, call `reloadCache()` before reading. ## Why the legacy API is on its way out `SharedPreferences.getInstance()` returns a per-isolate singleton that loads every key carrying its prefix (`flutter.` by default) into memory. It works, but it has sharp edges the newer APIs remove: - the cache is per isolate and per engine, so a background handler and the UI can each hold a different view until `reload()` is called; - there is no key scoping beyond the prefix, so it caches everything; - `setPrefix` must be called before the first `getInstance`, and changing it silently hides old values; - `commit()` is already a deprecated no-op. The README describes it as a legacy API that will be deprecated in the future and encourages new code to use the other two. The official Flutter cookbook page still demonstrates `getInstance`, so candidates will see it everywhere; knowing that it is legacy is the point. ## Choosing between the two newer APIs - Pick **`SharedPreferencesWithCache`** when a handful of known keys are read often, the app is the only writer, and synchronous access simplifies the code. - Pick **`SharedPreferencesAsync`** when freshness matters more than latency, when several isolates or engines touch the store, or when the set of keys is open-ended. For an app that remembers a language code and an onboarding flag, a `SharedPreferencesWithCache` with `allowList: {'languageCode', 'onboardingDone'}` created once at start-up is the natural fit.

  • A push-message background handler writes a preference, but the UI still shows the old value. Why?
    The background handler runs in a different isolate or engine, and a cached API — `SharedPreferencesWithCache` or the legacy singleton — keeps its own in-memory copy per isolate. Either call `reloadCache()` (or `reload()` on the legacy class) before reading, or use `SharedPreferencesAsync`, which always reads from the platform.
  • Why is calling SharedPreferencesAsync().clear() with no allowList risky?
    It removes every preference in the platform store, not just the ones your code wrote — including values from native code or other packages using the same store. The source recommends always passing an `allowList` so only your keys are cleared.
  • What does an empty allowList do on SharedPreferencesWithCacheOptions?
    It allows nothing: no key can be cached, read or written, and every access throws `ArgumentError`. `null` is the value that means no filtering, so the two are opposites rather than synonyms.

saying these in an interview costs you the question

  • SharedPreferencesAsync caches values in memory, so reads are synchronous.
  • An empty allowList means every key is allowed.
  • Reading a key outside the allowList just returns null.
  • The legacy getInstance singleton is shared across all isolates.
  • getInstance is already annotated @Deprecated and fails the build.