In this post I will show a singleton implementation I created to read / write to the Android Datastore. Android Datastore is the newer version of Android Preferences.
A singleton is very important when using Android Datastore. If not implemented properly, the app could crash if it happens to access the datastore in multiple instances. It will state there is more than 1 instance accessing the datastore file.
Accessing the datastore to perform any operation needs to be inside a runBlocking{ } method or other similar method. This will ensure the task is finished before continuing. It is also extra protection to ensure only one instance will attempt to read / write at a single time.
Code
Need to add the library to the module build.gradle
implementation(libs.androidx.datastore.preferences)
libs.versions.toml
[versions]
datastorePreferences = "1.1.2"
.....
[libraries]
androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastorePreferences" }
....
This is the DataStore object. I am working on JWTs hence the naming convention.
The important piece of creating a singleton is to use the companion object portion of the class. Also include the @Volatile annotation tag for the INSTANCE variable. This will ensure the same part of memory is used for the INSTANCE variable.
The getInstance function is run inside the companion object method to check if there is an instance. If there is no instance then it creates one.
class TokenSingleton {
companion object {
lateinit var dataStore: DataStore<Preferences>
@Volatile
private var INSTANCE: TokenSingleton? = null
fun getInstance(context : Context): TokenSingleton? {
if (INSTANCE == null) {
synchronized(this) {
if (INSTANCE == null) {
// create the singleton instance
INSTANCE = TokenSingleton()
dataStore = PreferenceDataStoreFactory.create(
corruptionHandler = ReplaceFileCorruptionHandler(
produceNewData = { emptyPreferences() },
),
produceFile = {
context
.preferencesDataStoreFile("test_jwt_datastore")
}
)
}
}
}
return INSTANCE
}
}
suspend fun getAllInfo() : JWT {
val prefs = dataStore.data.first()
return JWT(prefs[PrefKeys.ACC_TOKEN] ?: PrefKeys.EMPTY, prefs[PrefKeys.REFRESH_TOKEN] ?: PrefKeys.EMPTY)
}
suspend fun getAccToken() : String {
val prefs = dataStore.data.first()
return prefs[PrefKeys.ACC_TOKEN] ?: PrefKeys.EMPTY
}
suspend fun getRefreshToken() : String {
val prefs = dataStore.data.first()
return prefs[PrefKeys.REFRESH_TOKEN] ?: PrefKeys.EMPTY
}
suspend fun updateAccToken(acc_token : String){
dataStore.edit {
it[PrefKeys.ACC_TOKEN] = acc_token
}
}
suspend fun updateRefreshToken(refresh_token : String){
dataStore.edit {
it[PrefKeys.REFRESH_TOKEN] = refresh_token
}
}
suspend fun clearData(){
dataStore.edit { it.clear() }
}
}
private object PrefKeys {
val ACC_TOKEN = stringPreferencesKey("acc_token")
val REFRESH_TOKEN = stringPreferencesKey("refresh_token")
val EMPTY = "HEADER.PAYLOAD.SIGNATURE"
}
A sample of how to use the above code would be
....
//get the acc_token preference
val acc_token : String = runBlocking {
TokenSingleton.geInstance(applicationContext)!!.getAccToken()
}
....
//edit the acc_token preference
runBlocking {
TokenSingleton.getInstance(applicationContext)!!.updateAccToken(acc_token)
}
BONUS
Below is a different approach to creating this using a class implementation.
class TokenManager(val context: Context) {
suspend fun getAllInfo() : JWT {
val prefs = context.dataStore.data.first()
return JWT(prefs[PreferenceKeys.ACC_TOKEN] ?: "", prefs[PreferenceKeys.REFRESH_TOKEN] ?: "")
}
suspend fun getAccToken() : String {
val prefs = context.dataStore.data.first()
return prefs[PreferenceKeys.ACC_TOKEN] ?: ""
}
suspend fun getRefreshToken() : String {
val prefs = context.dataStore.data.first()
return prefs[PreferenceKeys.REFRESH_TOKEN] ?: ""
}
suspend fun updateAccToken(acc_token : String){
context.dataStore.edit {
it[PreferenceKeys.ACC_TOKEN] = acc_token
}
}
suspend fun updateRefreshToken(refresh_token : String){
context.dataStore.edit {
it[PreferenceKeys.REFRESH_TOKEN] = refresh_token
}
}
suspend fun clearData(){
context.dataStore.edit { it.clear() }
}
}
private val Context.dataStore by preferencesDataStore(name = "test_jwt_datastore")
private object PreferenceKeys {
val ACC_TOKEN = stringPreferencesKey("acc_token")
val REFRESH_TOKEN = stringPreferencesKey("refresh_token")
}