How to Save Data Locally in Flutter with SharedPreferences

Save settings on the device with SharedPreferences in Flutter: store bools, strings and numbers, load data before the app starts, remember dark mode, show onboarding once, and save objects as JSON.

By Yaqoob Developer · · 9 min read

  • #Flutter
  • #SharedPreferences
  • #Local Storage
  • #Dark Mode
  • #Beginners

When you close a Flutter app, every variable is lost. If the user turned on dark mode, typed their name or finished onboarding, the app forgets it all on the next launch.

SharedPreferences fixes this. It saves small pieces of data on the device so your app can remember them. In this guide you will build a settings screen that remembers dark mode and the user's name, even after the app is closed.

In this post you will learn:

  • What SharedPreferences is and what it should (and should not) store
  • How to save and read bool, int, double, String and List<String>
  • How to load saved data when the app starts
  • How to build a settings screen with a dark mode switch that is remembered
  • How to show onboarding only once
  • How to save a small object as JSON
  • Common mistakes and how to fix them

What is SharedPreferences?

SharedPreferences stores simple key-value pairs on the device, like a tiny dictionary that survives app restarts:

'darkMode'   → true
'userName'   → 'Ali'
'openCount'  → 7

On Android it uses the platform's preference storage, on iOS NSUserDefaults, and on the web the browser's localStorage. The shared_preferences package, made by the Flutter team, gives you one simple API for all of them.

What to store (and what not to)

Good for SharedPreferencesUse something else
dark mode on/offpasswords, tokens, API keys → flutter_secure_storage
language choicelists of hundreds of items → a database like sqflite or drift
"onboarding seen" flagimages and files → the file system
user name, last searchdata that must never be lost → a backend or Firebase

SharedPreferences is not encrypted, and it is meant for small data. Think "settings", not "database".

Step 1: Add the package

flutter pub add shared_preferences

If the app was running, stop it and run it again. New plugins need a full restart.

Step 2: Save and read data

The package offers a modern API called SharedPreferencesAsync. Every call is async, so you use await:

import 'package:shared_preferences/shared_preferences.dart';

final prefs = SharedPreferencesAsync();

// Save
await prefs.setBool('darkMode', true);
await prefs.setString('userName', 'Ali');
await prefs.setInt('openCount', 7);
await prefs.setDouble('fontScale', 1.2);
await prefs.setStringList('recentSearches', ['flutter', 'dart']);

// Read (returns null if nothing was saved yet)
final bool? darkMode = await prefs.getBool('darkMode');
final String? name = await prefs.getString('userName');
final int? openCount = await prefs.getInt('openCount');

// Delete one value
await prefs.remove('userName');

Every getter returns a nullable value (bool?, String?) because the key might not exist yet, for example on the very first launch. Use ?? to give a default value:

final darkMode = await prefs.getBool('darkMode') ?? false;
final name = await prefs.getString('userName') ?? 'Guest';

Supported types

TypeSaveRead
boolsetBoolgetBool
intsetIntgetInt
doublesetDoublegetDouble
StringsetStringgetString
List<String>setStringListgetStringList

Anything else, such as a custom object or a DateTime, must be converted to one of these types first. We will see how later.

Note: You may also see older tutorials using SharedPreferences.getInstance(). That is the legacy API. It still works, but the package authors recommend SharedPreferencesAsync (or SharedPreferencesWithCache) for new code.

Step 3: Keep your keys in one place

Typing 'darkMode' in many files invites typos. A typo means you save to one key and read from another, and nothing seems to be saved. Put keys in constants:

class PrefKeys {
  static const darkMode = 'darkMode';
  static const userName = 'userName';
  static const openCount = 'openCount';
  static const onboardingSeen = 'onboardingSeen';
}

Step 4: Build a settings screen that remembers

Here is a complete app. It has a settings screen with a dark mode switch and a name field. Close the app, open it again, and everything is exactly as you left it. It also counts how many times the app has been opened.

Replace lib/main.dart with:

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class PrefKeys {
  static const darkMode = 'darkMode';
  static const userName = 'userName';
  static const openCount = 'openCount';
}

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  final prefs = SharedPreferencesAsync();

  // Load saved values before the first frame, so there is no "flash"
  // of the wrong theme.
  final darkMode = await prefs.getBool(PrefKeys.darkMode) ?? false;
  final userName = await prefs.getString(PrefKeys.userName) ?? '';
  final openCount = (await prefs.getInt(PrefKeys.openCount) ?? 0) + 1;
  await prefs.setInt(PrefKeys.openCount, openCount);

  runApp(SettingsApp(
    prefs: prefs,
    initialDarkMode: darkMode,
    initialName: userName,
    openCount: openCount,
  ));
}

