tfcporciuncula / flow-preferences Goto Github PK
View Code? Open in Web Editor NEWKotlin Flow version of rx-preferences -- Coroutines support for Android SharedPreferences
License: Apache License 2.0
Kotlin Flow version of rx-preferences -- Coroutines support for Android SharedPreferences
License: Apache License 2.0
Right now the library doesn't support null values. However, shared preference does, so I think you should consider to include it.
I recently migrated a class that uses https://github.com/f2prateek/rx-preferences to now use FlowPreferences
. In our app, when the user logs out, we clear all SharedPreferences
. During the logout, the app is crashing with the following exception:
kotlinx.coroutines.JobCancellationException: FlowSubscription was cancelled; job=FlowSubscription{Cancelled}@bed4c69
There isn't really any more details regarding the stack trace. I suspect I am running into Kotlin/kotlinx.coroutines#974 which may make sense given FlowSharedPreferences.keyFlow
is implemented as such:
val keyFlow = callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> offer(key) }
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
}
Something tells me that the offer(key)
is being performed after the Channel
has been closed.
Hey, we are getting random NPEs on:
java.lang.NullPointerException: Attempt to invoke interface method 'void android.content.SharedPreferences$OnSharedPreferenceChangeListener.onSharedPreferenceChanged(android.content.SharedPreferences, java.lang.String)' on a null object reference
at androidx.security.crypto.EncryptedSharedPreferences$Editor.notifyListeners(EncryptedSharedPreferences.java:349)
at androidx.security.crypto.EncryptedSharedPreferences$Editor.apply(EncryptedSharedPreferences.java:325)
at com.tfcporciuncula.flow.StringPreference.set(StringPreference.kt:17)
at com.tfcporciuncula.flow.StringPreference.set(StringPreference.kt:7)
I'm pretty sure it revolves around SharedPreferences.java:367 ff.:
/**
* Registers a callback to be invoked when a change happens to a preference.
*
* <p class="caution"><strong>Caution:</strong> The preference manager does
* not currently store a strong reference to the listener. You must store a
* strong reference to the listener, or it will be susceptible to garbage
* collection. We recommend you keep a reference to the listener in the
* instance data of an object that will exist as long as you need the
* listener.</p>
*
* @param listener The callback that will run.
* @see #unregisterOnSharedPreferenceChangeListener
*/
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
So I would suggest changing FlowSharedPreferences.kt:19-23
internal val keyFlow: KeyFlow = callbackFlow {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> offer(key) }
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
}
to
private lateinit var sharedPreferencesListener: SharedPreferences.OnSharedPreferenceChangeListener
internal val keyFlow: KeyFlow = callbackFlow {
sharedPreferencesListener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> offer(key) }
sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferencesListener)
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferencesListener) }
}
I tried opening a PR myself but I haven't setup my github account locally right now.
Great work otherwise!
Should be enough to document Preference
and FlowSharedPreferences
.
While writing a test for my implementation with FlowSharedPreferences
i discovered a unexpected behavior of the Flow:
@Test
fun testBug3() = runTest {
val tokenPreferences = getApplicationContext<MDEApplication>().getSharedPreferences("Token", Context.MODE_PRIVATE)
val tokenPreferencesFlow = FlowSharedPreferences(tokenPreferences).apply { clear() }
val accessTokenPreferenceFlow = tokenPreferencesFlow.getFloat("access_token", defaultValue = 0.5f)
launch {
Log.i("TESTX", "collection started!")
accessTokenPreferenceFlow.asFlow()
// .distinctUntilChanged()
.collectLatest {
Log.i("TESTX", "collected: $it")
}
}
launch {
Log.i("TESTX", "setAndCommit 20")
accessTokenPreferenceFlow.setAndCommit(20f)
Log.i("TESTX", "setAndCommit 30")
accessTokenPreferenceFlow.setAndCommit(30f)
Log.i("TESTX", "setAndCommit 50")
accessTokenPreferenceFlow.setAndCommit(50f)
}
}
}
TEST: collection started!
TEST: setAndCommit 20
TEST: collected: 20.0
TEST: setAndCommit 30
TEST: setAndCommit 50
TEST: collected: 50.0
TEST: collected: 50.0
As far as i can see right know, the two issues belong together.
In my Test i need to have a start value collected on flow collection start (Value X). Then later on, a second value (Y) is set and needs to be processed by the collection also. But right now, the first value is never processed, instead the first collection is the second value (Y). The first value is skipped for some unknown reason like 30 in the example above.
Sometimes even worse:
setAndCommit X
collection started!
setAndCommit Y
collected: X
Y is never collected. Right know i really don't know whats going on. :)
RxPreferences allows to read preference key which is useful for debug purposes. However FlowPreferences interface doesn't have preference key accessing method declaration. Would be good if this will be added in future releases.
For example, I have this:
val activeDaysCount = prefs.getLong("app_active_days_count", defaultValue = 0L)
If I want change it to Int, I get error, because previous value was saved as Long
I would like to be able to observe any key or only a bunch of keys inside a preference file - any plans on adding this?
Something like following:
flowSharedPreferences
.asFlow("darkTheme", "mainColor", "accentColor") // list of keys
.onEach {
print(it)
}
.launchIn(scope)
// or following
flowSharedPreferences
.asFlowAny() // all preference changes will be emitted here
.onEach {
print(it)
}
.launchIn(scope)
The flow should emit a custom class that also holds the key, e.g. a simple sealed class...
I'm having an issue where after the initial value my Flows aren't updating when a new value is saved in SharedPreferences.
I've linked to a project that reproduces it but the gist is:
val myPref: Preference<Int> = preferences.getInt(MY_PREF_KEY)
update_pref.setOnClickListener {
val selection = (0..2).random()
GlobalScope.launch { myPref.setAndCommit(selection) }
}
myPref.asFlow()
// This doesn't get triggered after the initial value
.onEach { Log.d(tag, "PREF CHANGE: $it") }
.launchIn(GlobalScope)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.