Dart Singleton Async

Erick
2 min readMar 20, 2019

--

Let’s say we want to create singleton class for our Database Manager, so we can access it everywhere on the code.

We create the class like this

class DBManager {

// Singleton pattern
static final DBManager _dbManager = new DBManager._internal();
DBManager._internal();
static DBManager get instance => _dbManager;
// Member
static
Database _database;
Database get database {
return _database;
}
}

Then, we an access the value of our Database using code below.

var db = DBManager.instance.database;

Looks clean. But the code doesn’t do anything yet.

Now let’s connect the database. In this example below i’m using flutter sqlite library.

So let’s add function to open the db.

Future<Database> _initDB() async {  // .. Copy initial database (data.db) from assets file to database path
// .. Let's assume we have copy our initial database, and put it to database path
String databasePath = await getDatabasesPath();
String databaseFilePath = p.join(databasePath, 'data.db');
return await openDatabase(databaseFilePath, version: 1);
}

Now let’s update our get database function

Future<Database> get database async {
if (_database != null)
return _database;
_database = await _initDB();
return _database;
}

Now, we can access the database and do some query.

var db = await DBManager.instance.database;
db.rawQuery('SELECT * FROM users');

Everything looks good until there is a case that you need to call the database twice in very short span of time, or there is another page calling it at the same time with your current page.

Let’s assume condition below happen.

var db = await DBManager.instance.database;
var db2 = await DBManager.instance.database;

This will trigger race condition as _initDB function will be called twice.

There will be a problem if you copy the initial db from assets, as it’s took some time, or you need to do some expensive stuff on _initDB function.

The concept also apply if you have other functions that takes time to finish.

How do we solve this issue?

AsyncMemoizer for the rescue! Async memoizer is a class for running an asynchronous function exactly once and caching its result.

So let’s update our get database method

final _initDBMemoizer = AsyncMemoizer<Database>();Future<Database> get database async {
if (_database != null)
return _database;

_database = await _initDBMemoizer.runOnce(() async {
return await _initDB();
});

return _database;
}

This way, _initDB will only called once and the result is cached.

Here is the class after all the changes.

class DBManager {

// Singleton pattern
static final DBManager _dbManager = new DBManager._internal();
DBManager._internal();
static DBManager get instance => _dbManager;

// Members
static Database _database;
final _initDBMemoizer = AsyncMemoizer<Database>();

Future<Database> get database async {
if (_database != null)
return _database;

// if _database is null we instantiate it
_database = await _initDBMemoizer.runOnce(() async {
return await _initDB();
});

return _database;
}

Future<Database> _initDB() async {
// .. Copy initial database (data.db) from assets file to database path
String databasePath = await getDatabasesPath();
String databaseFilePath = p.join(databasePath, 'data.db');

return await openDatabase(databaseFilePath, version: 1);
}

}

--

--