'Flutter: How to use SharedPreferences synchronously?

I am using Shared Preferences in my Flutter app and what I would like to do is store SharedPreferences as a field on startup and then use it synchronously in the app. However I'm not sure if I'm not missing anything. What I want to achieve is instead of:

method1() async {
  SharedPreferences sp = await SharedPreferences.getInstance();
  return sp.getString('someKey');
}

to

SharedPreferences sp;
//I would probably pass SharedPreferences in constructor, but the idea is the same
someInitMethod() async {
  sp = await SharedPreferences.getInstance();
}
method1() {
  return sp.getString('someKey');
}
method2() {
  return sp.getString('someKey2');
}
method3() {
  return sp.getString('someKey3');
}

In that way I would achieve synchronous access to sharedPrefs. Is it bad solution?

EDIT:
What is worth mentioning is that getInstance method will only check for instance and if there is any than it returns it, so as I see it, is that async is only needed to initialize instance. And both set and get methods are sync anyway.

static Future<SharedPreferences> getInstance() async {
  if (_instance == null) {
    final Map<String, Object> fromSystem =
        await _kChannel.invokeMethod('getAll');
    assert(fromSystem != null);
    // 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];
    }
    _instance = new SharedPreferences._(preferencesMap);
  }
  return _instance;
}


Solution 1:[1]

I use the same approach as the original poster suggests i.e. I have a global variable (actually a static field in a class that I use for all such variables) which I initialise to the shared preferences something like this:

in globals.dart:

class App {      
  static SharedPreferences localStorage;
  static Future init() async {
    localStorage = await SharedPreferences.getInstance();
  }
}

in main.dart:

void main() {
  start();
}

Async.Future start() async {
  await App.init();
  localStorage.set('userName','Bob');
  print('User name is: ${localStorage.get('userName)'}');  //prints 'Bob'
}

The above worked fine but I found that if I tried to use App.localStorage from another dart file e.g. settings.dart it would not work because App.localStorage was null but I could not understand how it had become null.

Turns out the problem was that the import statement in settings.dart was import 'package:<packagename>/src/globals.dart'; when it should have been import 'globals.dart;.

Solution 2:[2]

@iBob101 's answer is good, but still, you have to wait before you use the SharedPreferences for the first time.

The whole point is NOT to await for your SharedPreferences and be sure that it will always be NOT NULL.

Since you'll have to wait anyway let's do it in the main() method:

class App {      
  static SharedPreferences localStorage;
  static Future init() async {
    localStorage = await SharedPreferences.getInstance();
  }
}

And the main method:

void main() async{
  await SharedPref.initSharedPref();
  runApp(MyApp());
}

the line await SharedPref.initSharedPref(); takes ~100ms to execute. This is the only drawback as far as I can see.

But you definitely know that in every place in the app your sharedPreferenes instance in NOT NULL and ready for accessing it:

String s = App.localStorage.getString(PREF_MY_STRING_VALUE);

I think it's worthwhile

Solution 3:[3]

The cleanest way is to retrieve SharedPreferences in main method and pass it to MyApp as a dependency:

void main() async {
  // Takes ~50ms to get in iOS Simulator.
  final SharedPreferences sharedPreferences =
      await SharedPreferences.getInstance();

  runApp(MyApp(sharedPreferences: sharedPreferences));
}

class MyApp extends StatefulWidget {
  final SharedPreferences sharedPreferences;

  const MyApp({Key key, this.sharedPreferences})
      : assert(sharedPreferences != null),
        super(key: key);

  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    // You can access shared preferences via widget.sharedPreferences
    return ...
  }

Solution 4:[4]

You can use FutureBuilder to render the loading screen while waiting for SharedPreferences to be intialized for the first time in a singleton-like class. After that, you can access it synchronously inside the children.

local_storage.dart

class LocalStorage {
  static late final SharedPreferences instance;

  static bool _init = false;
  static Future init() async {
    if (_init) return;
    instance = await SharedPreferences.getInstance();
    _init = true;
    return instance;
  }
}

app_page.dart

final Future _storageFuture = LocalStorage.init();

@override
Widget build(BuildContext context) {
  return FutureBuilder(
    future: _storageFuture,
    builder: (context, snapshot) {
      Widget child;
      if (snapshot.connectionState == ConnectionState.done) {
        child = MyPage();
      } else if (snapshot.hasError) {
        child = Text('Error: ${snapshot.error}');
      } else {
        child = Text('Loading...');
      }
      return Scaffold(
        body: Center(child: child),
      );
    },
  );
}

my_page.dart

return Text(LocalStorage.instance.getString(kUserToken) ?? 'Empty');

Solution 5:[5]

  1. call shared prefs on startup of a stateful main app (we call ours a initState() override of a StatefulWidget after super.initState())
  2. after shared prefs inits, set the value to a field on main (ex: String _someKey)
  3. inject this field into any child component
  4. You can the call setState() on _someKey at you leisure and it will persist to children injected with your field

Solution 6:[6]

I made a simple way to using this PrefUtil class:

import 'package:shared_preferences/shared_preferences.dart';

class PrefUtil {
  static late final SharedPreferences preferences;
  static bool _init = false;
  static Future init() async {
    if (_init) return;
    preferences = await SharedPreferences.getInstance();
    _init = true;
    return preferences;
  }

  static setValue(String key, Object value) {
    switch (value.runtimeType) {
      case String:
        preferences.setString(key, value as String);
        break;
      case bool:
        preferences.setBool(key, value as bool);
        break;
      case int:
        preferences.setInt(key, value as int);
        break;
      default:
    }
  }

static Object getValue(String key, Object defaultValue) {
    switch (defaultValue.runtimeType) {
      case String:
        return preferences.getString(key) ?? "";
      case bool:
        return preferences.getBool(key) ?? false;
      case int:
        return preferences.getInt(key) ?? 0;
      default:
        return defaultValue;
    }
  }
}

In main.dart:

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

Save it like:

PrefUtil.setValue("isLogin", true);

Get the value like:

PrefUtil.getValue("isLogin", false) as bool

By this, it will initialize only once and get it where ever you need.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 iBob101
Solution 2 Kirill Karmazin
Solution 3 Andrey Gordeev
Solution 4
Solution 5 Coldstar
Solution 6