Skip to content

Commit

Permalink
Updated Extensions package (#2)
Browse files Browse the repository at this point in the history
Moved the `DataStore<Preference>` extensions to a `preference` package and added generic `DataStore` extensions
  • Loading branch information
brianwernick authored Jun 24, 2023
1 parent 2b31145 commit 13674a9
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import com.devbrackets.android.datastore.converter.core.EnumValueConverter
import com.devbrackets.android.datastore.converter.core.StringBytesValueConverter
import com.devbrackets.android.datastore.converter.crypto.EncryptedValueConverter
import com.devbrackets.android.datastore.converter.then
import com.devbrackets.android.datastore.delegate.value
import com.devbrackets.android.datastore.flow
import com.devbrackets.android.datastore.preference.delegate.value
import com.devbrackets.android.datastore.preference.flow
import com.devbrackets.android.datastoredemo.data.model.Month
import com.devbrackets.android.datastoredemo.data.token.DemoAccountTokenValueConverter

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.devbrackets.android.datastore

import androidx.annotation.WorkerThread
import androidx.datastore.core.DataStore
import com.devbrackets.android.datastore.converter.ValueConverter
import com.devbrackets.android.datastore.converter.core.NoOpValueConverter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty


/**
* A Kotlin delegate to read and write values in a [DataStore].
* For example
* ```kotlin
* var theme by dataStore.value("uiTheme", Theme.FOLLOW_SYSTEM)
* ```
*
* **NOTE:**
* Reading a writing [DataStore] values can be slower than expected and it is recommended
* to perform reads/writes (get/set) on threads other than Main/UI.
*
* @param valueGetter The function that handles retrieving the value from the [DataStore] of [DS]
* @param valueSetter The function that handles setting the value in the [DataStore] of [DS]
*/
@WorkerThread
fun <T, DS> DataStore<DS>.value(
valueSetter: suspend (store: DS, value: T) -> DS,
valueGetter: suspend (store: DS) -> T
): ReadWriteProperty<Any, T> {
return value(
valueSetter = valueSetter,
valueGetter = valueGetter,
converter = NoOpValueConverter()
)
}

/**
* A Kotlin delegate to read and write values in a [DataStore].
* For example
* ```kotlin
* var theme by dataStore.value("uiTheme", Theme.FOLLOW_SYSTEM)
* ```
*
* **NOTE:**
* Reading a writing [DataStore] values can be slower than expected and it is recommended
* to perform reads/writes (get/set) on threads other than Main/UI.
*
* @param valueGetter The function that handles retrieving the value from the [DataStore] of [DS]
* @param valueSetter The function that handles setting the value in the [DataStore] of [DS]
* @param converter The [ValueConverter] to convert between the use type [T] (e.g. an Enum)
* and a format that the can be stored in preferences [S]. Supported preference types
* are `Int`, `Double`, `String`, `Boolean`, `Float`, and `Long`.
*/
@WorkerThread
fun <T, S, DS> DataStore<DS>.value(
valueSetter: suspend (store: DS, value: S) -> DS,
valueGetter: suspend (store: DS) -> S,
converter: ValueConverter<T, S>,
): ReadWriteProperty<Any, T> {
return object : ReadWriteProperty<Any, T> {
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
runBlocking {
this@value.updateData { store ->
valueSetter(store, converter.toConverted(value))
}
}
}

override fun getValue(thisRef: Any, property: KProperty<*>): T {
return runBlocking {
data.first().let {
converter.toOriginal(valueGetter(it))
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.devbrackets.android.datastore
package com.devbrackets.android.datastore.preference

import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import com.devbrackets.android.datastore.converter.ValueConverter
import com.devbrackets.android.datastore.converter.core.NoOpValueConverter
import com.devbrackets.android.datastore.delegate.getPreferencesKey
import com.devbrackets.android.datastore.preference.delegate.getPreferencesKey
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
Expand Down Expand Up @@ -33,8 +33,8 @@ inline fun <reified T> DataStore<Preferences>.sharedFlow(
started: SharingStarted = SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
replay: Int = 0
): SharedFlow<T> {
return this.sharedFlow(
key = key,
return sharedFlow(
key = getPreferencesKey(key),
defaultValue = defaultValue,
converter = NoOpValueConverter(),
scope = scope,
Expand All @@ -60,13 +60,48 @@ inline fun <reified T> DataStore<Preferences>.sharedFlow(
* @param started The strategy that controls when sharing is started and stopped
* @param replay The number of values replayed to new subscribers (cannot be negative, defaults to zero).
*/
inline fun <reified T, reified S> DataStore<Preferences>.sharedFlow(
inline fun <T, reified S> DataStore<Preferences>.sharedFlow(
key: String,
defaultValue: T,
converter: ValueConverter<T, S>,
scope: CoroutineScope,
started: SharingStarted = SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
replay: Int = 0
): SharedFlow<T> {
return sharedFlow(
key = getPreferencesKey(key),
defaultValue = defaultValue,
converter = converter,
scope = scope,
started = started,
replay = replay
)
}

/**
* A Kotlin delegate to retrieve a [SharedFlow] for values in a [Preferences] [DataStore].
* For example
* ```kotlin
* val themeFlow = dataStore.sharedFlow("uiTheme", Theme.FOLLOW_SYSTEM, EnumValueConverter(Theme::class), viewModelScope)
* ```
*
* @param key The unique id used to store and retrieve the preference, typically this is a
* descriptive id such as "uiTheme"
* @param defaultValue The value to return when [key] isn't contained by the [DataStore]
* @param converter The [ValueConverter] to convert between the use type (e.g. an Enum)
* and a format that the can be stored in preferences. Supported preference types
* are `Int`, `Double`, `String`, `Boolean`, `Float`, and `Long`.
* @param scope The [CoroutineScope] to use when constructing the [SharedFlow]
* @param started The strategy that controls when sharing is started and stopped
* @param replay The number of values replayed to new subscribers (cannot be negative, defaults to zero).
*/
fun <T, S> DataStore<Preferences>.sharedFlow(
key: Preferences.Key<S>,
defaultValue: T,
converter: ValueConverter<T, S>,
scope: CoroutineScope,
started: SharingStarted = SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
replay: Int = 0
): SharedFlow<T> {
return this.flow(
key = key,
Expand Down Expand Up @@ -115,16 +150,39 @@ inline fun <reified T> DataStore<Preferences>.flow(
* and a format that the can be stored in preferences. Supported preference types
* are `Int`, `Double`, `String`, `Boolean`, `Float`, and `Long`.
*/
inline fun <reified T, reified S> DataStore<Preferences>.flow(
inline fun <T, reified S> DataStore<Preferences>.flow(
key: String,
defaultValue: T,
converter: ValueConverter<T, S>
): Flow<T> {
val prefKey: Preferences.Key<S> = getPreferencesKey(key)
return flow(
key = getPreferencesKey(key),
defaultValue = defaultValue,
converter = converter
)
}

/**
* A Kotlin delegate to retrieve a [Flow] for values in a [Preferences] [DataStore].
* For example
* ```kotlin
* val themeFlow = dataStore.flow("uiTheme", Theme.FOLLOW_SYSTEM, EnumValueConverter(Theme::class))
* ```
*
* @param key The [Preferences.Key] used to store and retrieve the preference
* @param defaultValue The value to return when [key] isn't contained by the [DataStore]
* @param converter The [ValueConverter] to convert between the use type (e.g. an Enum)
* and a format that the can be stored in preferences. Supported preference types
* are `Int`, `Double`, `String`, `Boolean`, `Float`, and `Long`.
*/
fun <T, S> DataStore<Preferences>.flow(
key: Preferences.Key<S>,
defaultValue: T,
converter: ValueConverter<T, S>
): Flow<T> {
return data.map { prefs ->
prefs.getOrDefault(
key = prefKey,
key = key,
defaultValue = defaultValue,
converter = converter
)
Expand All @@ -139,7 +197,7 @@ inline fun <reified T, reified S> DataStore<Preferences>.flow(
* @param key The [Preferences.Key] used to retrieve the [Preferences] value
* @param defaultValue The value to return when [key] isn't defined in [Preferences]
*/
inline fun <reified T> Preferences.getOrDefault(
fun <T> Preferences.getOrDefault(
key: Preferences.Key<T>,
defaultValue: T
): T {
Expand All @@ -160,7 +218,8 @@ inline fun <reified T> Preferences.getOrDefault(
* @param converter The [ValueConverter] used to convert between the stored type [S]
* and the expected read/write type [T]
*/
inline fun <reified T, S> Preferences.getOrDefault(
@Suppress("UNCHECKED_CAST")
fun <T, S> Preferences.getOrDefault(
key: Preferences.Key<S>,
defaultValue: T,
converter: ValueConverter<T, S>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
package com.devbrackets.android.datastore.delegate
package com.devbrackets.android.datastore.preference.delegate

import androidx.annotation.WorkerThread
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import com.devbrackets.android.datastore.converter.ValueConverter
import com.devbrackets.android.datastore.converter.core.NoOpValueConverter
import com.devbrackets.android.datastore.getOrDefault
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import com.devbrackets.android.datastore.preference.getOrDefault
import com.devbrackets.android.datastore.value
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

/**
* A Kotlin delegate to read and write values in a [Preferences] [DataStore].
Expand All @@ -27,12 +25,12 @@ import kotlin.reflect.KProperty
* @param defaultValue The value to return when [key] isn't contained by the [DataStore]
*/
@WorkerThread
inline fun <reified T : Any?> DataStore<Preferences>.value(
inline fun <reified T> DataStore<Preferences>.value(
key: String,
defaultValue: T
): ReadWriteProperty<Any, T> {
return value(
key = key,
key = getPreferencesKey(key),
defaultValue = defaultValue,
converter = NoOpValueConverter()
)
Expand All @@ -57,38 +55,57 @@ inline fun <reified T : Any?> DataStore<Preferences>.value(
* are `Int`, `Double`, `String`, `Boolean`, `Float`, and `Long`.
*/
@WorkerThread
inline fun <reified T, reified S> DataStore<Preferences>.value(
inline fun <T, reified S> DataStore<Preferences>.value(
key: String,
defaultValue: T,
converter: ValueConverter<T, S>
): ReadWriteProperty<Any, T> {
return object : ReadWriteProperty<Any, T> {
private val prefKey: Preferences.Key<S> = getPreferencesKey(key)

override fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
runBlocking {
set(value)
}
}
return value(
key = getPreferencesKey(key),
defaultValue = defaultValue,
converter = converter
)
}

override fun getValue(thisRef: Any, property: KProperty<*>): T {
return runBlocking {
data.first().getOrDefault(
key = prefKey,
defaultValue = defaultValue,
converter = converter
)
}
}
/**
* A Kotlin delegate to read and write values in a [Preferences] [DataStore] with value transformations.
* For example
* ```kotlin
* var theme by dataStore.value("uiTheme", Theme.FOLLOW_SYSTEM, EnumValueConverter(Theme::class))
* ```
*
* **NOTE:**
* Reading a writing [DataStore] values can potentially be slower than expected and it is recommended
* to perform reads/writes (get/set) on threads other than Main/UI.
*
* @param key The [Preferences.Key] used to store and retrieve the preference
* @param defaultValue The value to return when [key] isn't contained by the [DataStore]
* @param converter The [ValueConverter] to convert between the use type (e.g. an Enum)
* and a format that the can be stored in preferences. Supported preference types
* are `Int`, `Double`, `String`, `Boolean`, `Float`, and `Long`.
*/
@WorkerThread
fun <T, S> DataStore<Preferences>.value(
key: Preferences.Key<S>,
defaultValue: T,
converter: ValueConverter<T, S>
): ReadWriteProperty<Any, T> {
val defaultStoreValue = converter.toConverted(defaultValue)

private suspend fun set(value: T): T {
this@value.edit { prefs ->
prefs[prefKey] = converter.toConverted(value)
return value(
valueSetter = { store, value ->
store.toMutablePreferences().apply {
this[key] = value
}

return value
}
}
},
valueGetter = { store ->
store.getOrDefault(
key = key,
defaultValue = defaultStoreValue
)
},
converter = converter
)
}

/**
Expand Down

0 comments on commit 13674a9

Please sign in to comment.