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 boilerplate.

But most tutorials only show the basic get/set snippet and leave out the things that actually trip up beginners: loading preferences correctly in initState, the three APIs that now exist (including two that most tutorials don’t cover), why your Switch widget snaps back after being tapped, how to clear data safely on logout without deleting app settings, and when SharedPreferences is no longer the right tool.

This post covers all of it.

📋 Prerequisites

1. What SharedPreferences Is (and Isn’t)

SharedPreferences is a Flutter plugin that wraps each platform’s native key-value storage. It is not a database — it is a thin layer over whatever the operating system already provides for storing lightweight settings:

Platform What it actually uses
Android (new API)Jetpack DataStore Preferences (recommended by Google)
Android (legacy API)Android SharedPreferences XML file
iOS / macOSNSUserDefaults
WeblocalStorage
WindowsRoaming AppData directory file
LinuxXDG_DATA_HOME directory file

Understanding this explains SharedPreferences’ hard limits — it can only store 5 primitive types, has no query or filter capability, and has no write-persistence guarantee on all platforms. It is designed for settings, not data.

✅ Use SharedPreferences for ❌ Do NOT use SharedPreferences for
Dark/light mode preferenceAuth tokens or passwords
Onboarding “seen” flagCustom Dart objects (use Hive)
Selected language codeLists of objects (use Hive)
Login state booleanAny data you need to query/filter
Simple counters or scoresRelational data (use sqflite)
Last-used search queryCritical financial or health data

2. Setup and Installation

🚨 Critical: WidgetsFlutterBinding.ensureInitialized()
If you load SharedPreferences before runApp() — for example to pass initial preferences to your app — you must call WidgetsFlutterBinding.ensureInitialized() first. Skipping it causes a crash. This is one of the most common beginner errors:

void main() async {
  WidgetsFlutterBinding.ensureInitialized(); // ← always first
  final prefs = await SharedPreferences.getInstance();
  runApp(MyApp(prefs: prefs));
}

3. The Three APIs — Which One to Use

As of v2.3.0+ there are three distinct SharedPreferences APIs. Most online tutorials only show the legacy one, which is being deprecated. Here is what all three are and when to use each:

API Reads Status Use when
SharedPreferencesSynchronous (cached)⚠️ Being deprecatedExisting codebases only — avoid for new projects
SharedPreferencesAsyncAsync (no cache)✅ NewFirebase Messaging, background isolates, or when you always need fresh data
SharedPreferencesWithCacheSynchronous (allowList cache)✅ Recommended defaultMost new projects — fast reads + scoped cache

4. Core Operations: Read, Write, Remove, Clear

SharedPreferences supports exactly 5 primitive types. Here is the complete API for all of them across both the legacy and modern approaches:

5. Async Loading in initState — Three Correct Patterns

initState() is not async — you cannot use await directly inside it. But SharedPreferences operations all return Futures. This mismatch is the most-Googled SharedPreferences question on Stack Overflow. Here are all three correct solutions:

❌ The wrong way (compile error)

✅ Pattern 1 — Async helper method (simplest, most common)

✅ Pattern 2 — FutureBuilder (when loading state drives the whole UI)

✅ Pattern 3 — Pre-load in main() (cleanest for production)

💡 Which pattern should I use?
Pattern 1 (async helper) — use for most screens. Simple, readable, handles loading state per widget.
Pattern 2 (FutureBuilder) — use when the entire screen layout depends on loaded preferences.
Pattern 3 (pre-load in main) — use for app-level settings that drive navigation or theming. Eliminates loading states entirely.

6. Key Naming Conventions and the PrefKeys Pattern

Every SharedPreferences entry is stored under a string key. Typos in keys are silent bugs — getString('username') vs getString('user_name') both compile fine but one returns null. This section is missing from most tutorials and causes real production bugs.

💡 Key naming rules
Use camelCase to match Dart conventions (isDarkMode, not is_dark_mode). Use descriptive names, not abbreviations (hasSeenOnboarding, not hso). For SharedPreferencesWithCache, the allowList must match key strings exactly — a mismatch silently prevents access. Flutter’s legacy API also internally prepends flutter. to all keys (so your 'isDarkMode' is stored as 'flutter.isDarkMode' on disk) — you never see this in code but it matters when reading from native Android tools.

7. The AppPrefs Service Wrapper (Production Pattern)

For real apps, the cleanest pattern is a wrapper service that hides the SharedPreferences instance, centralises all keys, and exposes typed getters and setters. This eliminates magic strings entirely, makes unit testing trivial, and ensures defaults are always consistent:

8. The Switch Snapping Bug — Root Cause and Fix

This is the most viral SharedPreferences bug on Stack Overflow. You add a Switch or SwitchListTile that toggles dark mode. When you tap it, it briefly flips to the new position — then snaps straight back to where it was. Infuriating and not obvious why it happens.

The root cause

✅ Fix 1 — Local state variable (recommended)

✅ Fix 2 — Provider/Riverpod (for app-wide theme state)

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

