russhwolf / multiplatform-settings

A Kotlin Multiplatform library for saving simple key-value data
Apache License 2.0
1.69k stars 67 forks source link

feature request: coroutines: Add generic getFlow / getOrNullFlow #199

Closed iamcalledrob closed 2 months ago

iamcalledrob commented 3 months ago

In Operators.kt, there is a generic get/set which works for primitive types.

/**
 * Get the typed value stored at [key] if present, or return null if not. Throws [IllegalArgumentException] if [T] is
 * not one of `Int`, `Long`, `String`, `Float`, `Double`, or `Boolean`.
 */
public inline operator fun <reified T : Any> Settings.get(key: String): T? = when (T::class) {
    Int::class -> getIntOrNull(key) as T?
    Long::class -> getLongOrNull(key) as T?
    String::class -> getStringOrNull(key) as T?
    Float::class -> getFloatOrNull(key) as T?
    Double::class -> getDoubleOrNull(key) as T?
    Boolean::class -> getBooleanOrNull(key) as T?
    else -> throw IllegalArgumentException("Invalid type!")
}

It would be convenient to have the same functionality for the flow/coroutine getters too, so it can be kept in sync with the supported primitive getters/setters.

For my use-cases, I implemented it as follows:

@Suppress("UNCHECKED_CAST")
@OptIn(ExperimentalSettingsApi::class)
private inline fun <reified T> ObservableSettings.getOrNullFlow(key: String): Flow<T?> = when (T::class) {
    Int::class -> getIntOrNullFlow(key)
    Long::class -> getLongOrNullFlow(key)
    String::class -> getStringOrNullFlow(key)
    Float::class -> getFloatOrNullFlow(key)
    Double::class -> getDoubleOrNullFlow(key)
    Boolean::class -> getBooleanOrNullFlow(key)
    else -> throw IllegalArgumentException("Invalid type!")
} as Flow<T?>

inline fun <reified T> ObservableSettings.getFlow(key: String, defaultValue: T): Flow<T> =
    getOrNullFlow<T?>(key).map { it ?: defaultValue }

My underlying use-case is a getMutableStateFlow for a setting, so it can easily be observed/set from compose UI.

Implementation for the curious ```kotlin private inline fun ObservableSettings.getMutableStateFlow( key: String, defaultValue: T, coroutineScope: CoroutineScope, ): MutableStateFlow { val flow = getFlow(key, defaultValue) val initialValue = runBlocking { flow.first() } val stateFlow = MutableStateFlow(initialValue) coroutineScope.launch { flow.collectLatest { value -> stateFlow.value = value } } coroutineScope.launch { // drop(1) so the default value isn't reapplied to ObservableSettings stateFlow.drop(1).collectLatest { newValue -> this@getMutableStateFlow[key] = newValue } } return stateFlow } ```
russhwolf commented 2 months ago

I'm generally not a big fan of APIs with this shape, because it's easy to get errors if the type-parameter is inferred incorrectly. If you feel differently, it's easy enough to add the extension in your own code.

The reason the functions in Operators.kt exist is because when using operator syntax, there's not always a clean way to indicate the type without having a generic function like this. But that's a special case and I'd rather avoid adding other similar APIs.