“This is the first day of my participation in the Gwen Challenge in November. Check out the details: The last Gwen Challenge in 2021”


To save simple data in Flutter, you need to use the shared_preferences plugin. This plugin can be used to persist key-value data.

The shared_Preferences plug-in uses SharedPreferences on Android and NSUserDefaults on iOS. Data is stored asynchronously to the device disk.

use

  1. Add in pubspec.yamlshared_preferencesRely on.
flutter:
	shared_preferences: ^ mid-atlantic moved
Copy the code
  1. Get the SharedPreferences instance, noting that getInstance() returnsFuture<SharedPreferences>Type. If you need to retrieve the instance multiple times in a Widget to call, you can retrieve it in the initState method and store it in a variable.
SharedPreferences prefs = await SharedPreferences.getInstance();
Copy the code
  1. Call the accessor method of SharedPreferences
/// The read method
// Return all keys
Set<String> getKeys();
// Return the value of a key, of an unspecified type
Object? get(String key);
bool? getBool(String key);
int? getInt(String key);
double? getDouble(String key);
String? getString(String key);
List<String>? getStringList(String key);

/// Write a method
Future<bool> setBool(String key, bool value);
Future<bool> setInt(String key, int value);
Future<bool> setDouble(String key, double value);
Future<bool> setString(String key, String value);
Future<bool> setStringList(String key, List<String> value);
Future<bool> remove(String key);
Future<bool> clear();
Copy the code

The sample

We write an example of a counter that is similar to the official Flutter template. When you press FloatingButton, count increases by 1. Unlike the template, in our example count is saved to the device’s disk.

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

class SpPluginLibPage extends StatefulWidget {
  @override
  _SpPluginLibPageState createState() => _SpPluginLibPageState();
}
class _SpPluginLibPageState extends State<SpPluginLibPage> {
  static const String KEY_COUNT = "sp_key_count";
  /// count
  int count = 0;
  Future<SharedPreferences> spFuture = SharedPreferences.getInstance();

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("SharedPreferences Demo"),
      ),
      body: Padding(
        padding: EdgeInsets.all(20),
        child: Center(
          child: Text(
            "You clicked$countTime. ""
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          storeCount(count + 1);
        },
        child: Icon(Icons.add),
      ),
    );
  }

  void readCount() async {
    SharedPreferences sp = await spFuture;
    setState(() {
      count = sp.getInt(KEY_COUNT) ?? 0;
    });
  }

  void storeCount(int count) async {
    SharedPreferences sp = await spFuture;
    await sp.setInt(KEY_COUNT, count);
    setState(() {
      this.count = count; }); }}Copy the code

The source code interpretation

Let’s take a look at the source code for this plug-in to understand how it works.

Take the SharedPreferences. GetInstance () method:

static Future<SharedPreferences> getInstance() async {
  if (_completer == null) {
    final completer = Completer<SharedPreferences>();
    try {
      final Map<String.Object> preferencesMap =
          await _getSharedPreferencesMap();
      completer.complete(SharedPreferences._(preferencesMap));
    } on Exception catch (e) {
      completer.completeError(e);
      final Future<SharedPreferences> sharedPrefsFuture = completer.future;
      _completer = null;
      return sharedPrefsFuture;
    }
    _completer = completer;
  }
  return_completer! .future; }Copy the code

The getInstance method checks if _completer is empty, and if not, returns its future directly. If the value is null, the _getSharedPreferencesMap method will be called to obtain all key-values stored in SP and save them to _preferenceCache. When reading the values, it can be read directly from the memory cache, which is faster. And set the result to _completer so you can get the instance directly next time.

Let’s look at the _getSharedPreferencesMap method again:

static Future<Map<String.Object>> _getSharedPreferencesMap() async {
  final Map<String.Object> fromSystem = await _store.getAll();  / / 1
  assert(fromSystem ! =null);
  // 2 Strip the flutter. prefix from the returned preferences.
  final Map<String.Object> preferencesMap = <String.Object> {};for (String key in fromSystem.keys) {
    assert(key.startsWith(_prefix)); preferencesMap[key.substring(_prefix.length)] = fromSystem[key]! ; }return preferencesMap;
}
Copy the code

