Save Data on the Device using shared_preferences in Flutter

When developing Flutter applications, it is common to need to persistently store and retrieve small amounts of data, like user preferences and settings. The shared_preferences plugin is a useful tool for this purpose. It provides a simple and efficient solution for managing persistent data on the device. With Shared Preferences, developers can easily store and retrieve data across different application sessions using a key-value pair approach.

What are Shared Preferences?

Shared Preferences in Flutter allow you to store and retrieve small amounts of data persistently on the device. They are commonly used to store user preferences, settings, and other data that need to be remembered even when the application is closed and reopened.

Shared Preferences work by associating each piece of data with a unique identifier called a key. The actual data is the corresponding value associated with that key. This data is stored on the device\’s disk, allowing it to be accessed even after the application is closed and reopened.

1. Installing

We will start by installing the shared_preferences plugin into our project. We can do that by executing the following command: flutter pub add shared_preferences.

After executing the command, verify that the package is installed by checking the dependencies in your pubspec.yaml file. It should include the shared_preferences package:

dependencies:
  flutter:
    sdk: flutter
  shared_preferences: ^2.2.0

2. How to work with shared preferences

Now that the package is installed, we can import it and obtain an instance of shared preferences using the SharedPreferences class. We will save this instance in a variable called preferences.

import 'package:shared_preferences/shared_preferences.dart';

Future<void> main() async {
  SharedPreferences preferences = await SharedPreferences.getInstance();
}

On our preferences variable, we can set the first shared preferences entry using the setString function:

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

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  SharedPreferences preferences = await SharedPreferences.getInstance();
  
  preferences.setString('key', 'value');

  print(preferences.get('key'));
}

In this code snippet, we use the setString function to set a string value in the shared preferences. This function requires a key and a value as parameters. To retrieve the value we just set, we can use the get method on our SharedPreferences instance, specifying the key as key. Printing the get function will result in the following:

value

Because we added the async keyword to make the main function asynchronous we have to add the following line WidgetsFlutterBinding.ensureInitialized(); to wait for the binding to be initialized to avoid errors.

Other than setting and getting the key we can also remove the entry with that key and check if our shared preferences contain the key:

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

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  SharedPreferences preferences = await SharedPreferences.getInstance();

  await preferences.setString('key', 'value');

  print(preferences.get('key'));
  print(await preferences.containsKey('key'));

  await preferences.remove('key');

  print(preferences.get('key'));
}

In the above code snippet, we called two additional functions, containsKey and remove. The containsKey function will check if our shared preferences have an entry with the given key. The remove function will delete the entry with the given key. The print functions will return the following:

Get: value
Contains: true
Get after delete: null

