Flutter SharedPreferences: Save User Data Without a Database (2026)

You don’t need a database to save a user’s dark mode preference, their username, or whether they’ve seen your onboarding screen. SharedPreferences handles all of this in under 10 lines of code — no SQL, no schema, no migrations.

But SharedPreferences is also one of the most misused packages in Flutter. Beginners store sensitive tokens in it (don’t), call it directly inside initState (crashes), read it as a Switch value (causes visual snap-back), and wipe it clean on logout (destroys the user’s settings). This guide covers all of it — the correct patterns, the bugs, the fixes, and exactly when to stop using SharedPreferences and reach for something else.

📋 Prerequisites

1. What SharedPreferences Is — and Isn’t

SharedPreferences is a Flutter plugin that wraps each platform’s native key-value storage engine. It is not a Flutter invention — it delegates to platform-specific storage that already exists on every device:

Platform Native backend
Android (legacy API)Android SharedPreferences
Android (new Async/WithCache API)Jetpack DataStore Preferences (Google’s recommendation)
iOS / macOSNSUserDefaults
WeblocalStorage
WindowsRoaming AppData directory
LinuxXDG_DATA_HOME directory

Because it delegates to platform storage designed for lightweight settings, SharedPreferences has hard constraints you must understand before using it:

✅ SharedPreferences is the right choice for:
  • Dark mode / theme preference
  • Onboarding “has seen” flag
  • Selected language code
  • Last logged-in email (to pre-fill the login form)
  • Simple counters (notification badge count, app open count)
  • Font size or display preference
  • Sort order preference in a list

2. Installation and Setup

🚨 Always call WidgetsFlutterBinding.ensureInitialized() first
If you use SharedPreferences before runApp() (e.g. to pre-load preferences in main()), you must call WidgetsFlutterBinding.ensureInitialized() as the very first line of main(). Without it, Flutter’s platform channels aren’t ready and you’ll get a runtime crash.

3. The Three APIs: Which One Should You Use?

Since shared_preferences v2.3.0 (Flutter 3.24+), there are three distinct APIs. Most tutorials still only show the legacy one — but the official team has flagged it for deprecation. Here’s what all three are and when to use each:

API Cache Reads Status Use when
SharedPreferencesIn-memorySync (after getInstance)⚠️ Being deprecatedExisting projects — don’t use for new code
SharedPreferencesAsyncNoneAlways async✅ New, maintainedFirebase Messaging in the stack, background isolates
SharedPreferencesWithCacheallowList cacheSync (from cache)✅ New, recommendedMost new projects — best of both worlds
💡 Which API should I use in 2026?
For most new Flutter projects: use SharedPreferencesWithCache. It gives you synchronous reads (like the legacy API), async writes, and a safe scoped cache. Only switch to SharedPreferencesAsync if you have Firebase Messaging or a background isolate that also writes preferences — in that case, you need reads that always hit disk, not a potentially stale cache.

4. All 5 Data Types: Read, Write, Remove, Clear

SharedPreferences supports exactly five primitive types. Here is every operation for all five, using the recommended SharedPreferencesWithCache API:

5. Async Loading in initState — The 3 Correct Patterns

This is one of the most-searched SharedPreferences questions on Stack Overflow. The problem: initState() is not async, but loading preferences requires a Future. Here are all three correct patterns:

Pattern 1: Async method called from initState (Recommended)

Define the loading logic in a separate Future<void> method. Call it from initState without await (fire-and-forget). Use setState inside the async method to trigger a rebuild when data arrives:

Pattern 2: Pre-load in main() and pass down

The cleanest pattern for production apps — load preferences before the first frame, pass the loaded instance down through the widget tree. No loading spinner, no initState complexity:

Pattern 3: FutureBuilder (for isolated widget loading)

Use FutureBuilder when the preference determines the entire widget subtree and you don’t want a loading state at the page level. Store the Future in a field — never call getInstance() directly in the future: parameter:

6. Key Naming Conventions and the AppPrefs Wrapper

Every SharedPreferences entry is stored under a string key. Typos in keys are silent bugsgetString('username') and getString('user_name') both compile fine but return null for the wrong key with no error. This is the most common cause of unexplained null values in beginner Flutter apps.

Step 1: Centralise all keys in a constants class

Step 2: The AppPrefs Wrapper Service (for larger apps)

For apps with more than a handful of preferences, wrap SharedPreferences in a typed service class. This gives you a single source of truth, typed getters and setters with baked-in defaults, and makes it easy to unit test (inject a mock):

Key Naming Rules

Rule Example
Use camelCase (matches Dart conventions)isDarkMode not is_dark_mode
Be descriptive, avoid abbreviationshasSeenOnboarding not hso
Group multi-user keys with a prefixuser_alice_theme
allowList keys must exactly match stored keysallowList: {'isDarkMode'} not {'is_dark'}
Internal flutter. prefix (legacy API)Your 'isDarkMode' is stored as 'flutter.isDarkMode' on disk — you never see this in code but native Android tools do

7. The Switch Snapping Bug — Root Cause and Fix

This is the most viral SharedPreferences bug. When you use a Switch or SwitchListTile to toggle a preference, the toggle visually snaps back to its old position immediately after the user taps it — even though the preference was saved correctly. Here is exactly why it happens and how to fix it.

Why it snaps

Fix 1: Separate local state from storage (Recommended)

