Implementing and Creating A Singleton of Android DataStore To Save JWT

You are currently viewing Implementing and Creating A Singleton of Android DataStore To Save JWT

In this post I am showing the singleton implementation I created to read / write to the Android Datastore which is what was Android Preferences.

A singleton is very important otherwise the app will crash stating there is more than 1 instance accessing the datastore file.

When accessing the datastore with the code I am posting here, make sure to do any operation ( read / write ) inside runBlocking { }. This is 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 along with the @Volatile tag for the INSTANCE variable.

The getInstance fun is run inside the companion object method to check if there is an instance, if not 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")
}

Leave a Reply