The position of comment 1 calls _store.getall () to getAll key-values from the device and then traverses the Map with all prefixes to flutter. The key of the preferencesMap, with the prefix removed, is finally assigned to _preferenceCache, which has a flutter. If the prefix is key, an exception will be thrown. The plugin automatically adds a flutter in front of our own written key. Is prefixed as the key that is ultimately written to the device.

Let’s see what _store is:

static SharedPreferencesStorePlatform get _store {
  
  if (_manualDartRegistrationNeeded) {
    // Only do the initial registration if it hasn't already been overridden
    // with a non-default instance.
    if(! kIsWeb && SharedPreferencesStorePlatform.instanceis MethodChannelSharedPreferencesStore) {
      if (Platform.isLinux) {
        SharedPreferencesStorePlatform.instance = SharedPreferencesLinux();
      } else if (Platform.isWindows) {
        SharedPreferencesStorePlatform.instance = SharedPreferencesWindows();
      }
    }
    _manualDartRegistrationNeeded = false;
  }

  return SharedPreferencesStorePlatform.instance;
}
Copy the code

As you can see, _store is a platform Sp instance. Different types of instances are created on different platforms, for example, SharedPreferencesLinux on Linux, SharedPreferencesWindows on Windows, Android and iOS is MethodChannelSharedPreferencesStore, Web is SharedPreferencesPlugin, They are all SharedPreferencesStorePlatform subclasses.

Let’s take a look at the implementation of the getAll method on each platform to understand how persistence is implemented on each platform:

Android & iOS: MethodChannelSharedPreferencesStore

@override
Future<Map<String.Object>> getAll() async {
  final Map<String.Object>? preferences =
      await _kChannel.invokeMapMethod<String.Object> ('getAll');

  if (preferences == null) return <String.Object> {};return preferences;
}
Copy the code

As you can see, the MethodChannel calls the relevant API of SharedPreferences in Android or NSUserDefaults in iOS.

Windows: SharedPreferencesWindows

@override
Future<Map<String.Object>> getAll() async {
  return _readPreferences();
}

Future<Map<String.Object>> _readPreferences() async {
	// If there is a memory cache, return the memory cache directly
  if(_cachedPreferences ! =null) {
    return_cachedPreferences! ; }Map<String.Object> preferences = {};
 	Json file shared_preferences
  final File? localDataFile = await _getLocalDataFile();
  if(localDataFile ! =null && localDataFile.existsSync()) {
    String stringMap = localDataFile.readAsStringSync();
    if (stringMap.isNotEmpty) {
      preferences = json.decode(stringMap).cast<String.Object> (); } } _cachedPreferences = preferences;return preferences;
}
Copy the code

The SharedPreferences data in Window is stored in a file named shared_preferences. Json in the software data directory. This method is to read the content of the file and parse it into a Map format.

Linux: SharedPreferencesLinux

@override Future<Map<String, Object>> getAll() async { return _readPreferences(); } Future<Map<String, Object>> _readPreferences() async { if (_cachedPreferences ! = null) { return _cachedPreferences! ; } Map<String, Object> preferences = {}; final File? localDataFile = await _getLocalDataFile(); if (localDataFile ! = null && localDataFile.existsSync()) { String stringMap = localDataFile.readAsStringSync(); if (stringMap.isNotEmpty) { preferences = json.decode(stringMap).cast<String, Object>(); } } _cachedPreferences = preferences; return preferences; }Copy the code

It can be seen that the code under Linux is basically the same as Windows. In fact, the file name of the content is also the same. The difference is that the file path of the two systems is different.

Web: SharedPreferencesPlugin

I don’t know why the Web version type name is SharedPreferencesPlugin instead of SharedPreferencesWeb.

@override
Future<Map<String.Object>> getAll() async {
  final Map<String.Object> allData = {};
  for (String key in _storedFlutterKeys) {
    allData[key] = _decodeValue(html.window.localStorage[key]!) ; }return allData;
}
Copy the code

In the Web version, the key-value of SharedPreferences is stored in html.window. LocalStorage.

Finally, let’s look at how the Android platform calls the platform API with MethodChannel.

/** SharedPreferencesPlugin */
public class SharedPreferencesPlugin implements FlutterPlugin {
  private static final String CHANNEL_NAME = "plugins.flutter.io/shared_preferences";
  private MethodChannel channel;
  private MethodCallHandlerImpl handler;

