15

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;
}
NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
Marcin Szałek
  • 4,609
  • 5
  • 30
  • 43
  • There is no way to make a asynchronous API into a synchronous one. Your example won't work because the return type of `await ` is always `Future` where `result` is the return type of `expression`. Reading and writing from shared preferences can always fail - so you should be careful. – Jonah Williams Apr 02 '18 at 05:22
  • Where exactly you think it would not work? I don't think `await ` returns `Future`. That's why we use `await`, don't we? – Marcin Szałek Apr 02 '18 at 05:34
  • `await` is just sugar for `.then`. The type of `sp` is actually `Future`. – Jonah Williams Apr 02 '18 at 05:47
  • I have the same issue. My "sp" variable is a global variable which I can initialise and use (by retrieving setting and loading them into variables) but when I later use it from another route in my app (the settings page in which the user may change a setting) sp has become null. – iBob101 Apr 28 '18 at 17:49
  • I think I've just figured my problem out. I was using a 'package' import reference in my 'settings' dart file to refer to the dart file with my global variable. See https://stackoverflow.com/questions/45772318/flutter-dart-static-variables-lost-keep-getting-reinitialized – iBob101 Apr 28 '18 at 17:55

6 Answers6

19

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;.

iBob101
  • 1,460
  • 3
  • 14
  • 20
6

@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

Kirill Karmazin
  • 6,256
  • 2
  • 54
  • 42
  • 1
    In newer version of Flutter, if you got an error due to adding async await on `main()`, just add this statement `WidgetsFlutterBinding.ensureInitialized()` in start of `main()` function – Rahmat Ali Jul 15 '20 at 10:02
  • 1
    Need to add `late` to avoid CTE like `static late SharedPreferences localStorage;` – Shailendra Madda May 04 '22 at 17:34
  • I don't know why `shared_preferences` isn't available synchronously by default. That's how User Defaults works on iOS. It's worth a slight startup delay to not have code filled with Futures! – Clifton Labrum Aug 15 '22 at 17:18
4

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 ...
  }
Andrey Gordeev
  • 30,606
  • 13
  • 135
  • 162
4

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.

Shailendra Madda
  • 20,649
  • 15
  • 100
  • 138
  • 1
    Hi @Shailendra Madda is this one of the best way to initialise SharedPreferences at root level, which will make it globally available to **read** or **write** the value. In addition to above solution, to remove any value `static Object removeValue(String key) { return preferences.remove(key); }` – Indrajeet Singh Sep 02 '22 at 12:48
2

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');
NearHuscarl
  • 66,950
  • 18
  • 261
  • 230
1
  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
Coldstar
  • 1,324
  • 1
  • 12
  • 32