Typesafe-Datastore is a lightweight abstraction layer on top of SharedPreferences DataStore that provides type-safety without dealing with Proto-DataStore.
Why Typesafe-Datastore?
- Proto-DataStore is, in my personal opinion, not-so-beginner-friendly, requires plugins and over-engineered for the use cases that required
SharedPreferences
. Although Proto-DataStore has its own advantages like better performance than normal serialization methods, it is usually recommmended to use Room if performance comes into play. - If one is already using Preferences DataStore but wants type-safety, migrating to Proto-DataStore would be a lot of pain in a live app since Proto-DataStore uses Protocol Buffers under the hood. Whereas, this implementation, provides flexibility and easy of migration on a live app.
- Apart from type-safety, various migration functions have been included to migrate data inside DataStore in a type-safe way.
- Testing is really easy.
Add the JitPack repository to your project's build.gradle file
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
Add the dependency in your app's build.gradle file
dependencies {
// Type-safe datastore
implementation("com.github.07jasjeet.typesafe-datastore:typesafe-datastore:1.0.1")
// Alternatively - Gson backed type-safe datastore
implementation("com.github.07jasjeet.typesafe-datastore:typesafe-datastore-gson:1.0.1")
// Testing
testImplementation("com.github.07jasjeet.typesafe-datastore:typesafe-datastore-test:1.0.1")
}
- Prerequisite: Latest version of the Android Studio and SDKs on your pc.
- Clone this repository.
- Use the
gradlew build
command to build the project directly or use the IDE to run the project to your phone or the emulator.
AutoTypedDataStore
has various preference creation functions that are backed by TypeSafeDataStore
(see below) for type-safety and Gson
serializer so that you don't have to write your own
serializers everytime.
To get Started, import the library by adding the following dependency.
implementation("com.github.07jasjeet.typesafe-datastore:typesafe-datastore-gson:$version")
And now in your code, just extend your pre-existing preferences class (if you don't have one, I recommend creating one) with AutoTypedDataStore.
// Your DataStore
val Context.dataStore: DataStore<Preferences> by preferencesDataStore("prefs")
class MyAutoTypedPreferences(context: Context): AutoTypedDataStore(context.dataStore) {
// ...
}
And now add preferences as follows:
class MyAutoTypedPreferences(context: Context): AutoTypedDataStore(context.dataStore) {
companion object {
val key = stringPreferencesKey("key")
}
val listPref: ComplexPreference<List<String>>
get() = listPreference(key)
// or
val mapPref: ComplexPreference<Map<String, List<String>>>
get() = mapPreference(key)
// ... and many other pre-defined preferences
}
You can also add custom preferences without any boilerplate as follows:
val key = stringPreferencesKey("key")
val customPref: ComplexPreference<SomeClass>
get() = customPreference(key, defaultValue)
To use these preferences, simply do as follows:
// Acquire object by injecting or create one.
val preferences = MyAutoTypedPreferences(context)
preferences.listPref.get()
preferences.listPref.set(...)
preferences.listPref.getFlow()
preferences.listPref.getAndUpdate{ ... }
If you want to use your own serialization library, you can use TypeSafeDataStore
and create preferences. To do that, import
this library by adding the following dependency:
implementation("com.github.07jasjeet.typesafe-datastore:typesafe-datastore:$version")
Similarly as before, extend your preferences class as shown.
class MyPreferences(context: Context): TypeSafeDataStore(context.dataStore)
companion object {
val key = booleanPreferencesKey("my-key")
}
val preference: PrimitivePreference<Boolean>
get() = createPrimitivePreference(key, false)
// or
val complexPref: ComplexPreference<List<List<String>>>
get() = createComplexPreference(key, serializer)
In-order to create Custom [DataStorePreference] you need to do some steps. Firstly, create a new interface as follows with your newer implementation in it.
interface CustomPreference<T, R>: Preference<T, R> {
// create new Functions such as getSorted() etc...
}
Then, extend this class as follows:
abstract class CustomDataStore(dataStore: DataStore<Preferences>): TypeSafeDataStore(dataStore) {
/* Required */
abstract inner class CustomDataStorePreference<T, R>(
key: Preferences.Key<R>,
serializer: DataStoreSerializer<T, R>
): CustomPreference<T>, DataStorePreference<T, R>(key, serializer) {
// ... override functions.
}
}
Why go all through this? Testability.
Now to use the new preference, do as follows as you do with normal preferences.
private val Context.dataStore: DataStore<Preferences> by ...
class UserPreferences(context: Context): CustomDataStore(context.dataStore) {
val userPref: CustomPreference<Map<String>, Int>
get() = createCustomPreference(key, serializer)
val anotherPref: CustomPreference<String, String>
get() = createCustomPreference(anotherKey, anotherSerializer)
}
Jetpack DataStore currently has solution to migrate SharedPreference to Preferences DataStore, but there is no such shorthand solution for intra-DataStore migrations. Migrating a simple preference from one key to another can be done as follows:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "name",
produceMigrations = {
listOf(
IntraDataMigration(currentKey, newKey) { currentValue ->
// Run transformations
return newValue
}
)
}
)
And now for the best part, mocking! To get started, import this library by adding the following as a dependency.
testImplementation("com.github.07jasjeet.typesafe-datastore:typesafe-datastore-test:$version")
Using mockito-kotlin or any other mocking framework, in your test file, do this:
@RunWith(MockitoJUnitRunner::class)
class Test {
@Mock
lateinit var myPreferences: MyPreferences
fun test {
wheneverBlocking {
myPreferences.booleanPreference
}.doReturn(MockPrimitivePreference(true))
// Your values will be mocked!
appPreferences.booleanPreference.get()
appPreferences.booleanPreference.set()
appPreferences.booleanPreference.getFlow()
appPreferences.booleanPreference.getAndUpdate{ ... }
}
}