-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Completing everything. Workable app just pending readme touchups.
- Loading branch information
Showing
35 changed files
with
1,464 additions
and
315 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,35 @@ | ||
# Fused User Preferences | ||
|
||
This is an experimental project, trying to put the legacy SharedPreferences and the new Jetpack Preferences Datastore side by side by means of dependency inversion, to see how it would look if it were to provide the same preferences storages at the data layer. | ||
This is an experimental project that tries to put the legacy SharedPreferences and the new Jetpack | ||
Preferences Datastore side by side by means of dependency inversion to see how it would look if it | ||
were to provide the same preferences storage at the data layer. | ||
|
||
## Background | ||
|
||
In my first few Android apps dated back to 2010, we did not have any architecture to follow. We did not have fragments, so mostly we allocate an activity class for each screen. There, if we want to deal with user preferences, it was so easy that we could have placed the code under onResume or even onCreated. SharedPreferences is non-blocking, so that when it does not break, it works quick and simple for most small use cases. | ||
In my first few Android apps, which date back to 2010, we did not have any architecture to follow. | ||
We did not have fragments, so we mostly allocated an activity class for each screen. There, if we | ||
wanted to deal with user preferences, it was so easy that we could have placed the code under | ||
onResume or even onCreated. SharedPreferences is non-blocking, so it works quickly and simply for | ||
most small use cases when it does not break. | ||
|
||
Later on, people suggested that being non-blocking (and synchronus) can be a problem. | ||
Later on, people came up with more different architectures. | ||
- Later, people suggested that SharedPreferences being synchronous can be a problem. That is | ||
sensible when developers abuse SharedPreferences by storing a massive amount of key pairs. | ||
- Later, people came up with more different architectures, so we are not simply accessing user | ||
preferences right from the activity class. | ||
|
||
Eventually if we want to access user preferences, we have many more boilerplate code before actually executing the few lines that really do the work. | ||
Eventually, if we want to access user preferences, we can have many more boilerplate codes before | ||
executing the few lines that do the work. | ||
|
||
Now we have Jetpack Preference Datastore. It is asynchrous, which means when we want to retrieve user perference, the result is not immediately available. Up to this moment, if we want to observe preferences changes, there is a known limitation that we are not being told which preference key has changed. We know only _something_ has changed, so that we probably have to propagate _all_ the keys we are interested in, whenever we know _one_ of them has changed. | ||
Now we have Jetpack Preference Datastore. It is asynchronous, which means that when we want to | ||
retrieve user preferences, the result is not immediately available. Up to this moment, if we want to | ||
observe preference changes, there is a known limitation: we are not being told which key pair has | ||
changed. We know only _something_ has changed, so we probably have to propagate _all_ the keys we | ||
are interested in, even if we know that only _one_ has changed. | ||
|
||
## Approach | ||
|
||
No matter it is for SharedPreferences or Jetpack Preferences Datastore, even the core is just about less than 10 lines of code, this code project tries to put them to the right place when following the MVVM and Clean architecture. That means the UI will reach the ViewModel, which will then connect to a repository that invisiblty talk to either the SharedPreferences or the Jetpack Preferences Datastore data store. | ||
|
||
Whether for SharedPreferences or Jetpack Preferences Datastore, even if the core is about 10 lines | ||
of code, this code project tries to put them in the right place when following the MVVM and Clean | ||
architecture. That means the UI will reach the ViewModel, which will then connect to a repository | ||
that invisibly talks to either the SharedPreferences or the Jetpack Preferences Datastore data | ||
store. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,12 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<!-- | ||
<?xml version="1.0" encoding="utf-8"?><!-- | ||
~ Copyright (c) 2024. Ryan Wong ([email protected]) | ||
--> | ||
|
||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
xmlns:tools="http://schemas.android.com/tools"> | ||
|
||
<application | ||
android:name=".FusedUserPreferencesApp" | ||
android:allowBackup="true" | ||
android:dataExtractionRules="@xml/data_extraction_rules" | ||
android:fullBackupContent="@xml/backup_rules" | ||
|
24 changes: 24 additions & 0 deletions
24
app/src/main/java/com/rwmobi/fuseduserpreferences/FusedUserPreferencesApp.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
/* | ||
* Copyright (c) 2024. Ryan Wong ([email protected]) | ||
*/ | ||
|
||
package com.rwmobi.fuseduserpreferences | ||
|
||
import android.app.Application | ||
import dagger.hilt.android.HiltAndroidApp | ||
import timber.log.Timber | ||
|
||
@HiltAndroidApp | ||
class FusedUserPreferencesApp : Application() { | ||
|
||
override fun onCreate() { | ||
super.onCreate() | ||
|
||
if (BuildConfig.DEBUG) { | ||
Timber.plant(Timber.DebugTree()) | ||
} else { | ||
// TODO: If Firebase Crashlytics is available, replace with a CrashReportingTree here | ||
Timber.plant(Timber.DebugTree()) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,16 +2,12 @@ | |
* Copyright (c) 2024. Ryan Wong ([email protected]) | ||
*/ | ||
|
||
package com.rwmobi.fuseduserpreferences.data.datasources | ||
package com.rwmobi.fuseduserpreferences.data.datasources.preferences | ||
|
||
import kotlinx.coroutines.flow.SharedFlow | ||
import kotlinx.coroutines.flow.StateFlow | ||
|
||
const val PREF_KEY_STRING = "keyString" | ||
const val PREF_KEY_BOOLEAN = "keyBoolean" | ||
const val PREF_KEY_INT = "keyInt" | ||
|
||
interface AppPreferences { | ||
interface Preferences { | ||
|
||
val stringPreference: StateFlow<String> | ||
val booleanPreference: StateFlow<Boolean> | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,18 +2,11 @@ | |
* Copyright (c) 2024. Ryan Wong ([email protected]) | ||
*/ | ||
|
||
package com.rwmobi.fuseduserpreferences.data.datasources.prefdatastore | ||
package com.rwmobi.fuseduserpreferences.data.datasources.preferences | ||
|
||
import androidx.datastore.core.DataStore | ||
import androidx.datastore.preferences.core.Preferences | ||
import androidx.datastore.preferences.core.booleanPreferencesKey | ||
import androidx.datastore.preferences.core.edit | ||
import androidx.datastore.preferences.core.intPreferencesKey | ||
import androidx.datastore.preferences.core.stringPreferencesKey | ||
import com.rwmobi.fuseduserpreferences.data.datasources.AppPreferences | ||
import com.rwmobi.fuseduserpreferences.data.datasources.PREF_KEY_BOOLEAN | ||
import com.rwmobi.fuseduserpreferences.data.datasources.PREF_KEY_INT | ||
import com.rwmobi.fuseduserpreferences.data.datasources.PREF_KEY_STRING | ||
import kotlinx.coroutines.CoroutineScope | ||
import kotlinx.coroutines.flow.MutableSharedFlow | ||
import kotlinx.coroutines.flow.MutableStateFlow | ||
|
@@ -22,13 +15,16 @@ import kotlinx.coroutines.flow.asStateFlow | |
import kotlinx.coroutines.flow.catch | ||
import kotlinx.coroutines.launch | ||
|
||
class PreferenceDataStoreWrapper( | ||
class PreferencesDataStoreWrapper( | ||
private val dataStore: DataStore<Preferences>, | ||
private val prefKeyString: Preferences.Key<String>, | ||
private val prefKeyBoolean: Preferences.Key<Boolean>, | ||
private val prefKeyInt: Preferences.Key<Int>, | ||
private val stringPreferenceDefault: String, | ||
private val booleanPreferenceDefault: Boolean, | ||
private val intPreferenceDefault: Int, | ||
externalCoroutineScope: CoroutineScope, | ||
private val stringPreferenceDefault: String = "", | ||
private val booleanPreferenceDefault: Boolean = false, | ||
private val intPreferenceDefault: Int = 0, | ||
) : AppPreferences { | ||
) : com.rwmobi.fuseduserpreferences.data.datasources.preferences.Preferences { | ||
private val _stringPreference = MutableStateFlow(stringPreferenceDefault) | ||
override val stringPreference = _stringPreference.asStateFlow() | ||
|
||
|
@@ -41,29 +37,23 @@ class PreferenceDataStoreWrapper( | |
private val _preferenceErrors = MutableSharedFlow<Throwable>() | ||
override val preferenceErrors = _preferenceErrors.asSharedFlow() | ||
|
||
companion object { | ||
val DATASTORE_PREF_KEY_STRING = stringPreferencesKey(PREF_KEY_STRING) | ||
val DATASTORE_PREF_KEY_BOOLEAN = booleanPreferencesKey(PREF_KEY_BOOLEAN) | ||
val DATASTORE_PREF_KEY_INT = intPreferencesKey(PREF_KEY_INT) | ||
} | ||
|
||
init { | ||
externalCoroutineScope.launch { | ||
dataStore.data.catch { exception -> | ||
_preferenceErrors.emit(exception) | ||
} | ||
.collect { prefs -> | ||
_stringPreference.value = prefs[DATASTORE_PREF_KEY_STRING] ?: stringPreferenceDefault | ||
_booleanPreference.value = prefs[DATASTORE_PREF_KEY_BOOLEAN] ?: booleanPreferenceDefault | ||
_intPreference.value = prefs[DATASTORE_PREF_KEY_INT] ?: intPreferenceDefault | ||
_stringPreference.value = prefs[prefKeyString] ?: stringPreferenceDefault | ||
_booleanPreference.value = prefs[prefKeyBoolean] ?: booleanPreferenceDefault | ||
_intPreference.value = prefs[prefKeyInt] ?: intPreferenceDefault | ||
} | ||
} | ||
} | ||
|
||
override suspend fun updateStringPreference(newValue: String) { | ||
try { | ||
dataStore.edit { mutablePreferences -> | ||
mutablePreferences[DATASTORE_PREF_KEY_STRING] = newValue | ||
mutablePreferences[prefKeyString] = newValue | ||
} | ||
} catch (e: Throwable) { | ||
_preferenceErrors.emit(e) | ||
|
@@ -73,7 +63,7 @@ class PreferenceDataStoreWrapper( | |
override suspend fun updateBooleanPreference(newValue: Boolean) { | ||
try { | ||
dataStore.edit { mutablePreferences -> | ||
mutablePreferences[DATASTORE_PREF_KEY_BOOLEAN] = newValue | ||
mutablePreferences[prefKeyBoolean] = newValue | ||
} | ||
} catch (e: Throwable) { | ||
_preferenceErrors.emit(e) | ||
|
@@ -83,7 +73,7 @@ class PreferenceDataStoreWrapper( | |
override suspend fun updateIntPreference(newValue: Int) { | ||
try { | ||
dataStore.edit { mutablePreferences -> | ||
mutablePreferences[DATASTORE_PREF_KEY_INT] = newValue | ||
mutablePreferences[prefKeyInt] = newValue | ||
} | ||
} catch (e: Throwable) { | ||
_preferenceErrors.emit(e) | ||
|
Oops, something went wrong.