class SettingsApp extends StatefulWidget {
  const SettingsApp({
    super.key,
    required this.prefs,
    required this.initialDarkMode,
    required this.initialName,
    required this.openCount,
  });

  final SharedPreferencesAsync prefs;
  final bool initialDarkMode;
  final String initialName;
  final int openCount;

  @override
  State<SettingsApp> createState() => _SettingsAppState();
}

class _SettingsAppState extends State<SettingsApp> {
  late bool _darkMode = widget.initialDarkMode;

  Future<void> _setDarkMode(bool value) async {
    setState(() => _darkMode = value);          // update the UI now
    await widget.prefs.setBool(PrefKeys.darkMode, value); // and remember it
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(colorSchemeSeed: Colors.indigo, useMaterial3: true),
      darkTheme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        brightness: Brightness.dark,
        useMaterial3: true,
      ),
      themeMode: _darkMode ? ThemeMode.dark : ThemeMode.light,
      home: SettingsScreen(
        prefs: widget.prefs,
        darkMode: _darkMode,
        onDarkModeChanged: _setDarkMode,
        initialName: widget.initialName,
        openCount: widget.openCount,
      ),
    );
  }
}

class SettingsScreen extends StatefulWidget {
  const SettingsScreen({
    super.key,
    required this.prefs,
    required this.darkMode,
    required this.onDarkModeChanged,
    required this.initialName,
    required this.openCount,
  });

  final SharedPreferencesAsync prefs;
  final bool darkMode;
  final ValueChanged<bool> onDarkModeChanged;
  final String initialName;
  final int openCount;

  @override
  State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
  late final _nameController = TextEditingController(text: widget.initialName);
  late String _savedName = widget.initialName;

  @override
  void dispose() {
    _nameController.dispose();
    super.dispose();
  }

  Future<void> _saveName() async {
    final name = _nameController.text.trim();
    await widget.prefs.setString(PrefKeys.userName, name);
    if (!mounted) return;
    setState(() => _savedName = name);
    FocusScope.of(context).unfocus();
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Name saved')),
    );
  }

  Future<void> _resetAll() async {
    await widget.prefs.clear(
      allowList: {PrefKeys.darkMode, PrefKeys.userName, PrefKeys.openCount},
    );
    if (!mounted) return;
    _nameController.clear();
    setState(() => _savedName = '');
    widget.onDarkModeChanged(false);
  }

  @override
  Widget build(BuildContext context) {
    final greeting = _savedName.isEmpty ? 'Hello, Guest!' : 'Hello, $_savedName!';

    return Scaffold(
      appBar: AppBar(title: const Text('Settings')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          Text(greeting, style: Theme.of(context).textTheme.headlineSmall),
          Text('You have opened this app ${widget.openCount} time(s).'),
          const SizedBox(height: 24),
          Card(
            child: SwitchListTile(
              secondary: const Icon(Icons.dark_mode_outlined),
              title: const Text('Dark mode'),
              subtitle: const Text('Remembered after you close the app'),
              value: widget.darkMode,
              onChanged: widget.onDarkModeChanged,
            ),
          ),
          const SizedBox(height: 16),
          TextField(
            controller: _nameController,
            textInputAction: TextInputAction.done,
            onSubmitted: (_) => _saveName(),
            decoration: const InputDecoration(
              labelText: 'Your name',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 12),
          FilledButton(onPressed: _saveName, child: const Text('Save name')),
          const SizedBox(height: 32),
          TextButton.icon(
            onPressed: _resetAll,
            icon: const Icon(Icons.restart_alt),
            label: const Text('Reset all settings'),
          ),
        ],
      ),
    );
  }
}

The settings screen in light mode and in dark mode, both remembered after restarting the app

Test it:

  1. Turn on Dark mode and save your name.
  2. Fully close the app (stop it in your IDE, or swipe it away on the phone).
  3. Run it again. Dark mode is on, your name is shown, and the open counter went up by one.

