Skip to content

SharedPreferences should have better singleton instantiation #42407

Closed
flutter/packages
#4159

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

Activity

  1. added
    P2Important issues not at the top of the work list
    on Jun 29, 2021
  2. sanekyy commented on May 19, 2022

    @sanekyy

    This issue still actual and can produce more than 1 instance:(

  3. gslender commented on Jun 5, 2023

    @gslender
    Contributor

    I'm seeing this pop up on lots of iOS users using a production app. Been tearing out my hair trying to figure this one out and why we're seeing issues in production that are hard to replicate in test.

    Is there any workaround that can be applied to using SharedPreferences to avoid this bug?

  4. gslender commented on Jun 5, 2023

    @gslender
    Contributor

    By "this" I mean cases where data stored is not able to be read moments later in the app. There is definitely some challenges around the async nature and even using reload() seems to have no improvement in ensuring the data is kept. I'm always using await with setString but for some unexplained reason, there are cases when getString followed after a setString results in the value not being stored !?

  5. gslender commented on Jun 7, 2023

    @gslender
    Contributor

    I've raised an issue #128368 that speaks to the issue I'm having, of which this issue here seems to be related

  6. stuartmorgan-g commented on Jun 7, 2023

    @stuartmorgan-g
    Contributor

    This issue still actual and can produce more than 1 instance:(

    It turns out that this was fixed years ago (slightly before this issue was filed, in fact), but regressed during the NNBD transition.

  7. stuartmorgan-g commented on Jun 7, 2023

    @stuartmorgan-g
    Contributor

    even using reload() seems to have no improvement in ensuring the data is kept

    If calling reload() before reading doesn't help, then your case is unrelated to this one. This could cause there to be different instances of the Dart-side cache, but the purpose of reload() is to update that cache with the current native state.

  8. github-actions commented on Jun 22, 2023

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  9. locked as resolved and limited conversation to collaborators on Jun 22, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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