diff --git a/arrow-libs/optics/arrow-optics-compose/api/android/arrow-optics-compose.api b/arrow-libs/optics/arrow-optics-compose/api/android/arrow-optics-compose.api new file mode 100644 index 00000000000..31d871ddeda --- /dev/null +++ b/arrow-libs/optics/arrow-optics-compose/api/android/arrow-optics-compose.api @@ -0,0 +1,17 @@ +public final class arrow/optics/CopyKt { + public static final fun update (Landroidx/compose/runtime/MutableState;Lkotlin/jvm/functions/Function1;)V + public static final fun updateCopy (Landroidx/compose/runtime/MutableState;Lkotlin/jvm/functions/Function1;)V + public static final fun updateCopy (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlin/jvm/functions/Function1;)V +} + +public final class arrow/optics/FlowKt { + public static final fun optic (Lkotlinx/coroutines/flow/MutableStateFlow;Larrow/optics/PLens;)Lkotlinx/coroutines/flow/MutableStateFlow; + public static final fun optic (Lkotlinx/coroutines/flow/SharedFlow;Larrow/optics/Getter;)Lkotlinx/coroutines/flow/SharedFlow; + public static final fun optic (Lkotlinx/coroutines/flow/StateFlow;Larrow/optics/Getter;)Lkotlinx/coroutines/flow/StateFlow; +} + +public final class arrow/optics/StateKt { + public static final fun optic (Landroidx/compose/runtime/MutableState;Larrow/optics/PLens;)Landroidx/compose/runtime/MutableState; + public static final fun optic (Landroidx/compose/runtime/State;Larrow/optics/Getter;)Landroidx/compose/runtime/State; +} + diff --git a/arrow-libs/optics/arrow-optics-compose/api/jvm/arrow-optics-compose.api b/arrow-libs/optics/arrow-optics-compose/api/jvm/arrow-optics-compose.api new file mode 100644 index 00000000000..31d871ddeda --- /dev/null +++ b/arrow-libs/optics/arrow-optics-compose/api/jvm/arrow-optics-compose.api @@ -0,0 +1,17 @@ +public final class arrow/optics/CopyKt { + public static final fun update (Landroidx/compose/runtime/MutableState;Lkotlin/jvm/functions/Function1;)V + public static final fun updateCopy (Landroidx/compose/runtime/MutableState;Lkotlin/jvm/functions/Function1;)V + public static final fun updateCopy (Lkotlinx/coroutines/flow/MutableStateFlow;Lkotlin/jvm/functions/Function1;)V +} + +public final class arrow/optics/FlowKt { + public static final fun optic (Lkotlinx/coroutines/flow/MutableStateFlow;Larrow/optics/PLens;)Lkotlinx/coroutines/flow/MutableStateFlow; + public static final fun optic (Lkotlinx/coroutines/flow/SharedFlow;Larrow/optics/Getter;)Lkotlinx/coroutines/flow/SharedFlow; + public static final fun optic (Lkotlinx/coroutines/flow/StateFlow;Larrow/optics/Getter;)Lkotlinx/coroutines/flow/StateFlow; +} + +public final class arrow/optics/StateKt { + public static final fun optic (Landroidx/compose/runtime/MutableState;Larrow/optics/PLens;)Landroidx/compose/runtime/MutableState; + public static final fun optic (Landroidx/compose/runtime/State;Larrow/optics/Getter;)Landroidx/compose/runtime/State; +} + diff --git a/arrow-libs/optics/arrow-optics-compose/build.gradle.kts b/arrow-libs/optics/arrow-optics-compose/build.gradle.kts new file mode 100644 index 00000000000..2df6f0fbf09 --- /dev/null +++ b/arrow-libs/optics/arrow-optics-compose/build.gradle.kts @@ -0,0 +1,95 @@ +@file:Suppress("DSL_SCOPE_VIOLATION") + +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + + +repositories { + google() + mavenCentral() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") +} + +plugins { + id(libs.plugins.kotlin.multiplatform.get().pluginId) + // alias(libs.plugins.arrowGradleConfig.kotlin) + alias(libs.plugins.arrowGradleConfig.publish) + alias(libs.plugins.spotless) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.android.library) +} + +apply(from = property("ANIMALSNIFFER_MPP")) + +kotlin { + explicitApi() + + jvm { + jvmToolchain(8) + } + js(IR) { + browser() + nodejs() + } + androidTarget() + // Native: https://kotlinlang.org/docs/native-target-support.html + // -- Tier 1 -- + linuxX64() + macosX64() + macosArm64() + iosSimulatorArm64() + iosX64() + // -- Tier 2 -- + // linuxArm64() + watchosSimulatorArm64() + watchosX64() + watchosArm64() + tvosSimulatorArm64() + tvosX64() + tvosArm64() + iosArm64() + // -- Tier 3 -- + mingwX64() + + sourceSets { + commonMain { + dependencies { + api(projects.arrowOptics) + api(libs.coroutines.core) + api(compose.runtime) + implementation(libs.kotlin.stdlibCommon) + } + } + + commonTest { + dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotest.assertionsCore) + implementation(libs.kotest.property) + } + } + } +} + +tasks.withType().configureEach { + useJUnitPlatform() +} + +compose { + // override the choice of Compose if we use a Kotlin -dev version + val kotlinVersion = project.rootProject.properties["kotlin_version"] as? String + if (kotlinVersion != null && kotlinVersion.contains("-dev-")) { + kotlinCompilerPlugin.set(dependencies.compiler.forKotlin("2.0.0-Beta1")) + kotlinCompilerPluginArgs.add("suppressKotlinVersionCompatibilityCheck=$kotlinVersion") + } +} + +android { + namespace = "arrow.optics.compose" + compileSdk = libs.versions.android.compileSdk.get().toInt() +} + +tasks.named("jvmJar").configure { + manifest { + attributes["Automatic-Module-Name"] = "arrow.optics.compose" + } +} diff --git a/arrow-libs/optics/arrow-optics-compose/gradle.properties b/arrow-libs/optics/arrow-optics-compose/gradle.properties new file mode 100644 index 00000000000..d569b6effb4 --- /dev/null +++ b/arrow-libs/optics/arrow-optics-compose/gradle.properties @@ -0,0 +1,4 @@ +# Maven publishing configuration +pom.name=Arrow Optics for Compose +# Build configuration +kapt.incremental.apt=false \ No newline at end of file diff --git a/arrow-libs/optics/arrow-optics-compose/src/commonMain/kotlin/arrow/optics/Copy.kt b/arrow-libs/optics/arrow-optics-compose/src/commonMain/kotlin/arrow/optics/Copy.kt new file mode 100644 index 00000000000..4fa50e90cba --- /dev/null +++ b/arrow-libs/optics/arrow-optics-compose/src/commonMain/kotlin/arrow/optics/Copy.kt @@ -0,0 +1,32 @@ +package arrow.optics + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.snapshots.Snapshot +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update + +/** + * Modifies the value in this [MutableState] + * by applying the function [block] to the current value. + */ +public inline fun MutableState.update(crossinline block: (T) -> T) { + Snapshot.withMutableSnapshot { + value = block(value) + } +} + +/** + * Modifies the value in this [MutableState] + * by performing the operations in the [Copy] [block]. + */ +public fun MutableState.updateCopy(block: Copy.() -> Unit) { + update { it.copy(block) } +} + +/** + * Updates the value in this [MutableStateFlow] + * by performing the operations in the [Copy] [block]. + */ +public fun MutableStateFlow.updateCopy(block: Copy.() -> Unit) { + update { it.copy(block) } +} diff --git a/arrow-libs/optics/arrow-optics-compose/src/commonMain/kotlin/arrow/optics/Flow.kt b/arrow-libs/optics/arrow-optics-compose/src/commonMain/kotlin/arrow/optics/Flow.kt new file mode 100644 index 00000000000..30902538cca --- /dev/null +++ b/arrow-libs/optics/arrow-optics-compose/src/commonMain/kotlin/arrow/optics/Flow.kt @@ -0,0 +1,70 @@ +package arrow.optics + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow + +/** + * Exposes the values of [this] through the optic. + */ +public fun SharedFlow.optic(g: Getter): SharedFlow = object : SharedFlow { + override suspend fun collect(collector: FlowCollector): Nothing = + this@optic.collect { collector.emit(g.get(it)) } + + override val replayCache: List + get() = this@optic.replayCache.map { g.get(it) } +} + +/** + * Exposes the values of [this] through the optic. + */ +public fun StateFlow.optic(g: Getter): StateFlow = object : StateFlow { + override val value: A + get() = g.get(this@optic.value) + + override suspend fun collect(collector: FlowCollector): Nothing = + this@optic.collect { collector.emit(g.get(it)) } + + override val replayCache: List + get() = this@optic.replayCache.map { g.get(it) } +} + +/** + * Exposes the values of [this] through the optic. + * Any change made to [value] is reflected in the original [MutableStateFlow]. + */ +public fun MutableStateFlow.optic(lens: Lens): MutableStateFlow = object : MutableStateFlow { + override var value: A + get() = lens.get(this@optic.value) + set(newValue) { + this@optic.value = lens.set(this@optic.value, newValue) + } + + override suspend fun collect(collector: FlowCollector): Nothing = + this@optic.collect { collector.emit(lens.get(it)) } + + override fun compareAndSet(expect: A, update: A): Boolean { + val expectT = lens.set(this@optic.value, expect) + val updateT = lens.set(this@optic.value, update) + return compareAndSet(expectT, updateT) + } + + override fun tryEmit(value: A): Boolean = + this@optic.tryEmit(lens.set(this@optic.value, value)) + + override suspend fun emit(value: A): Unit = + this@optic.emit(lens.set(this@optic.value, value)) + + override val subscriptionCount: StateFlow + get() = this@optic.subscriptionCount + + override val replayCache: List + get() = this@optic.replayCache.map { lens.get(it) } + + @ExperimentalCoroutinesApi + override fun resetReplayCache() { + this@optic.resetReplayCache() + } +} diff --git a/arrow-libs/optics/arrow-optics-compose/src/commonMain/kotlin/arrow/optics/State.kt b/arrow-libs/optics/arrow-optics-compose/src/commonMain/kotlin/arrow/optics/State.kt new file mode 100644 index 00000000000..9fc93058b31 --- /dev/null +++ b/arrow-libs/optics/arrow-optics-compose/src/commonMain/kotlin/arrow/optics/State.kt @@ -0,0 +1,27 @@ +package arrow.optics + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State + +/** + * Exposes the value of [this] through the optic. + */ +public fun State.optic(g: Getter): State = object : State { + override val value: A + get() = g.get(this@optic.value) +} + +/** + * Exposes the value of [this] through the optic. + * Any change made to [value] is reflected in the original [MutableState]. + */ +public fun MutableState.optic(lens: Lens): MutableState = object : MutableState { + override var value: A + get() = lens.get(this@optic.value) + set(newValue) { + this@optic.value = lens.set(this@optic.value, newValue) + } + + override fun component1(): A = value + override fun component2(): (A) -> Unit = { value = it } +} diff --git a/build.gradle.kts b/build.gradle.kts index d943b93b3f1..aa1c203ebdd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -37,6 +37,7 @@ allprojects { plugins { base + alias(libs.plugins.android.library) apply false alias(libs.plugins.dokka) alias(libs.plugins.animalSniffer) apply false alias(libs.plugins.kotest.multiplatform) apply false @@ -46,6 +47,7 @@ plugins { alias(libs.plugins.kotlin.binaryCompatibilityValidator) alias(libs.plugins.arrowGradleConfig.nexus) alias(libs.plugins.spotless) apply false + alias(libs.plugins.jetbrainsCompose) apply false } apply(plugin = libs.plugins.kotlinx.knit.get().pluginId) diff --git a/gradle.properties b/gradle.properties index 2f87e20b88c..4bc9a0ebb16 100644 --- a/gradle.properties +++ b/gradle.properties @@ -26,7 +26,7 @@ kotlin.mpp.stability.nowarn=true # https://youtrack.jetbrains.com/issue/KT-32476 kotlin.native.ignoreIncorrectDependencies=true kotlin.native.ignoreDisabledTargets=true -kotlin.native.cacheKind.linuxX64=none +# kotlin.native.cacheKind.linuxX64=none # https://youtrack.jetbrains.com/issue/KT-45545#focus=Comments-27-4773544.0-0 kapt.use.worker.api=false kotlin.mpp.applyDefaultHierarchyTemplate=false @@ -37,3 +37,5 @@ ANIMALSNIFFER=../../gradle/animalsniffer.gradle ANIMALSNIFFER_MPP=../../gradle/animalsniffer-mpp.gradle dokkaEnabled=false + +android.useAndroidX=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 31b92c86567..4bd7ec8b68e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,10 @@ mockWebServer = "4.12.0" retrofit = "2.9.0" retrofitKotlinxSerialization = "1.0.0" spotlessVersion = "6.25.0" +compose = "1.6.0-rc01" +composePlugin = "1.6.0-dev1369" +agp = "8.2.0" +android-compileSdk = "34" cache4k = "0.12.0" [libraries] @@ -52,6 +56,7 @@ ksp = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = kspGradlePlugin = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "kspVersion" } assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" } classgraph = { module = "io.github.classgraph:classgraph", version.ref = "classgraph" } +compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } kotlinCompileTesting = { module = "dev.zacsweers.kctfork:core", version.ref = "kotlinCompileTesting" } kotlinCompileTestingKsp = { module = "dev.zacsweers.kctfork:ksp", version.ref = "kotlinCompileTesting" } cache4k = { module = "io.github.reactivecircus.cache4k:cache4k", version.ref = "cache4k" } @@ -73,3 +78,5 @@ kotlinx-kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlinxSerializationPlugin" } ksp = { id = "com.google.devtools.ksp", version.ref = "kspVersion" } spotless = { id = "com.diffplug.spotless", version.ref = "spotlessVersion" } +jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "composePlugin" } +android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index ea9caf1d8a3..b21acef692b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,3 +1,5 @@ +@file:Suppress("LocalVariableName") + enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") rootProject.name = "arrow" @@ -9,6 +11,8 @@ pluginManagement { mavenCentral() mavenLocal() kotlin_repo_url?.also { maven(it) } + google() + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } @@ -17,11 +21,12 @@ plugins { id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0") } -dependencyResolutionManagement { - @Suppress("LocalVariableName") val kotlin_repo_url: String? by settings - @Suppress("LocalVariableName") val kotlin_version: String? by settings - @Suppress("LocalVariableName") val ksp_version: String? by settings +val kotlin_repo_url: String? by settings +val kotlin_version: String? by settings +val ksp_version: String? by settings +val compose_version: String? by settings +dependencyResolutionManagement { repositories { mavenCentral() gradlePluginPortal() @@ -38,6 +43,10 @@ dependencyResolutionManagement { println("Overriding KSP version with $ksp_version") version("kspVersion", ksp_version!!) } + if (!compose_version.isNullOrBlank()) { + println("Overriding Compose version with $compose_version") + version("composePlugin", compose_version!!) + } } } } @@ -87,6 +96,11 @@ project(":arrow-optics").projectDir = file("arrow-libs/optics/arrow-optics") include("arrow-optics-reflect") project(":arrow-optics-reflect").projectDir = file("arrow-libs/optics/arrow-optics-reflect") +if (kotlin_version.isNullOrBlank() || "2.0" !in kotlin_version!!) { + include("arrow-optics-compose") + project(":arrow-optics-compose").projectDir = file("arrow-libs/optics/arrow-optics-compose") +} + include("arrow-optics-ksp-plugin") project(":arrow-optics-ksp-plugin").projectDir = file("arrow-libs/optics/arrow-optics-ksp-plugin")