skip to content

When a Flutter app switches from SharedPreferences.getInstance() to SharedPreferencesAsync, why can saved values seem to disappear, and how do you migrate them safely?

level: seniorimportance: nice to knowfreq 22%

answer

  1. legacy keys carry a hidden prefix
  2. Android: DataStore versus the old file
  3. a migration util in lib/util
  4. migrationCompletedKey makes it rerunnable
  5. switch every call site at once

basics

~20 s

The legacy API stores keys with a hidden flutter. prefix, and on Android the new APIs default to DataStore instead of the old SharedPreferences file, so the new API reads different keys. Run migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary once at start-up before any new read.

solid answer

~40 s

Two things differ. The legacy `SharedPreferences` class writes every key with a `flutter.` prefix that it adds and strips invisibly, while `SharedPreferencesAsync` and `SharedPreferencesWithCache` use the key as given. And on Android the newer APIs default to DataStore Preferences, a separate store from the `SharedPreferences` file the legacy class used. So `getString('languageCode')` on the new API finds nothing. The fix is the package's utility `migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary` from `package:shared_preferences/util/legacy_to_async_migration_util.dart`: pass the legacy instance, the new API's options and a `migrationCompletedKey`, and await it before the first new-API read. It copies each supported value and then sets the completion flag, so running it on every launch is safe.

code

dart · 22 lines
dart
import 'package:flutter/widgets.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences/util/legacy_to_async_migration_util.dart';

Future<SharedPreferencesWithCache> openPreferences() async {
  WidgetsFlutterBinding.ensureInitialized();
  const options = SharedPreferencesOptions();

  final legacy = await SharedPreferences.getInstance();
  await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary(
    legacySharedPreferencesInstance: legacy,
    sharedPreferencesAsyncOptions: options,
    migrationCompletedKey: 'prefsMigratedToAsync',
  );

  return SharedPreferencesWithCache.create(
    sharedPreferencesOptions: options,
    cacheOptions: const SharedPreferencesWithCacheOptions(
      allowList: <String>{'languageCode', 'onboardingDone'},
    ),
  );
}

go deeper

for a junior

Know that moving off getInstance can make saved settings look lost and that the package ships a migration helper for it.

for a middle

Explain the flutter. prefix and the DataStore default on Android as the two reasons, and what the migration utility's three arguments do.

for a senior

Plan the rollout: migrate before any read, move every call site together, keep the completion key stable and test the upgrade path from an old build.

for a principal

Treat storage-API migrations as data migrations with rollback and cleanup phases, not as refactors, and schedule legacy cleanup deliberately.

## The symptom A team replaces `SharedPreferences.getInstance()` with `SharedPreferencesAsync` or `SharedPreferencesWithCache`. Nothing crashes, but existing users suddenly see onboarding again and the app forgets their chosen language. The values are still on the device; the new API is simply looking somewhere else. ## Why the values seem to vanish There are two independent reasons, and either is enough. 1. **The key prefix.** The legacy class prepends a prefix — `flutter.` by default — to every key it writes, and strips it when it reads. Your code said `setString('languageCode', 'pt')`; the platform store holds `flutter.languageCode`. The newer APIs do not add a prefix, so they look for `languageCode` and find nothing. This affects every platform, including iOS where both APIs use `NSUserDefaults`. 2. **A different store on Android.** The legacy class uses an Android `SharedPreferences` file. The newer APIs default to **DataStore Preferences**, a separate storage system, so even an unprefixed key written by the legacy class is not visible to them. | | Legacy `SharedPreferences` | `SharedPreferencesAsync` / `WithCache` | |---|---|---| | Key on disk for `'languageCode'` | `flutter.languageCode` | `languageCode` | | Android backing store | Android `SharedPreferences` file | DataStore Preferences (default) | | iOS / macOS backing store | `NSUserDefaults` | `NSUserDefaults` | ## The migration utility Since 2.4.0 the package ships `migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary` in `package:shared_preferences/util/legacy_to_async_migration_util.dart`. It takes three required arguments: - `legacySharedPreferencesInstance` — a `SharedPreferences` obtained exactly as the app used it; if the app called `setPrefix`, that call must happen first. - `sharedPreferencesAsyncOptions` — the `SharedPreferencesOptions` (or a platform subclass) the new API will use from now on. - `migrationCompletedKey` — a key written to the **new** store when the copy finishes. What it does, in order: 1. If the new store already contains `migrationCompletedKey`, it returns immediately. 2. It calls `reload()` on the legacy instance and reads its keys. 3. For each value that is a `bool`, `int`, `double`, `String` or string list, it writes the same key, without the prefix, to the new store; lists that are not all strings are skipped. 4. It sets `migrationCompletedKey` to `true`. Because of the completion flag, it is safe to call on every launch. If the app is killed halfway, the flag is not yet set and the next launch simply copies again. ## Doing it safely - **Run it before any new-API read**, in `main` after `WidgetsFlutterBinding.ensureInitialized()`, so no screen sees the empty new store. - **Switch every call site in the same release.** After migration the two stores diverge: a value still written through the legacy class after the flag is set is never copied again. - **Pick a flag key that cannot collide** with a real preference, and never delete it; the source warns that altering it can cause data loss by re-running the copy over newer values. - **The legacy values are not deleted.** Clean them up in a later release if you care about the space, once you are sure no build still reads them. - **Test the upgrade path**, not just a fresh install: install the old build, save settings, upgrade, and check they survive. ## An alternative on Android If the goal is to read preferences written by code you do not control — native Android code, or an older app — you can point the new API at Android `SharedPreferences` instead of DataStore with `SharedPreferencesAsyncAndroidOptions(backend: SharedPreferencesAndroidBackendLibrary.SharedPreferences, originalSharedPreferencesOptions: AndroidSharedPreferencesStoreOptions(fileName: ...))` from `shared_preferences_android`. That solves a different problem; for a plain move off the legacy API, the migration utility is the intended path.

  • Why must setPrefix be called before running the migration if the app used a custom prefix?
    The utility reads keys through the legacy instance you pass. If that instance was created with the default `flutter.` prefix while the app actually stored keys under another prefix, it sees none of them and marks the migration complete with nothing copied.
  • What happens if some code still writes through the legacy class after the migration ran?
    That write goes to the old store and prefix, and the migration never runs again because the completion key is set. The new API never sees the value, so every call site has to move in the same release.

saying these in an interview costs you the question

  • The new API reads the same keys, so no migration is needed.
  • The problem only exists on Android; iOS values carry over automatically.
  • The migration utility deletes the legacy values after copying them.
  • It is unsafe to call the migration on every launch.
  • Changing the migrationCompletedKey later is harmless.