Other than setting a string in the shared preferences we can also set these other types, they all have their own function:

  • prefs.setInt(\'key\', 5): Save an integer value with the given key.
  • prefs.setBool(\'key\', true): Save a boolean value with the given key.
  • prefs.setDouble(\'key\', 0.5): Save a double value with the given key.
  • prefs.setStringList(\'key\', <String>[\'Green\', \'Red\', \'Yellow\']): Save a list of strings with the given key.

The same goes for the get functions.

3. How does it work under the hood?

Under the hood, shared preferences in Flutter use platform-specific implementations to store and retrieve data persistently. Here is how it typically works:

  1. On Android, shared preferences are implemented using the Android SharedPreferences API, which stores data in an XML file in the application\’s private directory on the device.
  2. On iOS, shared preferences are implemented using the NSUserDefaults class, which stores data in a .plist file in the application\’s sandboxed container.
  3. When you call SharedPreferences.getInstance(), the Flutter plugin communicates with the platform-specific implementation to obtain an instance of shared preferences.
  4. Once you have an instance of shared preferences, you can use its methods to store and retrieve data. The data is typically stored as key-value pairs, where the key is a unique identifier and the value can be a primitive data type like a string, List<String>, bool, int, or double.
  5. When you store data using prefs.setString(\'key\', \'value\') (or similar methods for other data types), the shared preferences plugin communicates with the platform-specific implementation to persist the data.
  6. When you retrieve data using prefs.getString(\'key\') (or similar methods), the plugin again communicates with the platform-specific implementation to retrieve the stored value for the given key.
  7. The data is returned to your Flutter application, allowing you to use it as needed.

It is worth mentioning that shared preferences are intended for storing small amounts of data, and they are not designed for complex data structures or large datasets. If you need to store more significant amounts of data, you may want to consider other options like a local database or file storage.

4. Implementation example

Now that we have a basic understanding let us go over a complete implementation:

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String title = '';
  SharedPreferences? sharedPreferences;

  @override
  void initState() {
    super.initState();
    initTitle();
  }

  initTitle() async {
    await SharedPreferences.getInstance().then((SharedPreferences preferences) {
      String? newTitle = preferences.getString('title');
      setState(() {
        title = newTitle ?? '';
        sharedPreferences = preferences;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shared Preferences Example',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Center(child: Text(title)),
        ),
        body: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                child: Text('Set title'),
                onPressed: () async {
                  await sharedPreferences?.setString('title', 'New title').then((isSet) {
                    if (isSet == true) {
                      String? newTitle = sharedPreferences?.getString('title');
                      setState(() => title = newTitle ?? '');
                    }
                  });
                },
              ),
              ElevatedButton(
                child: Text('Delete title'),
                onPressed: () async {
                  await sharedPreferences?.remove('title').then((isRemoved) {
                    if (isRemoved == true) {
                      String? newTitle = sharedPreferences?.getString('title');
                      setState(() => title = newTitle ?? '');
                    }
                  });
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

In this code snippet, the MyApp widget represents the root of the application and is responsible for maintaining the state. Inside the _MyAppState class, we have a title variable that holds the title of the application, and a nullable sharedPreferences variable that will store an instance of the SharedPreferences class.

The initState method is called when the widget is initialized and it retrieves the stored title from shared preferences using the initTitle method. This method is asynchronous and retrieves the shared preferences instance using SharedPreferences.getInstance(). It then obtains the stored title using the getString method. If a new title is obtained, it updates the application\’s state using setState and assigns the shared preferences instance to the sharedPreferences variable.

The build method is responsible for constructing the user interface of the application. It returns a MaterialApp widget, which serves as the root of the widget tree. The MaterialApp widget sets the application\’s title and theme and specifies the home widget.

The home widget is a Scaffold, which provides a basic layout structure for the application. It consists of an AppBar with the title displayed at the center and a body that contains a Row widget with two ElevatedButton widgets.

The first button sets a new title by calling the setString method on the sharedPreferences instance. If the title is set successfully, it updates the application\’s state. The second button deletes the title by calling the remove method on the sharedPreferences instance. If the title is removed successfully, it updates the application\’s state.

When we run the application it will persist the data. As shown in the GIF below once we press the restart to reset the application\’s state you will see that the new title is still displayed because it is retrieved from the shared preferences.

5. Best practices

When working with shared preferences in Flutter, there are several best practices that can improve code organization and simplify usage.

One important consideration is to avoid manually providing keys as strings every time you access a specific entry in the shared preferences. This approach is error-prone and can lead to mistakes. Instead, a recommended practice is to use a controller class to handle shared preferences functionality.

5.1 Using a Controller Class

To implement this we start by creating a separate file such as shared_preferences_controller.dart. In this file we will create two classes, the first class being the _SharedPreferencesKeys class, where we save our shared preferences keys. The second class is the SharedPreferencesController. In this controller class, we want to access the keys directly so that we never have to provide our keys as strings. Let us have a look at the following code:

import 'package:shared_preferences/shared_preferences.dart';

class _SharedPreferencesKeys {
  static const String title = 'title';
}

class SharedPreferencesController {
  static Future<String> get title async {
    SharedPreferences preferences = await SharedPreferences.getInstance();
    return preferences.getString(_SharedPreferencesKeys.title) ??
        'Set the title by clicking the button';
  }

  static Future<void> setTitle(String value) async {
    SharedPreferences preferences = await SharedPreferences.getInstance();
    await preferences.setString(_SharedPreferencesKeys.title, value);
  }

  static Future<bool> containsTitle() async {
    SharedPreferences preferences = await SharedPreferences.getInstance();
    return await preferences.containsKey(_SharedPreferencesKeys.title);
  }

  static Future<bool> deleteTitle() async {
    SharedPreferences preferences = await SharedPreferences.getInstance();
    return await preferences.remove(_SharedPreferencesKeys.title);
  }
}

In this code snippet, we create a private class called _SharedPreferencesKeys where we store our title key. We use the static and const keywords so that we can access the key without creating an instance of the class, and to ensure that the key does not change.

We also create a class called SharedPreferencesController that has a function to get the title from the shared preferences. We use the getString function, which can sometimes return null, so we handle that by providing an alternative title.

Apart from the getter, there are three other functions in the class that help us set, check, and remove the title from the shared preferences.

Now in our main.dart file we made the following changes:

import 'package:codeonwards_demo/shared_preferences_controller.dart';
import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String title = '';

  @override
  void initState() {
    super.initState();
    initTitle();
  }

  initTitle() async {
    String newTitle = await SharedPreferencesController.title;
    setState(() => title = newTitle);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shared Preferences Example',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Center(child: Text(title)),
        ),
        body: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                child: Text('Set title'),
                onPressed: () async {
                  await SharedPreferencesController.setTitle('New title');
                  String newTitle = await SharedPreferencesController.title;
                  setState(() => title = newTitle);
                },
              ),
              ElevatedButton(
                child: Text('Delete title'),
                onPressed: () async {
                  await SharedPreferencesController.deleteTitle();
                  String newTitle = await SharedPreferencesController.title;
                  setState(() => title = newTitle);
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Instead of importing the shared_preferences plugin, we now import our SharedPreferencesController. This means that we do not need to create an instance of the SharedPreferences class anymore. Instead, we can directly access all the shared preferences functionality through our SharedPreferencesController class. This is possible because both the getter and the other functions are static. As you can see, the code in our main.dart file is much easier to understand.

Additionally, if you check the GIF below, you will notice that the functionality remains the same. The only difference is that the default title is now set to \”Set the title by clicking the button\” to demonstrate that this GIF is using the new implementation.

Another positive aspect of this approach is that you will only use the functionality provided by the SharedPreferencesController. This ensures that you will not accidentally use functions that you have not implemented. For example, if you have not included a delete function, you will not mistakenly delete a shared preferences entry that you did not mean to delete. Although this implementation has already made our code much simpler, there is still room for further improvement.

5.2 Singleton

A singleton is a design pattern that guarantees the existence of only one instance of a class. It provides a convenient way to access that single instance from any part of the application. In simpler terms, a singleton class ensures that there is only one instance of it available.

Let us improve the previous implementation by using the singleton design pattern for the SharedPreferencesController:

import 'package:shared_preferences/shared_preferences.dart';

class _SharedPreferencesKeys {
  static const String title = 'title';
}

class SharedPreferencesController {
  static late final SharedPreferences _preferences;

  static Future init() async =>
      _preferences = await SharedPreferences.getInstance();

  static String get title =>
      _preferences.getString(_SharedPreferencesKeys.title) ??
          'Set the new title';

  static Future setTitle(String value) async {
    await _preferences.setString(_SharedPreferencesKeys.title, value);
  }

  static bool containsTitle() =>
      _preferences.containsKey(_SharedPreferencesKeys.title);

  static Future deleteTitle() async {
    return await _preferences.remove(_SharedPreferencesKeys.title);
  }
}

In this code snippet, we added a private variable _preferences. This variable will be set inside our new init function. That is why we use the late keyword. We use the final keyword because we want the variable to be set once. Notice that we only retrieve the shared preferences instance using the SharedPreferences.getInstance() inside our init function. This means that the functions are shorter and some of them are not longer asynchronous.

In this code snippet, we introduced a private variable called _preferences. This variable is set within our new init function, which is why we used the late keyword. We used the final keyword because we want the value to stay the same once it is set. It is important to note that we retrieve the shared preferences instance using SharedPreferences.getInstance() only within the init function. This makes our functions shorter, and for some functions removes the need to be asynchronous.

In our main.dart file we have made the following changes:

import 'package:codeonwards_demo/shared_preferences_controller.dart';
import 'package:flutter/material.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await SharedPreferencesController.init();

  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String title = '';

  @override
  void initState() {
    super.initState();
    setState(() => title = SharedPreferencesController.title);
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Shared Preferences Example',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: Scaffold(
        appBar: AppBar(
          title: Center(child: Text(title)),
        ),
        body: Center(
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              ElevatedButton(
                child: Text('Set title'),
                onPressed: () async {
                  SharedPreferencesController.setTitle('New title');
                  String newTitle = SharedPreferencesController.title;
                  setState(() => title = newTitle);
                },
              ),
              ElevatedButton(
                child: Text('Delete title'),
                onPressed: () async {
                  await SharedPreferencesController.deleteTitle();
                  String newTitle = SharedPreferencesController.title;
                  setState(() => title = newTitle);
                },
              ),
            ],
          ),
        ),
      ),
    );
  }
}

In the main function, we added the WidgetsFlutterBinding.ensureInitialized(); function to wait for the binding to be initialized. Right after that, we initialize our SharedPreferences instance by calling the init function of our SharedPreferencesController class. Because our getter is no longer asynchronous we can directly set our title inside the initState function. When we run our application again you notice that the functionality remains unchanged.

Conclusion

The Shared Preferences plugin in Flutter is a convenient way for persistently storing and retrieving small amounts of data such as user preferences and settings. It offers a straightforward key-value pair mechanism for data storage.

However, it is essential to follow best practices for improved code organization and readability. Implementing a controller class can simplify access to Shared Preferences by encapsulating keys and providing dedicated functions for setting, getting, and deleting data.

Additionally, adopting the singleton design pattern ensures a single instance of Shared Preferences throughout the application, enhancing efficiency and ease of use.

Once again, it is essential to remember that Shared Preferences is most suitable for handling small amounts of data. If you have larger data storage needs, it is recommended to use alternative solutions such as local databases or file storage.

Tijn van den Eijnde
Tijn van den Eijnde
Articles: 87

Leave a Reply

Your email address will not be published. Required fields are marked *