7

I need a Singleton for the shared preferences which has async initialisation but also works with null safety. Usually I used the following singleton implementation, but what is the best way to have a singleton which works with null safety?

class SharedPrefs {
static SharedPrefs _instance;
static Future<Null> _mutex;

static Future<SharedPrefs> getInstance() async {
  if (_mutex != null) {
    await _mutex;
  }
  var completer = Completer<Null>();
  _mutex = completer.future;

  if (_instance == null) {
    _instance = SharedPrefs();
    await _instance.init();
  }

  completer.complete();
  _mutex = null;

  return _instance;
}

SharedPreferences prefs;

SharedPrefs();

Future<SharedPrefs> init() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  return this;
}

dynamic get(String key) {
  return prefs.getString(key);
}

Future<bool> put(String key, dynamic value) async {
  return await prefs.setString(key,value);
}
}
midi
  • 3,128
  • 5
  • 30
  • 47

4 Answers4

6

Here you go:

class SharedPreferencesProvider {
  static SharedPreferencesProvider? _instance;
  final SharedPreferences _sharedPreferences;

  static Future<SharedPreferencesProvider> getInstance() async {
    if (_instance == null) {
      final sharedPreferences = await SharedPreferences.getInstance();
      _instance = SharedPreferencesProvider._(sharedPreferences);
    }
    return _instance!;
  }

  SharedPreferencesProvider._(SharedPreferences sharedPreferences)
      : _sharedPreferences = sharedPreferences;
pedro pimont
  • 2,764
  • 1
  • 11
  • 24
3

init-first singleton

class Singleton {
  late Map<String,String> obj;

  Db._();
  static Db? _instance;
  static Db get inst => _instance ??= Db._();
  init () async {
    // await ...
    obj = {'a': 1};
  }
}

void main () async {
  // put init first
  await Db.inst.init();

  // your other code
  // your other code

  print(Db.inst.obj);

  // your other code
  // your other code


}

I use this method in all languages which is stable and easy to understand.

A tested example

db.dart

import 'package:mongo_dart/mongo_dart.dart' as Mongo;

class Db {
  late Mongo.DbCollection test;

  Db._();
  static Db? _instance;
  static Db get inst => _instance ??= Db._();
  init () async {
    var db = Mongo.Db("mongodb://localhost:27017/");
    await db.open();
    test = db.collection('test');
  }
}

test.dart

import 'package:mj_desk_server/db.dart';

void main () async {
  
  await Db.inst.init();
  
  myApp();
}

myApp () async {
  // do anything
  // do anything
  // do anything
  print(await Db.inst.test.find().toList());
  print('ok');
}
Yin
  • 612
  • 7
  • 10
0

Not sure if accepted answers works when there are two calls to SharedPreferencesProvider.getInstances() back to back

The following code should work no matter how many back to back calls the getInstance() method gets, and how much ever time the creation takes

class SharedPreferencesProvider {
  static SharedPreferences? _backingFieldPrefs;
  static Future<SharedPreferences>? _prefsFuture;

  static Future<SharedPreferences> getInstance() async {
    if (_backingFieldPrefs != null) {
      return _backingFieldPrefs!;
    }

    _prefsFuture ??= await SharedPreferences.getInstance();
    _backingFieldPrefs ??= await _prefsFuture!;
    return _backingFieldPrefs!;
  }
}
okmanideep
  • 960
  • 7
  • 23
0

I think the simplest solution (and safe at the same time) caches the Future in a static final attribute. No need for any nullable class attributes at all.

import 'package:shared_preferences/shared_preferences.dart';

class SharedPreferencesProvider {
  static final _instance = () async {
    final sp = await SharedPreferences.getInstance();
    return SharedPreferencesProvider._(sp);
  }();

  static Future<SharedPreferencesProvider> getInstance() => _instance;

  const SharedPreferencesProvider._(SharedPreferences sharedPreferences)
      : _sharedPreferences = sharedPreferences;
  
  final SharedPreferences _sharedPreferences;
}
Tomasz Noinski
  • 456
  • 2
  • 10