Sergey Shevtsov
02/13/2023, 4:31 PMprivate val preferences = // shared preferences initialization
private val mutex = Mutex()
override fun observeString(prefKey: String, defaultValue: String?): Flow<String?> {
return observeValueChanges(prefKey).mapWithLock {
preferences.getString(prefKey, defaultValue)
}.flowOn(<http://Dispatchers.IO|Dispatchers.IO>)
}
override fun observeBoolean(prefKey: String, defaultValue: Boolean): Flow<Boolean> {
return observeValueChanges(prefKey).mapWithLock {
preferences.getBoolean(prefKey, defaultValue)
}.flowOn(<http://Dispatchers.IO|Dispatchers.IO>)
}
private fun observeValueChanges(prefKey: String): Flow<Unit> {
return callbackFlow {
val listener = OnSharedPreferenceChangeListener { _, key ->
if (prefKey == key) {
trySend(Unit)
}
}
preferences.registerOnSharedPreferenceChangeListener(listener)
send(Unit)
awaitClose { preferences.unregisterOnSharedPreferenceChangeListener(listener) }
}
}
private inline fun <T, R> Flow<T>.mapWithLock(crossinline transform: suspend (T) -> R): Flow<R> {
return transform { value ->
return@transform emit(
mutex.withLock {
transform(value)
}
)
}
}
Coroutines mutex locking is used for synchronized write/read operations to avoid ConcurrentModificationException.
The issue is that registering/unregistering a listener is not thread safe. I can't just synchronize this with the coroutine mutex, because the awaitClose lambda is not suspend.
Possible solutions, in my opinion, are the following:
1. Creating a single thread dispatcher and calling registering/unregistering on them.
2. Just to use synchronized { }
, but here's the question. How good is this practice of mixing concurrency of coroutines and no-coroutines?
I would be glad to see more elegant solutions, if any. Thank you.Nick Allen
02/13/2023, 5:57 PMsynchronized { ... }
is fine to call with coroutines so long as you don't include suspend methods in the synchronized block, in which case, it has all the same benefits and risks as when used with non-coroutine code.denis090712
02/13/2023, 6:10 PMSergey Shevtsov
02/13/2023, 6:58 PMsynchronized { ... }
option.denis090712
02/13/2023, 7:24 PMNick Allen
02/13/2023, 10:54 PMThe issue with the ConcurrentModificationException has been solved with the coroutines mutex.That's seems unlikely given that those methods are already thread safe. The entire bodies of
getString
and getBoolean
are wrapped in synchronized
blocks when I peek at the AOSP.
I asked about registering/unregistering of listeners. Without any kind of synchronization, these operations may be performed in the wrong order. I'm looking for a good way to synchronize them.Same. Also, code above
awaitClose
will run entirely before code inside the awaitClose
block.uli
02/14/2023, 8:18 PMSergey Shevtsov
02/16/2023, 3:27 PMuli
02/16/2023, 3:57 PMeditor.commit()/.apply()
call!preferences.registerOnSharedPreferenceChangeListener(listener)
send(Unit)
var registered = MutableStateFlow<Boolean>(true)
launch {
registered.filter{ it.not }
.first {
preferences.unregisterOnSharedPreferenceChangeListener(listener)
}
}
awaitClose { registered = false }