Skip to content

Commit

Permalink
Added StateFlow.onStateSubscription and use dispatchers.main for bett…
Browse files Browse the repository at this point in the history
…er iOS interop
  • Loading branch information
wkornewald committed Feb 3, 2024
1 parent cfbd339 commit 19510f3
Show file tree
Hide file tree
Showing 16 changed files with 104 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Install JDK
uses: actions/setup-java@v2
with:
java-version: "11"
java-version: "17"
distribution: "temurin"
cache: "gradle"
check-latest: true
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 5.7.0

* Upgraded to Kotlin 1.9.22.
* Switched `derived` and `AutoRunner` default dispatcher to `dispatcher.main`, to prevent threading errors on iOS by default.
* Added `MutableStateFlow<T>.onStateSubscription {}` and `StateFlow<T>.onStateSubscription {}` which behave like `onSubscription` but return a `MutableStateFlow`/`StateFlow`.

## 5.6.0

* Upgraded to kotlinx.coroutines 1.7.3.
Expand Down
24 changes: 13 additions & 11 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform

buildscript {
ext.kotlin_version = '1.8.21'
ext.kotlin_version = '1.9.22'
}

plugins {
id 'com.android.application' version '7.4.2' apply false
id 'com.android.application' version '8.2.0' apply false
id 'org.jetbrains.kotlin.android' version "$kotlin_version" apply false
id "org.jetbrains.dokka" version "1.8.20"
id "org.jetbrains.dokka" version "1.9.10"
id 'pl.allegro.tech.build.axion-release' version '1.15.3'
id 'com.github.ben-manes.versions' version '0.47.0'
id "io.github.gradle-nexus.publish-plugin" version "1.3.0"
Expand Down Expand Up @@ -80,7 +80,7 @@ subprojects {
}

if (isAndroidProject) {
android {
androidTarget {
publishLibraryVariants("release")
}

Expand All @@ -107,17 +107,22 @@ subprojects {

// wasm32()

applyDefaultHierarchyTemplate()

if (isIosProject) {
ios()
iosArm64()
iosX64()
iosSimulatorArm64()
testAll.dependsOn "iosSimulatorArm64Test"
testAll.dependsOn "iosX64Test"

tvos()
tvosArm64()
tvosX64()
tvosSimulatorArm64()

watchos()
watchosX86()
watchosArm32()
watchosArm64()
watchosX64()
watchosSimulatorArm64()

sourceSets {
Expand Down Expand Up @@ -146,8 +151,6 @@ subprojects {
watchosArm32Test { dependsOn(watchosTest) }
watchosX64Main { dependsOn(watchosMain) }
watchosX64Test { dependsOn(watchosTest) }
watchosX86Main { dependsOn(watchosMain) }
watchosX86Test { dependsOn(watchosTest) }
watchosSimulatorArm64Main { dependsOn(watchosMain) }
watchosSimulatorArm64Test { dependsOn(watchosTest) }
}
Expand Down Expand Up @@ -197,7 +200,6 @@ subprojects {
if (isAndroidProject) {
androidLibrary(
minVersion: isComposeProject ? 21 : 19,
kotlinCompilerArgs: kotlinCompilerArgs,
)

android {
Expand Down
6 changes: 3 additions & 3 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ dependencies {
androidTestDependency "androidx.fragment:fragment-testing:$fragmentVersion"
androidTestDependency "androidx.test:core-ktx:1.5.0"
androidTestDependency "androidx.test.ext:junit-ktx:1.1.5"
androidTestDependency "org.robolectric:robolectric:4.10.3"
androidTestDependency "org.robolectric:robolectric:4.11.1"
}

composeVersion = "1.4.3"
composeCompilerVersion = "1.4.7"
composeVersion = "1.6.0"
composeCompilerVersion = "1.5.8"
jetpackCompose = {
android {
buildFeatures {
Expand Down
3 changes: 2 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ kotlin.mpp.androidSourceSetLayoutVersion=2
# https://youtrack.jetbrains.com/issue/KT-58818
#kotlin.mpp.enableCInteropCommonization=true

kotlin.native.binary.freezing=disabled
kotlin.native.binary.objcExportSuspendFunctionLaunchThreadRestriction=none

org.gradle.caching=true
kotlin.compiler.preciseCompilationResultsBackup=true

android.nonTransitiveRClass=true
14 changes: 2 additions & 12 deletions gradle/common/android-common.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ class Config {
Integer minVersion = null
Integer targetVersion = null
Integer compileSdkVersion = null
Boolean kotlinExplicitApiMode = false
List<String> kotlinCompilerArgs = []
}

def isMultiplatform = ["androidMain", "jvmMain", "jvmCommonMain", "commonMain"].any { project.file("src/$it").exists() }
Expand All @@ -17,9 +15,9 @@ ext.androidCommon = { args = [:] ->
Config config = new Config(args)

int minVersion = config.minVersion ?: 19
int targetVersion = config.targetVersion ?: 33
int targetVersion = config.targetVersion ?: 34
int compileVersion = config.compileSdkVersion ?: targetVersion
def javaVersion = JavaVersion.VERSION_11
def javaVersion = JavaVersion.VERSION_17

kotlinCommon()

Expand All @@ -32,14 +30,6 @@ ext.androidCommon = { args = [:] ->
targetCompatibility javaVersion
}

kotlinOptions {
jvmTarget = javaVersion.toString()
freeCompilerArgs = getCompilerArgs(
kotlinExplicitApiMode: config.kotlinExplicitApiMode,
kotlinCompilerArgs: config.kotlinCompilerArgs,
)
}

namespace = "$group.${name.replace('-', '.')}"

defaultConfig {
Expand Down
4 changes: 0 additions & 4 deletions gradle/common/android-library.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ class Config {
Integer minVersion = null
Integer targetVersion = null
Integer compileSdkVersion = null
Boolean kotlinExplicitApiMode = true
List<String> kotlinCompilerArgs = []
}

apply plugin: 'com.android.library'
Expand All @@ -16,8 +14,6 @@ ext.androidLibrary = { args = [:] ->
minVersion: config.minVersion,
targetVersion: config.targetVersion,
compileSdkVersion: config.compileSdkVersion,
kotlinExplicitApiMode: config.kotlinExplicitApiMode,
kotlinCompilerArgs: config.kotlinCompilerArgs,
)

android {
Expand Down
1 change: 1 addition & 0 deletions gradle/common/kotlin-common.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ext.getCompilerArgs = { args = [:] ->
CompilerArgsConfig config = new CompilerArgsConfig(args)
def compilerArgs = [
"-opt-in=kotlin.RequiresOptIn",
"-Xexpect-actual-classes",
] + config.kotlinCompilerArgs
if (config.kotlinExplicitApiMode) {
compilerArgs.add("-Xexplicit-api=strict")
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ public fun CoroutineLauncher.autoRun(
* @param onChange Gets called when the observables change. If you provide a handler you have to
* manually call [run].
* @param flowTransformer How changes should be executed/collected. Defaults to [conflatedWorker].
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.default`.
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.main`.
* @param withLoading Tracks loading state for the (re-)computation. Defaults to [CoroutineLauncher.loading].
* @param observer The callback which is used to track the observables.
*/
public fun CoroutineLauncher.coAutoRun(
onChange: CoAutoRunOnChangeCallback<Unit>? = null,
flowTransformer: AutoRunFlowTransformer = { conflatedWorker(transform = it) },
dispatcher: CoroutineDispatcher = dispatchers.default,
dispatcher: CoroutineDispatcher = dispatchers.main,
withLoading: MutableValueFlow<Int>? = loading,
observer: CoAutoRunCallback<Unit>,
): CoAutoRunner<Unit> =
Expand Down Expand Up @@ -103,15 +103,15 @@ public fun CoroutineScope.autoRun(
* @param onChange Gets called when the observables change. If you provide a handler you have to
* manually call [run].
* @param flowTransformer How changes should be executed/collected. Defaults to [conflatedWorker].
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.default`.
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.main`.
* @param withLoading Tracks loading state for the (re-)computation. Defaults to `launcher.loading`.
* @param observer The callback which is used to track the observables.
*/
public fun CoroutineScope.coAutoRun(
launcher: CoroutineLauncher = SimpleCoroutineLauncher(this),
onChange: CoAutoRunOnChangeCallback<Unit>? = null,
flowTransformer: AutoRunFlowTransformer = { conflatedWorker(transform = it) },
dispatcher: CoroutineDispatcher = dispatchers.default,
dispatcher: CoroutineDispatcher = dispatchers.main,
withLoading: MutableValueFlow<Int>? = launcher.loading,
observer: CoAutoRunCallback<Unit>,
): CoAutoRunner<Unit> =
Expand Down Expand Up @@ -268,7 +268,7 @@ public class AutoRunner<T>(
* @param onChange Gets called when the observables change. Your onChange handler has to
* manually call [run] at any point (e.g. asynchronously) to change the tracked observables.
* @param flowTransformer How changes should be executed/collected. Defaults to [conflatedWorker].
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.default`.
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.main`.
* @param withLoading Tracks loading state for the (re-)computation. Defaults to [CoroutineLauncher.loading].
* @param immediate Whether to start tracking in a background coroutine immediately.
* @param observer The callback which is used to track the observables.
Expand All @@ -277,7 +277,7 @@ public class CoAutoRunner<T>(
launcher: CoroutineLauncher,
onChange: CoAutoRunOnChangeCallback<T>? = null,
flowTransformer: AutoRunFlowTransformer = { conflatedWorker(transform = it) },
private val dispatcher: CoroutineDispatcher = dispatchers.default,
private val dispatcher: CoroutineDispatcher = dispatchers.main,
override val withLoading: MutableValueFlow<Int>? = launcher.loading,
immediate: Boolean = false,
private val observer: CoAutoRunCallback<T>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,14 @@ public fun <T> CoroutineScope.derived(
*
* @param initial The initial value (until the first computation finishes).
* @param flowTransformer How changes should be executed/collected. Defaults to [conflatedWorker].
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.default`.
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.main`.
* @param withLoading Tracks loading state for the (re-)computation. Defaults to `null`.
* @param observer The callback which is used to track the observables.
*/
public fun <T> derivedWhileSubscribed(
initial: T,
flowTransformer: AutoRunFlowTransformer = { conflatedWorker(transform = it) },
dispatcher: CoroutineDispatcher = dispatchers.default,
dispatcher: CoroutineDispatcher = dispatchers.main,
withLoading: MutableValueFlow<Int>? = null,
observer: CoAutoRunCallback<T>,
): StateFlow<T> {
Expand All @@ -161,15 +161,15 @@ public fun <T> derivedWhileSubscribed(
* @param started When the value should be updated. Pass [SharingStarted.WhileSubscribed] to compute only on demand.
* Defaults to [SharingStarted.Eagerly].
* @param flowTransformer How changes should be executed/collected. Defaults to [conflatedWorker].
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.default`.
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.main`.
* @param withLoading Tracks loading state for the (re-)computation. Defaults to [CoroutineLauncher.loading].
* @param observer The callback which is used to track the observables.
*/
public fun <T> CoroutineLauncher.derived(
initial: T,
started: SharingStarted = SharingStarted.Eagerly,
flowTransformer: AutoRunFlowTransformer = { conflatedWorker(transform = it) },
dispatcher: CoroutineDispatcher = dispatchers.default,
dispatcher: CoroutineDispatcher = dispatchers.main,
withLoading: MutableValueFlow<Int>? = loading,
observer: CoAutoRunCallback<T>,
): StateFlow<T> {
Expand Down Expand Up @@ -197,7 +197,7 @@ public fun <T> CoroutineLauncher.derived(
* Defaults to [SharingStarted.Eagerly].
* @param launcher The [CoroutineLauncher] to use.
* @param flowTransformer How changes should be executed/collected. Defaults to [conflatedWorker].
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.default`.
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.main`.
* @param withLoading Tracks loading state for the (re-)computation. Defaults to `null`.
* @param observer The callback which is used to track the observables.
*/
Expand All @@ -206,7 +206,7 @@ public fun <T> CoroutineScope.derived(
started: SharingStarted = SharingStarted.Eagerly,
launcher: CoroutineLauncher = SimpleCoroutineLauncher(this),
flowTransformer: AutoRunFlowTransformer = { conflatedWorker(transform = it) },
dispatcher: CoroutineDispatcher = dispatchers.default,
dispatcher: CoroutineDispatcher = dispatchers.main,
withLoading: MutableValueFlow<Int>? = null,
observer: CoAutoRunCallback<T>,
): StateFlow<T> =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.ensody.reactivestate

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

/**
* A [StateFlow] which can retrieve a new value e.g. from a backend via [refresh].
*/
@ExperimentalReactiveStateApi
public interface RefreshableStateFlow<T> : StateFlow<T> {
/**
* Refreshes the [value].
*
* @param force Can enforce refreshing the value and bypassing any caching mechanism.
*/
public suspend fun refresh(force: Boolean = false)
}

@ExperimentalReactiveStateApi
public fun <T> MutableStateFlow<T>.withRefresh(
block: suspend MutableStateFlow<T>.(force: Boolean) -> Unit,
): RefreshableStateFlow<T> =
DefaultRefreshableStateFlow(this) { block(it) }

@ExperimentalReactiveStateApi
public fun <T> StateFlow<T>.withRefresh(
block: suspend StateFlow<T>.(force: Boolean) -> Unit,
): RefreshableStateFlow<T> =
DefaultRefreshableStateFlow(this) { block(it) }

private class DefaultRefreshableStateFlow<T>(
private val delegate: StateFlow<T>,
private val refresher: suspend (force: Boolean) -> Unit,
) : RefreshableStateFlow<T>, StateFlow<T> by delegate {
override suspend fun refresh(force: Boolean) {
refresher(force)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.ensody.reactivestate

import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.onSubscription

/**
* Replaces the [MutableStateFlow.value] with [block]'s return value.
Expand All @@ -24,3 +27,19 @@ public fun <T> MutableStateFlow<T>.replace(block: T.() -> T): T {
value = value.block()
return previous
}

/**
* Similar to [MutableStateFlow.onSubscription] but returns a [MutableStateFlow].
*/
public fun <T> MutableStateFlow<T>.onStateSubscription(
block: suspend FlowCollector<T>.() -> Unit,
): MutableStateFlow<T> =
onSubscription(block).stateOnDemand { value }.toMutable { this@onStateSubscription.value = it }

/**
* Similar to [StateFlow.onSubscription] but returns a [StateFlow].
*/
public fun <T> StateFlow<T>.onStateSubscription(
block: suspend FlowCollector<T>.() -> Unit,
): StateFlow<T> =
onSubscription(block).stateOnDemand { value }
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public fun LifecycleOwner.autoRun(
* @param [onChange] Gets called when the observables change. If you provide a handler you have to
* manually call [run].
* @param flowTransformer How changes should be executed/collected. Defaults to [conflatedWorker].
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.default`.
* @param dispatcher The [CoroutineDispatcher] to use. Defaults to `dispatchers.main`.
* @param withLoading Tracks loading state for the (re-)computation. Defaults to [CoroutineLauncher.loading] if
* this is a [CoroutineLauncher] or `null` otherwise.
* @param [observer] The callback which is used to track the observables.
Expand All @@ -131,7 +131,7 @@ public fun LifecycleOwner.coAutoRun(
launcher: CoroutineLauncher = if (this is CoroutineLauncher) this else LifecycleCoroutineLauncher(this),
onChange: CoAutoRunOnChangeCallback<Unit>? = null,
flowTransformer: AutoRunFlowTransformer = { conflatedWorker(transform = it) },
dispatcher: CoroutineDispatcher = dispatchers.default,
dispatcher: CoroutineDispatcher = dispatchers.main,
withLoading: MutableValueFlow<Int>? = if (this is CoroutineLauncher) launcher.loading else null,
observer: AutoRunCallback<Unit>,
): CoAutoRunner<Unit> {
Expand Down
Loading

0 comments on commit 19510f3

Please sign in to comment.