16

I want to store some preferences using DataStore. But the problem is that my application can have multiple users and therefor needs to store these preferences in separate files. I got a working example using only one user but I'm struggling to support multiple users.

Here is an example of my code:

class DataStorageRepository(private val context: Context, private val userRepository: UserRepository) {

    private object PreferencesKeys {
        val SETTING_ONE = intPreferencesKey("setting_one")
    }

    // retrieve datastore for currently logged in user. 
    private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = userRepository.currentRegistration().name)

    val userPreferencesFlow: Flow<UserPreferences> = context.dataStore.data.map { preferences ->
        val settingOne = preferences[PreferencesKeys.SETTING_ONE] ?: 0

        UserPreferences(settingOne)
    }

    suspend fun storeSettingOne(settingOne: Int) {
        context.dataStore.edit { preferences ->
            preferences[PreferencesKeys.SETTING_ONE] = settingOne
        }
    }

    data class UserPreferences(val lastUsedToAccountTab: Int)
}

I'm using Koin and I tried unloading the DataStorageRepository on logout and recreating it on login but the DataStore seems to stay alive until the app is killed and I get the following crash:

java.lang.IllegalStateException: There are multiple DataStores active for the same file: [...] You should either maintain your DataStore as a singleton or confirm that there is no two DataStore's active on the same file (by confirming that the scope is cancelled).

I also tried to use a CoroutineScope and kill that when I log out, but after recreating the scope on login the DataStore doesn't seem to get recreated.

Does DataStore support a way to close the connection or to handle multiple files?

Wirling
  • 4,810
  • 3
  • 48
  • 78

5 Answers5

15

Put this line inside companion object { }

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settingPrefs")

My Code

class SettingPrefs(private val context: Context) {

    companion object {
        private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settingPrefs")
        private val soundKey = booleanPreferencesKey("sound")
        private val vibrateKey = booleanPreferencesKey("vibrate")
    }

    val getSound: Flow<Boolean>
        get() = context.dataStore.data.map {
            it[soundKey] ?: true
        }

    suspend fun setSound(value: Boolean) {
        context.dataStore.edit { it[soundKey] = value }
    }

    val getVibration: Flow<Boolean>
        get() = context.dataStore.data.map {
            it[vibrateKey] ?: true
        }

    suspend fun setVibration(value: Boolean) {
        context.dataStore.edit { it[vibrateKey] = value }
    }
}
Mohd Naushad
  • 514
  • 4
  • 15
7

You can use different key for different user or manual keep DataStore singleton.


For exception:

java.lang.IllegalStateException: There are multiple DataStores active for the same file: [...] You should either maintain your DataStore as a singleton or confirm that there is no two DataStore's active on the same file (by confirming that the scope is cancelled).

androidx.datastore:datastore-*:1.0.0-alpha07 is released.

Put this at the top level of your kotlin file so there is only one instance of it.

private val Context.dataStore by preferencesDataStore("settings")

class Xxx{

}

https://developer.android.com/jetpack/androidx/releases/datastore#1.0.0-alpha07.

The Context.createDataStore extension function has been removed and replaced with globalDataStore property delegate. Call globalDataStore once at the top level in your kotlin file. For example:

val Context.myDataStore by dataStore(...) 

Put this at the top level of your kotlin file so there is only one instance of it. (I57215, b/173726702)

mixer
  • 1,583
  • 14
  • 27
5

At the moment I was posting this question I found a solution to this problem. In order to solve my problem I needed to combine my previous two solutions. So on logout I unload the DataStorageRepository and on login I reload it again. I also needed to create a CoroutineScope that I cancel on logout.

My Module

val loggedInModule = module {
    single { DataStorageRepository(get(), get()) }
}

I created a scope and passed it to the DataStore

var loggedInScope: CoroutineScope = CoroutineScope(Dispatchers.Default)

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = userRepository.currentRegistration().name, scope = loggedInScope)

On Login

loggedInScope = CoroutineScope(Dispatchers.Default)
loadKoinModules(loggedInModule)

On Logout

loggedInScope.cancel()
unloadKoinModules(loggedInModule)
Wirling
  • 4,810
  • 3
  • 48
  • 78
2

Just put your declaration datastore out of your DataStorageRepository class

private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name="settings")

class DataStorageRepository(context: Context) {

   private var appContext = context.applicationContext

   val mData: Flow<String?> = appContext.dataStore.data.map { preferences ->
          preferences[YOUR_KEY]
   }

   suspend fun insertData(value: String) {
        appContext.dataStore.edit { preferences ->
             preferences[YOUR_KEY] = authToken
        }
   }

   companion object {
        private val KEY = stringPreferencesKey("data")
   }
}
0

This is what I'm using in my project:

private object UserIdBasedPrefDs {

  val lock = Any()

  @GuardedBy("lock")
  @Volatile
  var currentId: String = ""

  @GuardedBy("lock")
  @Volatile
  var INSTANCE: DataStore<Preferences>? = null
}

fun Context.happyStore( // rename what ever you like.
  userId: String,
  // below 3 optional params are same as int the `preferencesDataStore`.
  corruptionHandler: ReplaceFileCorruptionHandler<Preferences>? = null,
  produceMigrations: (Context) -> List<DataMigration<Preferences>> =
    { listOf() },
  scope: CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())

): DataStore<Preferences> = UserIdBasedPrefDs.run {

  // check if current user id has been changed.
  if (userId != currentId && INSTANCE != null) {
    synchronized(lock) {
      // release previous saved.
      INSTANCE = null
      // reset new user id.
      currentId = userId
    }
  }

  // below is the same logic inside the `preferencesDataStore` delegate.
  INSTANCE ?: synchronized(lock) {
    if (INSTANCE == null) {
      INSTANCE = PreferenceDataStoreFactory.create(
        corruptionHandler = corruptionHandler,
        migrations = produceMigrations(applicationContext),
        scope = scope
      ) {
        applicationContext.preferencesDataStoreFile(userId)
      }
    }
    INSTANCE!!
  }
}

Hope it can be helpful to you.

rockyoung
  • 36
  • 4