Shared preferences vs secure storage: when to use which
By John · 6 October 2025
Flutter has two main local storage options for user preferences and settings: SharedPreferences for simple key-value data, and flutter_secure_storage for sensitive data. Picking the wrong one is a common security mistake.
SharedPreferences
SharedPreferences (backed by NSUserDefaults on iOS and SharedPreferences XML on Android) stores data as plaintext on disk.
final prefs = await SharedPreferences.getInstance();
// Write
await prefs.setString('theme', 'dark');
await prefs.setBool('notifications_enabled', true);
await prefs.setInt('onboarding_step', 3);
await prefs.setStringList('recent_searches', ['flutter', 'dart']);
// Read
final theme = prefs.getString('theme') ?? 'system';
final notificationsOn = prefs.getBool('notifications_enabled') ?? true;
// Delete
await prefs.remove('theme');
await prefs.clear(); // Remove everything
Secure storage
flutter_secure_storage stores data encrypted in the platform's protected storage (iOS Keychain, Android EncryptedSharedPreferences).
const storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device),
);
// Same API — write, read, delete
await storage.write(key: 'auth_token', value: token);
final token = await storage.read(key: 'auth_token');
await storage.delete(key: 'auth_token');
await storage.deleteAll();
When to use which
SharedPreferences:
✅ UI preferences (theme, language, font size)
✅ Feature flags and settings
✅ Onboarding progress, tutorial completion
✅ Last-selected tab, sort order
✅ User-facing preferences the user set
❌ Auth tokens, API keys
❌ PINs, passwords, encryption keys
❌ Personal data (account numbers, addresses)
Secure Storage:
✅ Auth tokens (access token, refresh token)
✅ API keys used at runtime
✅ Encryption keys for local database
✅ Biometric enrollment data
✅ Session identifiers
❌ Large datasets (slow, not designed for it)
❌ Non-sensitive settings (unnecessary overhead)
Why it matters
On an unrooted Android device, SharedPreferences XML is in /data/data/com.example.app/shared_prefs/ — accessible only to the app. But:
- On rooted devices, any app with root access can read it
- Backup & restore (
adb backup) copies SharedPreferences - Device exploits and forensic tools can extract it
- Developer tools can read it without root during development
An auth token in SharedPreferences means an attacker with brief physical access to a rooted device can extract it. In secure storage, the key is protected by the Android Keystore hardware module — extracting it requires defeating secure enclave, which is not practical.
Typed preferences wrapper
Avoid magic strings and type errors with a typed wrapper:
class AppPreferences {
AppPreferences(this._prefs);
final SharedPreferences _prefs;
// Theme
static const _themeKey = 'theme_mode';
ThemeMode get themeMode {
final value = _prefs.getString(_themeKey);
return ThemeMode.values.firstWhere(
(m) => m.name == value,
orElse: () => ThemeMode.system,
);
}
Future<void> setThemeMode(ThemeMode mode) =>
_prefs.setString(_themeKey, mode.name);
// Language
static const _langKey = 'language_code';
String get languageCode => _prefs.getString(_langKey) ?? 'en';
Future<void> setLanguageCode(String code) =>
_prefs.setString(_langKey, code);
// Onboarding
bool get hasCompletedOnboarding =>
_prefs.getBool('onboarding_done') ?? false;
Future<void> markOnboardingComplete() =>
_prefs.setBool('onboarding_done', true);
}
// Riverpod provider
@riverpod
Future<AppPreferences> appPreferences(AppPreferencesRef ref) async {
final prefs = await SharedPreferences.getInstance();
return AppPreferences(prefs);
}
Migrating data between versions
When your key names or data format changes between app versions:
Future<void> migratePreferences(SharedPreferences prefs) async {
// v1 stored theme as int, v2 stores as string name
final legacyTheme = prefs.getInt('theme_index');
if (legacyTheme != null) {
final modeName = ThemeMode.values[legacyTheme].name;
await prefs.setString('theme_mode', modeName);
await prefs.remove('theme_index');
}
}
Common pitfalls
Storing auth tokens in SharedPreferences. This is the most common mistake. Any token that authenticates a user should live in secure storage. The ~1ms overhead per read is negligible compared to the network call that follows.
Reading SharedPreferences synchronously after the first load. After the first await SharedPreferences.getInstance(), all reads are synchronous in memory — you don't need to await individual getString calls. But the initial getInstance() must be awaited.
Not clearing secure storage on logout. Calling prefs.clear() clears SharedPreferences but not secure storage. Always call storage.deleteAll() on logout to clear tokens.
Sign in to like, dislike, or report.