Issue
I'm currently working on a small project that uses DataStore<Preferences>
to save some data. At seemingly random times, when I would uninstall and reinstall the app, I would get the exception: java.lang.IllegalStateException: There are multiple DataStores active for the same file
This is the relevant code:
class MainActivity : ComponentActivity() {
private lateinit var appControllerSettings: AppControllerSettings
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
appControllerSettings = AppControllerSettings.getInstance(this@MainActivity)
}
}
}
class AppControllerSettings private constructor(context: Context) {
companion object {
private val USERS_KEY = stringSetPreferencesKey("users")
private val mutex = Mutex()
private var instance: AppControllerSettings? = null
/**
* Return the singleton instance of [AppControllerSettings]
* */
suspend fun getInstance(context: Context): AppControllerSettings {
return instance ?: mutex.withLock {
instance ?: AppControllerSettings(context).apply {
try {
val preferences = withContext(Dispatchers.IO) {
preferencesStore.data.first()
}
val userIds: Set<String>? = preferences[USERS_KEY]
if (userIds != null) {
users.addAll(userIds)
}
} catch (ioe: IOException) {
debugError("failed to read app controller settings from preferences store", ioe)
}
instance = this
}
}
}
}
private val preferencesStore = createPreferencesStoreFromFile(context, "app_controller_settings")
private val users: MutableSet<String> = mutableSetOf()
suspend fun addUserToSignInHistory(user: User): Result<Unit> {
return runCatching {
withContext(Dispatchers.IO) {
preferencesStore.edit { preferences ->
users.add(user.id)
preferences[USERS_KEY] = users
}
}
}
}
fun hasUserPreviouslySignedIn(user: User): Boolean {
return users.contains(user.id)
}
}
I have tested the code and getInstance
returns the same instance every time it is called in code, but the exception still occurs. After adding some logging statements I saw the difference between when there is a crash and when there is no crash:
Working
MainActivity class initialized
MainActivity object instance created
onCreate
AppControllerSettings.getInstance is called
AppControllerSettings class initialized
instance = null
PreferenceDataStoreFactory.create app_controller_settings
AppControllerSettings object instance created
onStart
onResume
Crashes
MainActivity class initialized
MainActivity object instance created
onCreate
AppControllerSettings.getInstance is called
AppControllerSettings class initialized
instance = null
PreferenceDataStoreFactory.create app_controller_settings
AppControllerSettings object instance created
onStart
onResume
onPause
onStop
onDestroy
MainActivity object instance created
onCreate
AppControllerSettings.getInstance is called
instance = null
onStart
onResume
PreferenceDataStoreFactory.create app_controller_settings
AppControllerSettings object instance created
java.lang.IllegalStateException: There are multiple DataStores active for the same file
I have a couple of questions about this:
- Why is the activity sometimes being created and destroyed and created again? No screen rotation is taking place. I have gone through the process of simply installing and uninstalling the app from ADB and Android Studio over and over again, and this behavior occurs about 1 in 4 times. A few minutes before I started to write this question, the MainActivity class itself was being reinitialized, not just a new object instance created, but that behavior has now suddenly stopped.
- Even if a new MainActivity object is being created, how is the static field
instance
inside ofAppControllerSettings
being reset to null? It's a separate class in the same file.
EDIT
This seems to at least answer question 2: https://discuss.kotlinlang.org/t/singleton-with-object-declarations-gets-garbage-collected/4786/5 The AppControllerSettings
class is only initialized when it's first used and is then a candidate for garbage collection if no reference is held to it anywhere than the MainActivity
which is disposed of. The only solution seems to be to initialize AppControllerSettings
in a custom Application
class.
Solution
I never figured out why at some times my activity is being created, destroyed and created again while simply launching the app with no other changes, but I ended up resolving my problem via the singleton pattern.
As Android doesn't give you any mechanism by which to close a DataStore
, once a DataStore
is opened for a given file it remains open for the lifetime of the application.
The solution was to create a DataStoreManager
like this:
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStoreFile
interface DataStoreManager {
companion object {
private val LOCK = Any()
private var instance: DataStoreManager? = null
fun getInstance(context: Context): DataStoreManager {
return instance ?: synchronized(LOCK) {
if (instance == null) {
instance = DataStoreManagerImpl(context)
}
instance!!
}
}
}
fun datastore(name: String): DataStore<Preferences>
}
private class DataStoreManagerImpl(private val context: Context) : DataStoreManager {
private val datastores = mutableMapOf<String, DataStore<Preferences>>()
override fun datastore(name: String): DataStore<Preferences> {
return datastores.computeIfAbsent(name) {
PreferenceDataStoreFactory.create {
context.preferencesDataStoreFile(name)
}
}
}
}
and pass that into my AppControllerSettings like so:
class AppControllerSettings private constructor(dataStoreManager: DataStoreManager) {
companion object {
private val USERS_KEY = stringSetPreferencesKey("users")
private val mutex = Mutex()
private var instance: AppControllerSettings? = null
/**
* Return the singleton instance of [AppControllerSettings]
* */
suspend fun getInstance(dataStoreManager: DataStoreManager): AppControllerSettings {
return instance ?: mutex.withLock {
instance ?: AppControllerSettings(dataStoreManager).apply {
try {
val preferences = withContext(Dispatchers.IO) {
preferencesStore.data.first()
}
val userIds: Set<String>? = preferences[USERS_KEY]
if (userIds != null) {
users.addAll(userIds)
}
} catch (ioe: IOException) {
debugError("failed to read app controller settings from preferences store", ioe)
}
instance = this
}
}
}
}
private val preferencesStore = dataStoreManager.datastore("app_controller")
private val users: MutableSet<String> = mutableSetOf()
suspend fun addUserToSignInHistory(user: User): Result<Unit> {
return runCatching {
withContext(Dispatchers.IO) {
preferencesStore.edit { preferences ->
users.add(user.id)
preferences[USERS_KEY] = users
}
}
}
}
fun hasUserPreviouslySignedIn(user: User): Boolean {
return users.contains(user.id)
}
}
This way dataStoreManager.datastore(String)
can be called many times, but only way DataStore will ever be created for a given name. Obvious in retrospect.
Answered By - Adam
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.