Skip to content
AB
3 min readIntermediate

Secure Storage and Shared Preferences Solve Different Problems

Choose the correct Flutter storage for tokens, preferences, onboarding flags, cached filters, and session-related data.

Ashish BhakhandPublished

Both secure storage and shared preferences save small values. That similarity can make them appear interchangeable.

They are not.

flutter_secure_storage uses platform-protected storage intended for sensitive values. shared_preferences stores simple preferences and explicitly should not be treated as storage for critical data.

The decision should begin with the value, not the API convenience.

Use secure storage for secrets#

Typical examples:

  • refresh tokens;
  • long-lived access credentials;
  • private keys;
  • a stable installation identifier when its exposure matters;
  • sensitive session material.
Code
class TokenStore {
  TokenStore(this._storage);

  final FlutterSecureStorage _storage;

  Future<void> saveRefreshToken(String token) {
    return _storage.write(
      key: 'refresh_token',
      value: token,
    );
  }

  Future<String?> readRefreshToken() {
    return _storage.read(key: 'refresh_token');
  }

  Future<void> clear() {
    return _storage.deleteAll();
  }
}

Do not expose the storage plugin throughout the app. A TokenStore gives authentication one place to define keys, migration, and cleanup.

Use preferences for user choices#

Appropriate values include:

  • selected theme;
  • onboarding completed;
  • preferred playback speed;
  • last selected filter;
  • dismissed tooltip version;
  • whether a non-sensitive hint was shown.
Code
class AppPreferences {
  AppPreferences(this._preferences);

  final SharedPreferences _preferences;

  static const _playbackSpeedKey = 'playback_speed';

  double get playbackSpeed =>
      _preferences.getDouble(_playbackSpeedKey) ?? 1.0;

  Future<void> setPlaybackSpeed(double value) {
    return _preferences.setDouble(
      _playbackSpeedKey,
      value,
    );
  }
}

These values improve experience but are not proof of authorization or ownership.

Never trust local flags for access#

This is unsafe:

Code
final isPremium = preferences.getBool('is_premium') ?? false;

A local preference can support optimistic UI, but the server remains the authority for subscriptions, permissions, purchases, and protected content.

The same applies to isLoggedIn. A stored flag may choose the first screen while credentials are checked; it cannot prove that a session is valid.

Plan logout explicitly#

Different stores need different cleanup policies:

Code
Future<void> logout() async {
  await tokenStore.clear();
  await userDatabase.clearPrivateData();
  await downloads.removeProtectedFiles();

  // Keep harmless choices such as theme and language.
}

Deleting every preference can make logout feel like reinstalling the app. Keeping protected cached data can expose another user's information. Classify stored values by ownership.

Handle missing data normally#

Mobile operating systems, backups, development installs, and platform errors can produce missing values. Reads should have safe behavior:

Code
final speed = await preferences.playbackSpeed;

Do not use a missing preference as an exceptional event when a sensible default exists. For a missing refresh token, return an unauthenticated session rather than crashing startup.

Version keys and migrate deliberately#

Renaming a Dart field does not rename persisted keys. Keep keys centralized and migrate old formats:

Code
Future<void> migratePreferences() async {
  final oldValue = preferences.getBool('intro_seen');

  if (oldValue != null &&
      !preferences.containsKey('onboarding_completed')) {
    await preferences.setBool(
      'onboarding_completed',
      oldValue,
    );
    await preferences.remove('intro_seen');
  }
}

For complex or growing data, use a database instead of encoding large JSON documents into preferences.

Test the policy, not the plugin#

Test:

  • defaults when a key is missing;
  • token cleanup on logout;
  • harmless preferences surviving logout;
  • migration from previous keys;
  • storage read failure during startup;
  • two users on the same installation;
  • reinstall and backup behavior relevant to supported platforms.

Secure storage reduces exposure; it does not make bad token lifecycle safe. Shared preferences make small choices convenient; they are not a database or authorization system. Naming wrappers after their responsibility keeps that difference visible.

← Back to all writing