How it works

  • Load before runApp. In main() we read every saved value before the first frame. That is why we call WidgetsFlutterBinding.ensureInitialized() first; plugins need it before runApp. Loading early prevents the app from flashing in light mode and then switching to dark.
  • Update the UI, then save. In _setDarkMode we call setState first so the switch responds instantly, then save in the background.
  • themeMode switches between the theme and darkTheme you defined in MaterialApp.
  • clear(allowList: {...}) removes only our keys. Always pass an allowList, so you never delete preferences saved by other packages.

Show onboarding only once

A very common use: show the welcome screens on the first launch only.

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  final prefs = SharedPreferencesAsync();
  final seen = await prefs.getBool('onboardingSeen') ?? false;

  runApp(MaterialApp(
    home: seen ? const HomeScreen() : const OnboardingScreen(),
  ));
}

And when the user taps "Get started" on the last onboarding page:

Future<void> _finishOnboarding() async {
  await SharedPreferencesAsync().setBool('onboardingSeen', true);
  if (!mounted) return;
  Navigator.pushReplacement(
    context,
    MaterialPageRoute(builder: (_) => const HomeScreen()),
  );
}

pushReplacement removes the onboarding screen, so the back button does not return to it. (See my navigation guide for more about this.)

Save a small object as JSON

SharedPreferences cannot store a custom class directly, but it can store a String, and any object can become a JSON string:

import 'dart:convert';

class UserProfile {
  const UserProfile({required this.name, required this.city});

  final String name;
  final String city;

  Map<String, dynamic> toJson() => {'name': name, 'city': city};

  factory UserProfile.fromJson(Map<String, dynamic> json) => UserProfile(
        name: json['name'] as String,
        city: json['city'] as String,
      );
}

// Save
const profile = UserProfile(name: 'Ali', city: 'Lahore');
await prefs.setString('profile', jsonEncode(profile.toJson()));

// Read
final saved = await prefs.getString('profile');
if (saved != null) {
  final loaded = UserProfile.fromJson(jsonDecode(saved) as Map<String, dynamic>);
  print(loaded.city); // Lahore
}

jsonEncode turns a map into text, and jsonDecode turns it back. This is the same JSON you work with in my HTTP API guide. Keep this for small objects only. For lists of many records, use a real database.

Combine with Provider

In a bigger app, put the loading and saving inside a ChangeNotifier, so any screen can read or change a setting:

class SettingsModel extends ChangeNotifier {
  SettingsModel(this._prefs, {required bool darkMode}) : _darkMode = darkMode;

  final SharedPreferencesAsync _prefs;
  bool _darkMode;

  bool get darkMode => _darkMode;

  Future<void> setDarkMode(bool value) async {
    _darkMode = value;
    notifyListeners();
    await _prefs.setBool('darkMode', value);
  }
}

If Provider is new to you, read Flutter setState vs Provider first.

Common mistakes and how to fix them

"Binding has not yet been initialized"

You used SharedPreferences in main() before runApp without calling WidgetsFlutterBinding.ensureInitialized() first. Add it as the first line of main().

"MissingPluginException"

You added the package while the app was running. Stop the app completely and run it again. If it still happens, run flutter clean and flutter pub get.

The value is always null

  • The key is spelled differently when saving and reading. Use constants (Step 3).
  • You forgot await when saving, and the app was closed before the save finished.
  • You are reading with the wrong type: a value saved with setInt must be read with getInt.

Data is gone after reinstalling the app

That is expected. Uninstalling an app deletes its stored data. Data that must survive reinstalls belongs on a server or in Firebase.

Dark mode flashes light for a moment on startup

You load the setting after the first frame (for example in initState). Load it in main() before runApp, as in Step 4.

Quick reference

I want to...Code
Create the preferences objectfinal prefs = SharedPreferencesAsync();
Save a boolawait prefs.setBool('key', true);
Read with a defaultawait prefs.getBool('key') ?? false
Delete one valueawait prefs.remove('key');
Delete my app's valuesawait prefs.clear(allowList: {'a', 'b'});
Save an objectsetString('key', jsonEncode(obj.toJson()))

What to learn next

Keep reading

More articles

  • · 9 min read

    Flutter Row, Column and Stack Explained with Examples

    Understand Flutter's core layout widgets: main and cross axis, mainAxisAlignment, crossAxisAlignment, Expanded, Flexible, Spacer, Stack and Positioned, plus a complete profile card example.

  • · 7 min read

    Flutter Navigation: How to Move Between Screens

    Learn Flutter navigation step by step: Navigator.push and pop, passing data to a screen, returning results, pushReplacement for login flows, named routes, and when to use go_router.