Skip to content

SharedPreferences should have better singleton instantiation #42407

@magnuswikhog

Description

@magnuswikhog

As discussed in this StackOverflow question, the getInstance() method in the SharedPreferences plugin is not reliable when called from both a microtask and regular code. The problem seems to be that _instance == null will be true until await _getSharedPreferencesMap(); has returned, and therefor it's possible to get two different instances from this singleton factory.

static Future<SharedPreferences> getInstance() async {
  if (_instance == null) {
    final Map<String, Object> preferencesMap =
        await _getSharedPreferencesMap();
    _instance = SharedPreferences._(preferencesMap);
  }
  return _instance;
}

Steps to Reproduce

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

initAsync() async {
  scheduleMicrotask(() async {
    print("microtask: START ");
    TestClass microtask = await TestClass.getInstance("microtask");
    print("microtask: FINISH (instance: ${microtask.hashCode})");
  });

  print("initAsync_1: START");
  TestClass initAsync_1 = await TestClass.getInstance("initAsync_1");
  print("initAsync_1: FINISH (instance: ${initAsync_1.hashCode})");

  Future.delayed(Duration(seconds: 3)).then((_) async {
    print("initAsync_2: START");
    TestClass initAsync_2 = await TestClass.getInstance("initAsync_2");
    print("initAsync_2: FINISH (instance: ${initAsync_2.hashCode})");
  });
}

class TestClass {
  static TestClass _instance;
  static Future<TestClass> getInstance(String call) async {
    if (_instance == null) {
      print("$call: without instance before");
      await Future.delayed(Duration(seconds: 3));
      _instance = TestClass();
      print("$call: without instance after");
    } else {
      print("$call: with instance");
    }
    return _instance;
  }
}

Logs

initAsync_1: START
initAsync_1: without instance before
microtask: START 
microtask: without instance before
initAsync_1: without instance after
initAsync_1: FINISH (instance: 674207757)
microtask: without instance after
microtask: FINISH (instance: 1059788553)
initAsync_2: START
initAsync_2: with instance
initAsync_2: FINISH (instance: 1059788553)

Possible solution

One possible solution would be to use a Completer:

static Future<SharedPreferences> getInstance() async {
    if( _initialization != null && !_initialization.isCompleted ) {
        await _initialization.future;
    }

    if (_instance == null) {
        _initialization = Completer<void>();
        
        try {
            final Map<String, Object> preferencesMap =
            await _getSharedPreferencesMap();
            _instance = SharedPreferences._(preferencesMap);
        }
        finally{
            _initialization.complete();
        }
    }

    return _instance;
}

Metadata

Metadata

Labels

P2Important issues not at the top of the work listp: shared_preferencesPlugin to read and write Shared Preferencespackageflutter/packages repository. See also p: labels.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions