multiplatform-settings icon indicating copy to clipboard operation
multiplatform-settings copied to clipboard

feature request: coroutines: Add generic getFlow / getOrNullFlow

Open iamcalledrob opened this issue 1 year ago • 0 comments

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
private inline fun <reified T> ObservableSettings.getMutableStateFlow(
    key: String,
    defaultValue: T,
    coroutineScope: CoroutineScope,
): MutableStateFlow<T> {
    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
}

iamcalledrob avatar Aug 09 '24 12:08 iamcalledrob