SharedPreferences can’t store custom Dart objects directly. The workaround is JSON serialisation — convert your object to a Map, encode it as a String, store the string. This works, but has clear limits:

⚠️ When the JSON workaround becomes a code smell
Stop using SharedPreferences for objects when any of these are true:
• You’re storing objects with more than 5 fields
• You’re storing a list of objects (not just List<String>)
• You need to filter, sort, or search stored data
• You’re managing more than 2–3 different object types

At that point, migrate to Hive (hive_ce) — it stores typed objects natively with zero serialization boilerplate and far better performance.

10. Clearing Data on Logout

prefs.clear() wipes everything — including dark mode, language, and onboarding flags that the user expects to survive logout. This is a subtle but common bug. Use targeted removal instead:

11. Security Pitfalls

🚨 SharedPreferences stores everything in plaintext
All data written to SharedPreferences is readable on rooted Android devices and visible in iOS device backups. Never store: auth tokens, JWT tokens, passwords, PINs, API keys, payment information, or any personally identifiable information (PII). Use flutter_secure_storage for sensitive values instead.

12. Migrating from the Legacy API

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

13. Common Mistakes

Mistake What happens Fix
Using await directly in initStateCompile errorExtract to async method, call from initState without await
Reading prefs directly as Switch valueVisual snap-back bugUse local bool state variable; update state first, persist second
Scattering key strings across filesSilent null returns from key typosCentralise in PrefKeys class or AppPrefs wrapper
Calling prefs.clear() on logoutWipes dark mode and app settingsUse selective remove() or allowList clear
Storing tokens in SharedPreferencesSecurity vulnerability — plaintextUse flutter_secure_storage
Not calling WidgetsFlutterBinding.ensureInitialized()Crash before runAppAdd as first line of main() when using async before runApp
Legacy API with Firebase MessagingStale cached reads in background isolateSwitch to SharedPreferencesAsync (no cache)
Storing 5+ JSON objects in SharedPreferencesUnmaintainable data layerMigrate to Hive (hive_ce) for object storage
Inline FutureBuilder(future: getInstance())New future created on every rebuildStore the future in a field, assign in initState

14. Interview Q&A

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

A: SharedPreferences is a Flutter plugin that wraps each platform’s native key-value storage (NSUserDefaults on iOS, Jetpack DataStore on Android, localStorage on Web). It supports only 5 primitive types: int, double, bool, String, and List<String>. Use it for simple app settings and user preferences — dark mode, language selection, onboarding flags, login state. Do not use it for custom objects, relational data, sensitive credentials, or anything requiring querying or filtering.
Q: How do you load SharedPreferences in initState?

A: initState is not async so you can’t use await directly. The standard pattern is to define a separate Future<void> _loadPreferences() async method, call it from initState without await, and inside that method call setState() once data is loaded. Alternatively, pre-load preferences in main() before runApp() and pass them down as constructor arguments — this eliminates the loading state entirely.
Q: Why does my Switch snap back after being tapped?

A: This happens when the Switch reads its value directly from SharedPreferences (prefs.getBool('isDarkMode')). When you tap it, the async setBool hasn’t completed yet when Flutter rebuilds — so the Switch re-reads the old cached value and snaps back. The fix is to maintain a local bool _isDarkMode state variable. Call setState(() => _isDarkMode = val) first (synchronous, instant UI update), then persist with await prefs.setBool('isDarkMode', val) in the background. The Switch reads local state — it never snaps.
Q: What is the difference between SharedPreferences, SharedPreferencesAsync, and SharedPreferencesWithCache?

A: The legacy SharedPreferences has an in-memory cache and synchronous reads after getInstance() — being deprecated. SharedPreferencesAsync has no cache — every read goes to platform storage (async), always returns fresh data — use with Firebase Messaging or background isolates. SharedPreferencesWithCache wraps Async with a configurable allowList-based cache, giving synchronous reads like the legacy API — recommended for most new projects.
Q: How do you clear preferences on logout without deleting app settings?

A: Never use prefs.clear() — it deletes everything including dark mode and language preferences. Instead, use selective removal: call prefs.remove() only for user-session keys (auth token, user ID, email). With the new SharedPreferencesAsync API, use prefs.clear(allowList: {'authToken', 'userId'}) — it only clears the listed keys and leaves everything else intact. The cleanest approach is the AppPrefs service wrapper with a dedicated logout() method that documents exactly which keys are cleared.
Post Why it’s relevant
Flutter Local Storage ComparisonThe hub post — confirms whether SharedPreferences is the right tool for your use case before you dive into implementation.
Flutter Dark Mode ToggleThe Switch snapping bug in this post appears directly in the dark mode guide — the fix here solves the exact issue described there.
Notes App Part 2: Local PersistenceSee SharedPreferences used in a real project — the Notes app uses it to persist notes across restarts.
Dart Crash Course (Part 0)Every SharedPreferences operation is async — the Dart crash course covers async/await and Future exactly as needed here.
Flutter Widgets: Stateless vs StatefulUnderstanding initState and setState is the prerequisite for the async loading patterns in Section 5.
Show 1 Comment

1 Comment

Leave a Reply