Fix 2: Provider / Riverpod (for app-wide dark mode)

8. Storing Objects via JSON (and When to Stop)

SharedPreferences only supports primitives. If you need to store a custom Dart object, the workaround is to serialize it to a JSON string. This works for one or two simple objects — beyond that, you’ve outgrown SharedPreferences:

⚠️ When to stop using SharedPreferences for objects
The JSON workaround becomes a code smell quickly. Stop using SharedPreferences and switch to Hive (hive_ce) when:
  • You’re serialising objects with more than 5 fields
  • You’re storing a list of objects (not just a single object)
  • You’re querying or filtering stored objects
  • You find yourself writing manual JSON parsing in multiple places
Hive handles all of these with TypeAdapters — see the local storage comparison post for when to make the switch.

9. Clearing on Logout — remove() vs clear() vs allowList

prefs.clear() wipes everything — including dark mode, language, and other app settings the user expects to survive logout. This is a subtle but common bug. There are three approaches:

10. Security Pitfalls

SharedPreferences stores data as plaintext — never store secrets

The Multi-Isolate Firebase Messaging Cache Bug

11. Migrating from Legacy to the New API

If you have an existing app using the legacy SharedPreferences.getInstance() API and want to migrate to SharedPreferencesAsync, the package ships an official migration utility. It is idempotent — safe to call on every launch:

12. Common Mistakes Reference Table

Mistake What happens Fix
Using await directly in initStateDart compile errorExtract to async method, call without await
Reading prefs directly as Switch valueVisual snap-back on toggleUse local bool state variable; update UI first, persist second
Scattering key strings across filesSilent null returns from typosCentralise in PrefKeys class or AppPrefs wrapper
Calling prefs.clear() on logoutWipes dark mode and app settingsUse selective remove() or allowList clear
Storing tokens/passwords in SharedPreferencesSecurity vulnerability — plaintext on diskUse flutter_secure_storage
Using legacy API with Firebase MessagingStale cached reads in UI isolateSwitch to SharedPreferencesAsync or call prefs.reload()
Not calling WidgetsFlutterBinding.ensureInitialized()Crash when using prefs before runAppAdd as first line of main()
Calling getInstance() inside FutureBuilder(future:)New Future on every rebuild → flickering / multiple readsStore the Future in a field in initState
Storing 3+ objects via JSON serialisationUnmaintainable, error-proneMigrate to Hive (hive_ce)

13. Interview Q&A

Q: What is SharedPreferences in Flutter and when should you use it?

A: SharedPreferences is a Flutter plugin that wraps platform-native key-value storage (NSUserDefaults on iOS, Android SharedPreferences or Jetpack DataStore on Android, localStorage on Web). It stores five primitive types: int, double, bool, String, and List<String>. Use it for app settings and user preferences — dark mode, language selection, onboarding flags, last-used email. Don’t use it for objects, sensitive data, or anything that needs querying or filtering.
Q: Why can’t you use await directly in initState?

A: initState() is a synchronous lifecycle method — Flutter calls it synchronously as part of widget insertion, and it is not declared async. The fix is to extract the async work into a separate Future<void> method and call that from initState() without await (fire-and-forget). Inside the async method, use setState() to update the widget once data is ready.
Q: What causes the Switch snapping bug with SharedPreferences?

A: The bug occurs when the Switch widget reads its value directly from SharedPreferences (e.g. value: prefs.getBool('isDarkMode') ?? false). When the user taps, the async write fires but hasn’t completed by the time Flutter rebuilds — so prefs.getBool returns the old cached value and the switch snaps back. The fix is to use a local bool state variable as the source of truth for the UI: update local state with setState immediately on tap, then persist to SharedPreferences asynchronously in the background.
Q: What is the difference between SharedPreferences, SharedPreferencesAsync, and SharedPreferencesWithCache?

A: SharedPreferences is the legacy API — it uses an in-memory cache so reads are synchronous, but it’s being deprecated. SharedPreferencesAsync is a new API with no cache — every read hits platform storage, making it always fresh but fully async. SharedPreferencesWithCache is the recommended replacement — it combines an allowList-scoped in-memory cache (fast synchronous reads) with async writes. Use SharedPreferencesWithCache for most new projects; use SharedPreferencesAsync if Firebase Messaging or background isolates also write to the same storage.
Q: Should you use SharedPreferences.clear() on logout?

A: Usually no. clear() removes every stored key — including app-level settings like dark mode and language that the user expects to survive between sessions. The correct approach is selective: use prefs.remove() for each user-specific key (auth token, email, session flags) while leaving app-level preferences untouched. With the new SharedPreferencesAsync API, you can use clear(allowList: {'key1', 'key2'}) to remove only specific keys cleanly.
Post Why it’s relevant
Flutter Local Storage: SharedPreferences vs Hive vs sqfliteThe hub comparison post — read this first if you’re not sure SharedPreferences is the right choice for your use case.
Flutter Dark Mode ToggleUses SharedPreferences to persist the theme preference — the most common real-world use case for this guide.
Notes App Part 2: Local PersistenceA complete project using SharedPreferences — see every pattern from this guide in context.
Flutter Widgets: Stateless vs StatefulThe initState loading pattern depends on understanding the StatefulWidget lifecycle — read this first.
Dart Crash Course (Part 0)SharedPreferences uses async/await throughout — the Dart crash course covers both in depth.
Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply