Flutter shared_preferences: Save Simple App Data

Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter shared_preferences: Save Simple App Data, with getinstance with setx and getx, safe defaults, remove, and clear, and asynchronous initialisation before rendering dependent ui.
Coding Liquids blog cover featuring Sagnik Bhattacharya for Flutter shared_preferences: Save Simple App Data, with getinstance with setx and getx, safe defaults, remove, and clear, and asynchronous initialisation before rendering dependent ui.

A video run makes the cold-start theme flash—and its fix—immediately obvious.

Subscribe on YouTube@codingliquids

Use preferences for small durable choices

shared_preferences stores primitive key-value data such as booleans, numbers, strings, and string lists. Good examples include theme mode, a dismissed tip, selected units, or the last non-sensitive filter.

Large JSON documents, secrets, and records requiring queries belong in different storage.

flutter pub add shared_preferences

Read and write every supported value type

Obtain SharedPreferences with getInstance, then use setBool, setInt, setDouble, setString, or setStringList. Reads are synchronous on the legacy instance after initialisation, and nullable getters should always receive an explicit default.

Casting one key to a different type after an app update can throw and needs a migration plan.

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: any

Instagram’s settings checklist covers defaults, migration, failed writes, and the difference between preferences and secrets.

Follow me on Instagram@sagnikteaches

Wrap keys in a typed SettingsService

A service prevents spelling variants such as dark-mode and darkMode from becoming two independent settings. Expose domain methods like setTextScale and onboardingComplete rather than handing SharedPreferences to widgets.

Scattered string keys make removal, testing, and future storage replacement needlessly risky.

The Complete Flutter Guide course thumbnail

Build production-ready shared_preferences features

The Complete Flutter Guide turns shared_preferences into maintainable app architecture, polished UI, and testable production code.

Enrol now

Import shared_preferences where the settings service is created; widgets can depend on that typed service instead.

import 'package:shared_preferences/shared_preferences.dart';

Remove one preference or reset them all

remove deletes one named choice while clear erases every key owned by the preferences store. Use remove when a value should fall back to its default, and reserve clear for a deliberate reset flow.

Calling clear on logout may erase device-level preferences that should survive account changes.

import 'package:shared_preferences/shared_preferences.dart';

class SettingsStore {
  SettingsStore(this.preferences);
  final SharedPreferencesAsync preferences;

  Future<bool> isOnboardingComplete() async =>
      await preferences.getBool('onboardingComplete') ?? false;

  Future<void> completeOnboarding() =>
      preferences.setBool('onboardingComplete', true);

  Future<void> reset() => preferences.remove('onboardingComplete');
}

Load required preferences before runApp

WidgetsFlutterBinding.ensureInitialized allows main to await SharedPreferences.getInstance before constructing the app. Passing the ready SettingsService into the root avoids a flash of the default theme before the saved mode is applied.

Blocking startup for preferences that are not visible on the first screen only increases time to first frame.

final asyncPrefs = SharedPreferencesAsync();
await asyncPrefs.setString('themeMode', 'dark');
final savedMode = await asyncPrefs.getString('themeMode') ?? 'system';

final cachedPrefs = await SharedPreferencesWithCache.create(
  cacheOptions: const SharedPreferencesWithCacheOptions(
    allowList: {'themeMode', 'textScale'},
  ),
);
final textScale = cachedPrefs.getDouble('textScale') ?? 1.0;

A LinkedIn note unpacks the architectural cost of letting preference keys leak into widgets.

Connect on LinkedInSagnik Bhattacharya
The Complete Flutter Guide course thumbnail

The Complete Flutter Guide: Build Android, iOS and Web apps

Go from scratch to building industry-standard apps with Riverpod, Firebase, animations, REST APIs, and more.

Enrol now

Choose Async or WithCache for new code

SharedPreferencesAsync reads through the platform store without a long-lived Dart cache, while SharedPreferencesWithCache offers allow-listed cached keys. WithCache must be created asynchronously and its cache can be refreshed when another isolate or native component writes values.

The legacy singleton cache can become stale across isolates or multiple engine instances.

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SettingsService {
  SettingsService(this._preferences);
  final SharedPreferences _preferences;

  bool get darkMode => _preferences.getBool('darkMode') ?? false;
  int get pageSize => _preferences.getInt('pageSize') ?? 20;
  String get locale => _preferences.getString('locale') ?? 'en-GB';
  List<String> get favourites => _preferences.getStringList('favourites') ?? const [];

  Future<void> setDarkMode(bool value) => _preferences.setBool('darkMode', value);
  Future<void> removeLocale() => _preferences.remove('locale');
  Future<void> reset() => _preferences.clear();
}

Move secrets and structured data elsewhere

Authentication tokens belong in flutter_secure_storage, queryable records belong in SQLite or Drift, and document-sized exports belong in files. Preferences do not encrypt values and offer no transactions across related keys.

Base64 encoding a secret changes its representation but provides no protection.

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final preferences = await SharedPreferences.getInstance();
  final settings = SettingsService(preferences);
  runApp(MaterialApp(
    themeMode: settings.darkMode ? ThemeMode.dark : ThemeMode.light,
    home: const Scaffold(body: Center(child: Text('Settings loaded'))),
  ));
}

Treat preferences as a small durable interface

Preference keys become part of the app’s data format as soon as a release reaches users. Collect them in one repository, give each value a declared default, and avoid scattering string literals through widgets. A renamed key otherwise looks exactly like a first launch. When a setting changes meaning, read the old value once, translate it, write the new key, and mark the migration complete. This keeps upgrades predictable without making the presentation layer understand historical storage.

Writes are asynchronous and should be reflected honestly. Update the control immediately only if rolling it back on failure is acceptable, or wait for the repository result before confirming a sensitive change. Rapid toggles can complete out of order, so serialise them or save only the latest intended value. The plugin is suitable for modest preferences, not an event log or a large cached response. Values that require queries, transactions, or relationships belong in a database.

Test persistence with more than a hot reload. Change a setting, kill the process, launch again, and verify that the first rendered frame uses the stored choice without a visible flash of the default. Also cover corrupt or unexpected values left by older builds. A string-based enum should fall back safely when it contains a removed option. Clearing account data needs an explicit list of account-scoped keys so device-wide choices such as theme or accessibility are not erased accidentally.

Shared preferences do not provide a secrecy boundary. Session tokens, refresh tokens, and personal secrets need platform-backed secure storage, while synchronised user settings may belong on the server. For ordinary flags, log only the key and success state when diagnosing a write; avoid recording the value if it can reveal behaviour. A thin fake implementation makes repository tests deterministic and lets the UI exercise read failure, write failure, and migration paths that are awkward to reproduce with the real platform store.

Multi-isolate or multi-engine applications require extra care because an in-memory preference cache can become stale outside the writer that owns it. Keep one repository as the normal write path, refresh before a read that must reflect external changes, and consult the plugin’s current API guarantees for the chosen platform. A settings screen should also expose failures without trapping the person: retain the last known value, mark the unsuccessful save, and let them retry. For testing, seed a migration with the exact keys from a released build and assert the final stored map, not only the value returned during the same process.

Common mistakes

  • Use preferences for small durable choices: In shared preferences, large JSON documents, secrets, and records requiring queries belong in different storage; inspect this shared preferences cause before changing another shared preferences widget.
  • Read and write every supported value type: In shared preferences, casting one key to a different type after an app update can throw and needs a migration plan; inspect this shared preferences cause before changing another shared preferences widget.
  • Wrap keys in a typed SettingsService: In shared preferences, scattered string keys make removal, testing, and future storage replacement needlessly risky; inspect this shared preferences cause before changing another shared preferences widget.
  • Remove one preference or reset them all: In shared preferences, calling clear on logout may erase device-level preferences that should survive account changes; inspect this shared preferences cause before changing another shared preferences widget.
  • Load required preferences before runApp: In shared preferences, blocking startup for preferences that are not visible on the first screen only increases time to first frame; inspect this shared preferences cause before changing another shared preferences widget.
  • Choose Async or WithCache for new code: In shared preferences, the legacy singleton cache can become stale across isolates or multiple engine instances; inspect this shared preferences cause before changing another shared preferences widget.

Frequently asked questions

How does use preferences for small durable choices work in shared preferences?

For shared preferences, the starting rule is that shared_preferences stores primitive key-value data such as booleans, numbers, strings, and string lists. Apply this shared preferences rule first because use preferences for small durable choices determines whether the shared preferences pattern fits.

Why does wrap keys in a typed SettingsService matter for shared preferences?

In shared preferences, expose domain methods like setTextScale and onboardingComplete rather than handing SharedPreferences to widgets. Keeping wrap keys in a typed SettingsService at the shared preferences call site exposes the shared preferences return value directly.

What failure should I test first in shared preferences?

First reproduce the shared preferences case where blocking startup for preferences that are not visible on the first screen only increases time to first frame. A controlled restart should load only the preferences needed for the first frame and defer the rest without a visual flash.

How can I verify shared preferences before release?

Exercise move secrets and structured data elsewhere with real shared preferences inputs on every shipped platform. Inspect the final shared preferences callback or output; a successful shared preferences button tap alone is not proof.

Further reads

Connect shared preferences to the surrounding Flutter stack through these published tutorials:

Sources: Flutter documentation and the package documentation on pub.dev; shared preferences examples verified against current stable Flutter.