diff --git a/.gitignore b/.gitignore index e7e0aac..243954b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ /secret.properties /local.properties -/build/ /.gradle/ /.idea/ /mobk-compose/build/ /mobk-core/build/ +build/ diff --git a/README.md b/README.md index 84838ca..7c1de08 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ kotlin { ### SwiftUI ### -You need add the following file [Observer.kt](./mobk-swift/Observer.kt) to your +You need add the following file [Observer.swift](./mobk-swift/Observer.swift) to your Xcode project. Be sure to replace the import line with the name of your Kotlin framework. @@ -133,3 +133,108 @@ Observer { } } ``` + +### MobK ViewModel ### + +#### Dependency #### +Add the mobk-viewmodel dependency to your commonMain dependencies : + +``` gradle +kotlin { + ... + sourceSets { + commonMain { + dependencies { + ... + api "io.monkeypatch:mobk-viewmodel:$mobk_version" + } + } +``` + +#### Usage #### + +The ViewModel class is a simple class that can be used to store state and logic for your application. +It is designed to be used with SwiftUI and Jetpack Compose, but can be used with any UI toolkit. + +It provides reactions, that respect lifecycle of the ViewModel, and can be used to perform side effects. + +``` kotlin +class CounterViewModel: MobKViewModel { + var counter by observable(0) + + val counterWatch = reaction( + delay = 5.seconds, + trackingFn = { counter }) { counter -> + if (counter != null && counter > 10) { + message = "Counter is too high" + } + } + + val warningWatch = whenReaction(predicate = { counter > 12 }) { + message = "Counter is really too high" + } + + fun increment() { + counter++ + } + + @override + fun onCleared() { + super.onCleared() + // Do some cleanup here + } +} +``` + +#### SwiftUI #### + +You need add the following file [MobKViewModel.swift](./mobk-swift/MobKViewModel.swift) to your Xcode project. Be sure to replace the import line with the name of your Kotlin framework. + +After that, you can use the @VM property wrapper to manage the lifecycle of your ViewModel. When component is destroyed, the ViewModel will be cleared. +Usually, you may also use the @StateObject property wrapper to manage the persistence of your ViewModel. + +``` swift +struct ContentView: View { + @StateObject @VM var counterViewModel: CounterViewModel = CounterViewModel() + + var body: some View { + VStack { + Text(verbatim: self.counterViewModel.counter) + + HStack { + Button(action: { + self.counterViewModel.increment() + }) { + Text("Increment") + } + } + } + } +} +``` + +If your view model has a dependency on a parameter, you have to use the following syntax: + +``` swift +struct ContentView: View { + @StateObject @VM var counterViewModel: CounterViewModel + + init(counter: Int) { + _counterViewModel = asStateObject(CounterViewModel(counter: counter)) + } + + var body: some View { + VStack { + Text(verbatim: self.counterViewModel.counter) + + HStack { + Button(action: { + self.counterViewModel.increment() + }) { + Text("Increment") + } + } + } + } +} +``` \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 74dc6b9..0000000 --- a/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -buildscript { - Properties properties = new Properties() - properties.load(project.rootProject.file("secret.properties").newDataInputStream()) - - ext { - bintrayUsername = properties.getProperty("bintrayUsername") - bintrayPassword = properties.getProperty("bintrayPassword") - coroutines_version = "1.5.1-native-mt" - } - repositories { - google() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' - } -} - -plugins { - id 'org.jetbrains.kotlin.multiplatform' version '1.5.21' -} - -allprojects { - repositories { - google() - mavenCentral() - } -} - -kotlin { - jvm() -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..6c8d832 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,27 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.1.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.21") + classpath("org.jetbrains.compose:compose-gradle-plugin:1.5.1") + } +} + +plugins { + kotlin("multiplatform") version "1.9.10" +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +kotlin { + jvm() +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 2d8d1e4..a50f1ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,2 @@ -android.useAndroidX=true \ No newline at end of file +android.useAndroidX=true +org.jetbrains.compose.experimental.uikit.enabled=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 673e9fd..7f50d7b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Aug 11 10:48:24 CEST 2021 +#Wed Sep 20 20:55:00 CEST 2023 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mobk-compose/build.gradle b/mobk-compose/build.gradle deleted file mode 100644 index fdf5c49..0000000 --- a/mobk-compose/build.gradle +++ /dev/null @@ -1,73 +0,0 @@ -plugins { - id 'maven-publish' -} - -apply plugin: 'com.android.library' -apply plugin: 'org.jetbrains.kotlin.android' - -group "io.monkeypatch" -version "0.0.9" - -android { - compileSdkVersion(30) - defaultConfig { - minSdkVersion(24) - targetSdkVersion(30) - } - buildFeatures { - compose = true - } - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - composeOptions { - kotlinCompilerExtensionVersion "1.0.1" - kotlinCompilerVersion "1.5.21" - } - - kotlinOptions { - - jvmTarget = "1.8" - useIR = true - } -} - -dependencies { - implementation project(":mobk-core") - implementation "androidx.compose.ui:ui:1.0.1" -} - -publishing { - repositories { - maven{ - url = "https://api.bintray.com/maven/alexandre-delattre/MonkeyPatchLibs/mobk-compose/;publish=1" - credentials { - username bintrayUsername - password bintrayPassword - } - authentication { - basic(BasicAuthentication) - } - } - } -} - - -afterEvaluate { - publishing { - publications { - // Creates a Maven publication called "release". - release(MavenPublication) { - // Applies the component for the release build variant. - from components.release - } - // Creates a Maven publication called “debug”. - debug(MavenPublication) { - // Applies the component for the debug build variant. - from components.debug - } - } - } -} \ No newline at end of file diff --git a/mobk-core/build.gradle b/mobk-core/build.gradle deleted file mode 100644 index 331b461..0000000 --- a/mobk-core/build.gradle +++ /dev/null @@ -1,57 +0,0 @@ -plugins { - id 'org.jetbrains.kotlin.multiplatform' - id 'maven-publish' -} - -apply plugin: 'com.android.library' -group = "io.monkeypatch" -version = "0.0.9" - -kotlin { - explicitApi() - - android { - publishLibraryVariants("release", "debug") - } - ios() - sourceSets { - commonMain { - dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") - } - } - commonTest { - dependencies { - implementation(kotlin("test-common")) - implementation(kotlin("test-annotations-common")) - } - } - - androidTest { - dependencies { - implementation(kotlin("test")) - implementation(kotlin("test-junit")) - } - } - } -} - -android { - compileSdkVersion(30) - defaultConfig { - minSdkVersion(21) - targetSdkVersion(30) - } -} - -publishing { - repositories { - maven{ - url = "https://api.bintray.com/maven/alexandre-delattre/MonkeyPatchLibs/mobk-core/;publish=1" - credentials { - username bintrayUsername - password bintrayPassword - } - } - } -} \ No newline at end of file diff --git a/mobk-core/build.gradle.kts b/mobk-core/build.gradle.kts new file mode 100644 index 0000000..50eeb49 --- /dev/null +++ b/mobk-core/build.gradle.kts @@ -0,0 +1,74 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") + id("org.jetbrains.compose") + id("maven-publish") +} + +group = "io.monkeypatch" +version = "0.0.11" + +@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) +kotlin { + targetHierarchy.default() + + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + + publishLibraryVariants("release") + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "mobk-core" + } + + } + sourceSets { + val commonMain by getting{ + dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation("org.jetbrains.kotlinx:atomicfu:0.21.0") + } + + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + val iosMain by getting + val iosTest by getting + } +} + +android { + namespace = "io.monkeypatch.mobk.core" + compileSdk = 34 + defaultConfig { + minSdk = 24 + } +} + + + +publishing { + repositories { + maven{ + url = uri("https://api.bintray.com/maven/alexandre-delattre/MonkeyPatchLibs/mobk-core/;publish=1") + credentials { + username = project.findProperty("bintrayUsername") as String? ?: "" + password = project.findProperty("bintrayPassword") as String? ?: "" + } + } + } +} \ No newline at end of file diff --git a/mobk-compose/src/main/AndroidManifest.xml b/mobk-core/src/androidMain/AndroidManifest.xml similarity index 76% rename from mobk-compose/src/main/AndroidManifest.xml rename to mobk-core/src/androidMain/AndroidManifest.xml index 4c760d5..abc6037 100644 --- a/mobk-compose/src/main/AndroidManifest.xml +++ b/mobk-core/src/androidMain/AndroidManifest.xml @@ -1,4 +1,4 @@ - diff --git a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/api/Api.kt b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/api/Api.kt index 10ce0da..d48bad8 100644 --- a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/api/Api.kt +++ b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/api/Api.kt @@ -4,6 +4,7 @@ import io.monkeypatch.mobk.core.* import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty +import kotlin.time.Duration public fun autorun(body: () -> Unit): ReactionDisposer = @@ -13,6 +14,23 @@ public fun action(body: () -> Unit) { Action(body).runAction() } +public fun reaction(context: ReactiveContext = ReactiveContext.main, + delay: Duration? = null, + equals: ((T?, T?) -> Boolean)? = null, + onError: ReactionErrorHandler? = null,trackingFn: (Reaction) -> T, effect: (T?) -> Unit): ReactionDisposer = + createReaction( + context = context, + delay = delay, + equals = equals, + onError = onError, + trackingFn = trackingFn, effect = effect) + +public fun whenReaction(context: ReactiveContext = ReactiveContext.main, + timeout: Duration? = null, + onError: ReactionErrorHandler? = null, + predicate: (Reaction) -> Boolean, + effect: () -> Unit) = createWhenReaction(context = context, timeout = timeout, onError = onError, predicate = predicate, effect = effect) + public fun observable(initialValue: T): ReadWriteProperty = ObservableDelegate(initialValue) public fun computed(body: () -> T): ReadOnlyProperty = ComputedDelegate(body) diff --git a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Autorun.kt b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Autorun.kt index d327c17..ed2d0b7 100644 --- a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Autorun.kt +++ b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Autorun.kt @@ -1,14 +1,5 @@ package io.monkeypatch.mobk.core -public interface ReactionDisposer { - public operator fun invoke() -} - -internal data class ReactionDisposerImpl(private val reaction: Reaction): ReactionDisposer { - override operator fun invoke() { - reaction.dispose() - } -} internal fun createAutorun( context: ReactiveContext = ReactiveContext.main, diff --git a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Derivation.kt b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Derivation.kt index 33612af..91a3e08 100644 --- a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Derivation.kt +++ b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Derivation.kt @@ -7,7 +7,7 @@ public enum class DerivationState { STALE } -internal interface Derivation { + interface Derivation { val name: String var observables: Set var newObservables: MutableSet diff --git a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Exception.kt b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Exception.kt index b775609..76e46fb 100644 --- a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Exception.kt +++ b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Exception.kt @@ -8,4 +8,6 @@ public sealed class MobXException(message: String): Exception(message) { /// This captures the stack trace when user-land code throws an exception public class MobXCaughtException(public val exception: Throwable): MobXException("MobXCaughtException: $exception") + + public class MobXTimeoutException(message: String): MobXException(message) } \ No newline at end of file diff --git a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Reaction.kt b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Reaction.kt index f524942..60b7185 100644 --- a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Reaction.kt +++ b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/Reaction.kt @@ -1,8 +1,8 @@ package io.monkeypatch.mobk.core -internal typealias ReactionErrorHandler = (error: Throwable, reaction: Reaction) -> Unit + typealias ReactionErrorHandler = (error: Throwable, reaction: Reaction) -> Unit -internal interface Reaction : Derivation { + interface Reaction : Derivation { val isDisposed: Boolean fun dispose() diff --git a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/ReactionDisposers.kt b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/ReactionDisposers.kt new file mode 100644 index 0000000..ce2bcab --- /dev/null +++ b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/ReactionDisposers.kt @@ -0,0 +1,14 @@ +package io.monkeypatch.mobk.core + + class ReactionDisposers { + private val disposers = mutableListOf() + + fun add(disposer: ReactionDisposer) { + disposers.add(disposer) + } + + fun clear() { + disposers.forEach { it() } + disposers.clear() + } +} \ No newline at end of file diff --git a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/ReactionHelper.kt b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/ReactionHelper.kt new file mode 100644 index 0000000..afb136d --- /dev/null +++ b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/ReactionHelper.kt @@ -0,0 +1,133 @@ +package io.monkeypatch.mobk.core + +import kotlinx.coroutines.Delay +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration + +public interface ReactionDisposer { + public operator fun invoke() +} + +internal data class ReactionDisposerImpl(private val reaction: Reaction): ReactionDisposer { + override operator fun invoke() { + reaction.dispose() + } +} + +internal fun createReaction( + context: ReactiveContext = ReactiveContext.main, + name: String = context.nameFor("Reaction"), + delay: Duration? = null, + equals: ((T?, T?) -> Boolean)? = null, + onError: ReactionErrorHandler? = null, + trackingFn: (Reaction) -> T, + effect: (T?) -> Unit +): ReactionDisposer { + var lastValue: T? = null + var firstTime = true + var runSync = delay == null + var rxn: ReactionImpl? = null + val effectAction = effect + + val reactionRunner: () -> Unit = { + val reaction = rxn + if (reaction == null || !reaction.isDisposed) { + var changed = false + + reaction?.run { + track { + val nextValue = trackingFn(reaction) + val isEqual = if (equals != null) equals(nextValue, lastValue) else lastValue == nextValue + + changed = firstTime || !isEqual + lastValue = nextValue + } + } + + val canInvokeEffect = !firstTime && changed + + if (canInvokeEffect) { + effectAction(lastValue) + } + + if (firstTime) { + firstTime = false + } + } + } + + var isScheduled = false + rxn = ReactionImpl( + context, name, onError + ) { + if (firstTime || runSync) { + reactionRunner() + } else if(!isScheduled) { + isScheduled = true + + context.config.reactionCoroutineScope.launch { + if (delay != null) { + delay(delay) + } + + isScheduled = false + + withContext(Dispatchers.Main) { + reactionRunner() + } + } + } + } + rxn.schedule() + return ReactionDisposerImpl(rxn) +} + +internal fun createWhenReaction( + context: ReactiveContext = ReactiveContext.main, + name: String = context.nameFor("Reaction"), + timeout: Duration? = null, + onError: ReactionErrorHandler? = null, + predicate: (Reaction) -> Boolean, + effect: () -> Unit +): ReactionDisposer { + val effectAction = effect + var dispose: ReactionDisposer? = null + val rxn: ReactionImpl? = null + + if (timeout != null) { + context.config.reactionCoroutineScope.launch { + withTimeout(timeout) { + val d = dispose + val r = rxn + if (d != null && r != null) { + if (!r.isDisposed) { + d() + + val error = MobXException.MobXTimeoutException("WHEN_TIMEOUT") + if (onError != null) { + onError(error, r) + } else { + throw error + } + } + } + } + } + } + + dispose = createAutorun( + context = context, + name = name, + onError = onError + ) { reaction -> + if (predicate(reaction)) { + reaction.dispose() + effectAction() + } + } + return dispose +} \ No newline at end of file diff --git a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/ReactiveContext.kt b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/ReactiveContext.kt index 7fd9667..90ee19c 100644 --- a/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/ReactiveContext.kt +++ b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/core/ReactiveContext.kt @@ -1,6 +1,8 @@ package io.monkeypatch.mobk.core import io.monkeypatch.mobk.utils.isMainThread +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlin.native.concurrent.ThreadLocal import kotlin.properties.Delegates @@ -59,7 +61,8 @@ public data class ReactiveConfig( val writePolicy: ReactiveWritePolicy, val readPolicy: ReactiveReadPolicy, val maxIterations: Int = 100, - val enforceWriteOnMainThread: Boolean = true + val enforceWriteOnMainThread: Boolean = true, + val reactionCoroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default) ) { internal val reactionErrorHandlers: MutableSet = mutableSetOf() diff --git a/mobk-compose/src/main/java/io/monkeypatch/mobk/ui/Observer.kt b/mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/ui/Observer.kt similarity index 100% rename from mobk-compose/src/main/java/io/monkeypatch/mobk/ui/Observer.kt rename to mobk-core/src/commonMain/kotlin/io/monkeypatch/mobk/ui/Observer.kt diff --git a/mobk-core/src/commonTest/kotlin/io/monkeypatch/mobk/core/TestReactiveContext.kt b/mobk-core/src/commonTest/kotlin/io/monkeypatch/mobk/core/TestReactiveContext.kt index ef004f4..9a34d3e 100644 --- a/mobk-core/src/commonTest/kotlin/io/monkeypatch/mobk/core/TestReactiveContext.kt +++ b/mobk-core/src/commonTest/kotlin/io/monkeypatch/mobk/core/TestReactiveContext.kt @@ -96,4 +96,63 @@ class TestReactiveContext { assertEquals(listOf(10, 11, 12, 20), xHistory) assertEquals(listOf(true, false, true), isPairHistory) } + + @Test + fun testReaction() { + val x = Observable(10) + val isPair = Computed { x.value % 2 == 0 } + + val xHistory = mutableListOf() + var pairCounter = 1 + + createAutorun { + xHistory.add(x.value) + } + + createReaction ( + trackingFn = { + isPair.value + } + ) { value -> + value?.let { + pairCounter++ + } + } + + x.value = 11 + x.value = 12 + x.value = 20 + + assertEquals(listOf(10, 11, 12, 20), xHistory) + assertEquals(3, pairCounter) + } + + @Test + fun testWhenReaction() { + val x = Observable(10) + + val xHistory = mutableListOf() + var isTwelve = 0 + + createAutorun { + xHistory.add(x.value) + } + + createWhenReaction ( + predicate = { + x.value == 12 + } + ) { + isTwelve++ + } + + x.value = 11 + x.value = 12 + x.value = 20 + x.value= 12 + x.value = 24 + + assertEquals(listOf(10, 11, 12, 20,12,24), xHistory) + assertEquals(1, isTwelve) + } } \ No newline at end of file diff --git a/mobk-swift/MobKViewModel.swift b/mobk-swift/MobKViewModel.swift new file mode 100644 index 0000000..ce6ac1e --- /dev/null +++ b/mobk-swift/MobKViewModel.swift @@ -0,0 +1,22 @@ +// +// MobKViewModel.swift +// + +import SwiftUI +import + +@propertyWrapper class VM: ObservableObject where S: Mobk_viewmodelMobkViewModel { + var wrappedValue: S + + public init(wrappedValue: S) { + self.wrappedValue = wrappedValue() + } + + deinit { + self.wrappedValue.onCleared() + } +} + +func asStateObject(_ factory: @autoclosure @escaping () -> S) -> StateObject> where S: Mobk_viewmodelMobkViewModel { + return StateObject(wrappedValue: VM(wrappedValue: factory())) +} diff --git a/mobk-swift/Observer.swift b/mobk-swift/Observer.swift new file mode 100644 index 0000000..e549034 --- /dev/null +++ b/mobk-swift/Observer.swift @@ -0,0 +1,39 @@ +// +// Observer.swift +// + +import SwiftUI +import + +struct ObserverView : View { + @ObservedObject var reactionObservable: ReactionObservable + + var body: some View { + self.reactionObservable.view + } +} + +func Observer(@ViewBuilder f: @escaping () -> V) -> some View { + let reactionObservable = ReactionObservable(f: f) + return ObserverView(reactionObservable: reactionObservable) +} + +class ReactionObservable: ObservableObject { + @Published var view: V? = nil + var disposer: ReactionDisposer? = nil + + init(f: @escaping () -> V) { + disposer = ApiKt.autorun { [weak self] in + self?.view = f() + } + } + + deinit { + let d = disposer + disposer = nil + DispatchQueue.main.async { + d?.invoke() + } + } + +} diff --git a/mobk-viewmodel/build.gradle.kts b/mobk-viewmodel/build.gradle.kts new file mode 100644 index 0000000..e7423b1 --- /dev/null +++ b/mobk-viewmodel/build.gradle.kts @@ -0,0 +1,58 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") + id("maven-publish") +} + + +group = "io.monkeypatch" +version = "0.0.11" + +@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) +kotlin { + targetHierarchy.default() + + androidTarget { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + + publishLibraryVariants("release") + } + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "mobk-viewmodel" + + export("dev.icerock.moko:mvvm-core:0.16.1") + } + } + + sourceSets { + val commonMain by getting { + dependencies { + api(project(":mobk-core")) + api("dev.icerock.moko:mvvm-core:0.16.1") + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test")) + } + } + } +} + +android { + namespace = "io.monkeypatch.mobk.viewmodel" + compileSdk = 33 + defaultConfig { + minSdk = 24 + } +} \ No newline at end of file diff --git a/mobk-viewmodel/src/commonMain/kotlin/io/monkeypatch/mobk/viewmodel/MobkViewModel.kt b/mobk-viewmodel/src/commonMain/kotlin/io/monkeypatch/mobk/viewmodel/MobkViewModel.kt new file mode 100644 index 0000000..73c867d --- /dev/null +++ b/mobk-viewmodel/src/commonMain/kotlin/io/monkeypatch/mobk/viewmodel/MobkViewModel.kt @@ -0,0 +1,44 @@ +package io.monkeypatch.mobk.viewmodel + +import dev.icerock.moko.mvvm.viewmodel.ViewModel +import io.monkeypatch.mobk.core.Reaction +import io.monkeypatch.mobk.core.ReactionDisposers +import io.monkeypatch.mobk.core.ReactionErrorHandler +import io.monkeypatch.mobk.core.ReactiveContext +import kotlin.time.Duration + + +interface MobkLifecycleAware { + fun reaction( + context: ReactiveContext = ReactiveContext.main, delay: Duration? = null, + equals: ((T?, T?) -> Boolean)? = null, + onError: ReactionErrorHandler? = null, + trackingFn: (Reaction) -> T, effect: (T?) -> Unit + ) +} + +abstract class MobkViewModel : ViewModel(), MobkLifecycleAware { + private val reactionDispatcher = ReactionDisposers() + + override fun reaction( + context: ReactiveContext, + delay: Duration?, + equals: ((T?, T?) -> Boolean)?, + onError: ReactionErrorHandler?, + trackingFn: (Reaction) -> T, effect: (T?) -> Unit + ) = + reactionDispatcher.add( + io.monkeypatch.mobk.api.reaction( + context = context, + delay = delay, + equals = equals, + trackingFn = trackingFn, + effect = effect + ) + ) + + override fun onCleared() { + super.onCleared() + reactionDispatcher.clear() + } +} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 604fc13..0000000 --- a/settings.gradle +++ /dev/null @@ -1,2 +0,0 @@ -rootProject.name = 'mobk' -include ':mobk-core', ':mobk-compose' \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..c801984 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,3 @@ +rootProject.name = "mobk" +include(":mobk-core") +include(":mobk-viewmodel")