How to Manage the Shared Preferences in Flutter

When you use the Shared Preferences plugin in Flutter you might have noticed that it can become difficult to manage. Think about having the Shared Preferences keys in different places in your code, calling the same function on multiple places, and perhaps even deleting entries that should not be deleted. In this post, we will discuss the best practices and ensure you can manage the Shared Preference entries effectively in Flutter.

Shared Preferences Plugin

The Shared Preferences plugin can be used to save small amounts of data in the persistent storage of the user’s device. The data is saved using key-value pairs. Given the use of key-value pairs, it is important to handle them correctly within your application to avoid issues such as key mismatches and other potential problems.

If you are new to the Shared Preferences plugin, check out this article: Save Data on the Device Using Shared Preferences in Flutter. The article covers everything you need to know, including how to install the plugin, its general usage, and the implementation steps.

Manage the Shared Preferences in Flutter

Let us start by clarifying what is meant by managing the Shared Preferences plugin effectively in Flutter.

The first important consideration is to avoid manually providing keys as strings every time you access a specific entry from the persistent storage using the Shared Preferences plugin. This approach is error-prone and can lead to mistakes. Instead, a recommended practice is to use a class to store your keys.

Secondly, we also want to guard the functions that can be called for each entry. For example, you might have entries that should not be deleted. If we create a controller class that will contain all the Shared Preferences functionality. We can ensure that other members of your team will never call functions that are not allowed.

Finally, we can make accessing the SharedPreferences instance easier. Right now, to get an instance of the SharedPreferences class, you have to use its getInstance function. This can be frustrating because it means you sometimes need to change your code to handle asynchronous calls.

Create a Controller Class

To create the controller class, we will start by creating a new Dart file called 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) ?? 'Placeholder';
  }

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

  static Future<bool> containsTitle() async {
    SharedPreferences preferences = await SharedPreferences.getInstance();
    return 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 started by creating a private class called _SharedPreferencesKeys where we store keys as properties. The static and const keywords are used to access the key without creating an instance of the class and to ensure the key remains unchanged.

We also created a class called SharedPreferencesController that has a getter to get the title from the Shared Preferences. We use the getString function with the title key from the _SharedPreferencesKeys class. The return type of the getString function is nullable, therefore we added a fallback string with the text “Placeholder”.

Apart from the getter, we also created three functions to set, check, and remove the title from the Shared Preferences.

Use the SharedPreferencesController

To implement our new controller class, we will modify the example application from the article mentioned at the beginning of this post. Let us take a look at the modifications:

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

void main() => runApp(const MyApp());

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String? _title;

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

  Future<void> _updateTitle() async {
    final title = await SharedPreferencesController.title;
    setState(() => _title = title);
  }

  Future<void> _setTitle() async {
    await SharedPreferencesController.setTitle('Only Flutter');
    _updateTitle();
  }

  Future<void> _clearTitle() async {
    await SharedPreferencesController.deleteTitle();
    _updateTitle();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Center(
            child: Text(_title ?? 'No Title'),
          ),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: _setTitle,
                child: const Text('Set title'),
              ),
              ElevatedButton(
                onPressed: _clearTitle,
                child: const Text('Clear title'),
              )
            ],
          ),
        ),
      ),
    );
  }
}

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.

Additionally, we no longer have to remember the name of the key, because this has been abstracted inside our controller class. If we execute the code you will notice that everything still works as before:

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 already made using the Shared Preferences Plugin much simpler, there is still room for further improvement.

Improving the SharedPreferencesController Class

Currently, in both the getter and functions of the SharedPreferencesController class, we repeatedly call the getInstance function to access the SharedPreferences instance. It would be more efficient to instantiate this instance only once. To achieve this, we can implement a singleton pattern.

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.

Implement the Singleton Pattern

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) ?? 'Placeholder';

  static Future<void> setTitle(String value) async =>
      await _preferences.setString(_SharedPreferencesKeys.title, value);

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

  static Future<bool> deleteTitle() async =>
      _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 instance of the SharedPreferences class using the SharedPreferences.getInstance function inside our init function.

This means that all the getters and functions can now use the _preferences variable. As a result, the functions are shorter and easier to read, and some of them no longer need to be asynchronous.

Finalizing the Example Application

With the above changes made in our SharedPreferencesController class, we can now adjust our example application accordingly. Take a look at the following changes:

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

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

  await SharedPreferencesController.init();

  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _title = SharedPreferencesController.title;

  void _updateTitle() =>
      setState(() => _title = SharedPreferencesController.title);

  Future<void> _setTitle() async {
    await SharedPreferencesController.setTitle('Only Flutter');
    _updateTitle();
  }

  Future<void> _clearTitle() async {
    await SharedPreferencesController.deleteTitle();
    _updateTitle();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Center(
            child: Text(_title),
          ),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              ElevatedButton(
                onPressed: _setTitle,
                child: const Text('Set title'),
              ),
              ElevatedButton(
                onPressed: _clearTitle,
                child: const Text('Clear title'),
              )
            ],
          ),
        ),
      ),
    );
  }
}

In the main function, we added the WidgetsFlutterBinding.ensureInitialized function to wait for the binding to be initialized. Because we are only able to access the Shared Preferences once the binding is initialized.

Right after that, we initialize our SharedPreferences instance by calling the init function of our SharedPreferencesController class. Because we do this before we call the runApp function the SharedPreferences instance inside our controller will always be instantiated and can therefore always be accessed inside our widgets.

Because our title getter is no longer asynchronous we can directly assign the title value to the _title variable. Also, the _updateTitle function is no longer asynchronous because we no longer await the getInstance function inside the title getter.

When we run our application again you notice that the functionality remains unchanged. However, our controller class is now much easier to use.

Conclusion

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 the SharedPreferences class throughout the application, improving performance and ease of use.

Tijn van den Eijnde
Tijn van den Eijnde
Articles: 41

Leave a Reply

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