Kodokon kodokon.com

Local persistence with shared_preferences

Persist preferences and lightweight data with shared_preferences while knowing its security and volume limits.

8 min · 3 questions

Open this lesson in Kodokon

shared_preferences is a key/value store backed by native mechanisms (SharedPreferences on Android, UserDefaults on iOS). Its sweet spot: small data - chosen theme, onboarding seen, last active tab. Beyond that, switch tools: a sqflite or drift database for large or queryable data, flutter_secure_storage for anything sensitive. The file is loaded entirely into memory: it is neither encrypted nor built to grow.

BASH
flutter pub add shared_preferences

getInstance() is asynchronous on the first call (reading the file), then returns a cached instance: the subsequent get* calls are synchronous, served from memory. The set* calls, on the other hand, write to disk asynchronously. In practice, don't scatter key strings all over the application: centralize them in a typed, injectable, testable wrapper.

DART
import 'package:shared_preferences/shared_preferences.dart';

class SettingsStore {
  SettingsStore(this._prefs);

  static const _darkModeKey = 'settings.darkMode';

  final SharedPreferences _prefs;

  static Future<SettingsStore> load() async {
    final prefs = await SharedPreferences.getInstance();
    return SettingsStore(prefs);
  }

  bool get darkMode =>
      _prefs.getBool(_darkModeKey) ?? false;

  Future<void> setDarkMode(bool value) =>
      _prefs.setBool(_darkModeKey, value);
}
Centralized keys, guaranteed types, injectable dependency.

The API only stores primitive types: bool, int, double, String, and List<String>. For a structured object, serialize it to JSON - your toJson/fromJson from the first lesson come in handy again here. An accepted trade-off: each save rewrites the entire value and no querying is possible. Perfect for a draft, unsuited to a history.

DART
import 'dart:convert';

import 'package:shared_preferences/shared_preferences.dart';

class Draft {
  const Draft({required this.title, required this.body});

  factory Draft.fromJson(Map<String, dynamic> json) =>
      Draft(
        title: json['title'] as String,
        body: json['body'] as String,
      );

  final String title;
  final String body;

  Map<String, dynamic> toJson() =>
      {'title': title, 'body': body};
}

Future<void> saveDraft(Draft draft) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('draft', jsonEncode(draft));
}

Future<Draft?> loadDraft() async {
  final prefs = await SharedPreferences.getInstance();
  final raw = prefs.getString('draft');
  if (raw == null) return null;
  return Draft.fromJson(
    jsonDecode(raw) as Map<String, dynamic>,
  );
}
A complete object stored as a simple JSON string.

Knowledge check

Make sure you remember the key points of this lesson.

  1. Which data belongs in shared_preferences?
    • The user's authentication token.
    • The user's complete history of 50,000 transactions.
    • The dark theme choice.
  2. After the first getInstance(), how do the get* reads behave?
    • Each read re-reads the file from disk.
    • They are synchronous: the values are served from a memory cache.
    • They return Futures you must await every time.
  3. How do you save a structured Draft object?
    • Serialize it to JSON with jsonEncode and store it via setString.
    • Use prefs.setObject('draft', draft) designed for this purpose.
    • Split it field by field into a setStringList.