  @SuppressWarnings("deprecation")
  public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) {
    final SharedPreferencesPlugin plugin = new SharedPreferencesPlugin();
    plugin.setupChannel(registrar.messenger(), registrar.context());
  }

  @Override
  public void onAttachedToEngine(FlutterPlugin.FlutterPluginBinding binding) {
    setupChannel(binding.getBinaryMessenger(), binding.getApplicationContext());
  }
  
  @Override
  public void onDetachedFromEngine(FlutterPlugin.FlutterPluginBinding binding) {
    teardownChannel();
  }
  
  / / 1
  private void setupChannel(BinaryMessenger messenger, Context context) {
    channel = new MethodChannel(messenger, CHANNEL_NAME);
    handler = new MethodCallHandlerImpl(context);
    channel.setMethodCallHandler(handler);
  }

  private void teardownChannel(a) {
    handler.teardown();
    handler = null;
    channel.setMethodCallHandler(null);
    channel = null; }}Copy the code

Can see SharedPreferencesPlugin core is in note 1 setupChannel method, created a MethodChannel, name is plugins. The flutter. IO/shared_preferences, The handler for this channel is MethodCallHandlerImpl. Let’s look at the code in this class:

// constructor
MethodCallHandlerImpl(Context context) {
  / / 1
  preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
  executor =
      new ThreadPoolExecutor(0.1.30L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
  handler = new Handler(Looper.getMainLooper());
}
Copy the code

At the position of comment 1 in the constructor, the SharedPreferences object in Android is created. The shared_preferences plugin does not support the creation of a Repo named FlutterSharedPreferences. The shared_preferences plugin does not support creating a Repo named FlutterSharedPreferences. Otherwise, you might have a problem overwriting the content of someone else’s key.

@Override
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
  String key = call.argument("key");
  try {
    switch (call.method) {
      case "setBool":
        commitAsync(preferences.edit().putBoolean(key, (boolean) call.argument("value")), result);
        break;
      case "setDouble":
        double doubleValue = ((Number) call.argument("value")).doubleValue();
        String doubleValueStr = Double.toString(doubleValue);
        commitAsync(preferences.edit().putString(key, DOUBLE_PREFIX + doubleValueStr), result);
        break;
      case "setInt":
        Number number = call.argument("value");
        if (number instanceof BigInteger) {
          BigInteger integerValue = (BigInteger) number;
          commitAsync(
              preferences
                  .edit()
                  .putString(
                      key, BIG_INTEGER_PREFIX + integerValue.toString(Character.MAX_RADIX)),
              result);
        } else {
          commitAsync(preferences.edit().putLong(key, number.longValue()), result);
        }
        break;
      case "setString":
        String value = (String) call.argument("value");
        if (value.startsWith(LIST_IDENTIFIER)
            || value.startsWith(BIG_INTEGER_PREFIX)
            || value.startsWith(DOUBLE_PREFIX)) {
          result.error(
              "StorageError"."This string cannot be stored as it clashes with special identifier prefixes.".null);
          return;
        }
        commitAsync(preferences.edit().putString(key, value), result);
        break;
      case "setStringList":
        List<String> list = call.argument("value");
        commitAsync(
            preferences.edit().putString(key, LIST_IDENTIFIER + encodeList(list)), result);
        break;
      case "commit":
        // We've been committing the whole time.
        result.success(true);
        break;
      case "getAll":
        result.success(getAllPrefs());
        return;
      case "remove":
        commitAsync(preferences.edit().remove(key), result);
        break;
      case "clear":
        Set<String> keySet = getAllPrefs().keySet();
        SharedPreferences.Editor clearEditor = preferences.edit();
        for (String keyToDelete : keySet) {
          clearEditor.remove(keyToDelete);
        }
        commitAsync(clearEditor, result);
        break;
      default:
        result.notImplemented();
        break; }}catch (IOException e) {
    result.error("IOException encountered", call.method, e); }}Copy the code

The core of this class is the onMethodCall method, which gets the name of the method from call.method and handles the logic in the corresponding case branch. Argument (“key”) gets the key, and argument(“value”) gets the value. The logic is similar on iOS.

The basic logic is like this, in fact, is relatively clear and simple, but also some details of the logic here will not repeat, you can see the source code to understand.