If you're building a Flutter app, you'll eventually need to store small pieces of data locally. User settings, theme preferences, auth tokens, onboarding flags. Shared Preferences is the simplest way to handle this without setting up a full database.

I've been using it in every Flutter project I've worked on, and it's one of those packages that just works. Here's everything you need to know to get started.

What Are Shared Preferences?

Shared Preferences provides a way to store key-value pairs on the device. It wraps NSUserDefaults on iOS and SharedPreferences on Android. The data persists across app restarts, which makes it perfect for lightweight storage.

It supports these types: int, double, bool, String, and List<String>. If you need to store complex objects, you'll want to serialize them to JSON strings first.

Setting It Up

Add the dependency to your pubspec.yaml:

dependencies:
  shared_preferences: ^2.0.15

Then run flutter pub get and import it:

import 'package:shared_preferences/shared_preferences.dart';

Saving Data

Getting an instance is async, so you'll use await:

Future<void> saveUsername(String username) async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setString('username', username);
}

Each type has its own setter:

await prefs.setInt('login_count', 5);
await prefs.setBool('dark_mode', true);
await prefs.setDouble('font_size', 16.0);
await prefs.setStringList('tags', ['flutter', 'dart']);

Reading Data

Reading follows the same pattern. Always provide a fallback value since the key might not exist yet:

Future<String> getUsername() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getString('username') ?? 'Guest';
}

Future<bool> isDarkMode() async {
  final prefs = await SharedPreferences.getInstance();
  return prefs.getBool('dark_mode') ?? false;
}

Removing Data

// Remove a specific key
await prefs.remove('username');

// Clear everything
await prefs.clear();

Practical Example: Theme Persistence

Here's how I use it to remember the user's theme choice. This is probably the most common use case I've seen:

class ThemeService {
  static const String _key = 'is_dark_mode';

  Future<bool> isDarkMode() async {
    final prefs = await SharedPreferences.getInstance();
    return prefs.getBool(_key) ?? false;
  }

  Future<void> setDarkMode(bool value) async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.setBool(_key, value);
  }
}

Then in your app's main(), load the preference before building the widget tree:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final themeService = ThemeService();
  final isDark = await themeService.isDarkMode();
  runApp(MyApp(isDarkMode: isDark));
}

Things to Watch Out For

  • Don't store sensitive data. Shared Preferences is not encrypted. Use flutter_secure_storage for tokens and passwords.
  • Don't store large amounts of data. It's meant for simple preferences. If you're storing lists of hundreds of items, use SQLite or Hive instead.
  • The getInstance() call is cached after the first call, so subsequent calls are fast. But I still recommend creating a service wrapper to keep your code clean.
  • Always handle null. If a key doesn't exist, getters return null. Use the ?? operator to set defaults.

When to Use Something Else

Shared Preferences is great for simple key-value storage. But if you need relational data, complex queries, or large datasets, look at sqflite for SQLite or Hive for a lightweight NoSQL option.

For most apps though, Shared Preferences handles 80% of local storage needs. I use it for onboarding states, user preferences, cached API responses, and feature flags. It's simple, reliable, and I've never had an issue with it in production.

If you're working with Go on the backend, check out my posts on session management with Redis and PostgreSQL transactions in Go for the server side of data persistence.