4 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/androidMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/WriteWith.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | public actual typealias WriteWith = kotlinx.parcelize.WriteWith
4 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/androidMain/kotlin/com/hoc081098/kmp/viewmodel/safe/internal/parcelableArrayTransform.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.safe.internal
2 |
3 | import com.hoc081098.kmp.viewmodel.parcelable.Parcelable
4 |
5 | @JvmSynthetic
6 | @PublishedApi
7 | internal actual inline fun parcelableArrayTransform(): ((Any?) -> Array)? {
8 | return transform@{ value ->
9 | // Workaround according to https://github.com/androidx/androidx/commit/2ffce096e13e3aa4675a8b0fd8b0d74cb1ced653
10 | //
11 | // From AndroidX's commit message:
12 | //
13 | // As part of the exploration of this problem, arrays
14 | // of Parcelables were found impossible to actually
15 | // properly support out of the box, so additional KDocs
16 | // were added to specifically call out the workaround
17 | // required to support those.
18 |
19 | value!!
20 |
21 | @Suppress("UNCHECKED_CAST")
22 | value as Array
23 |
24 | Array(value.size) { value[it] as T? }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/androidUnitTest/kotlin/com/hoc081098/kmp/viewmodel/safe/assertValue.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.safe
2 |
3 | import androidx.lifecycle.LiveData
4 | import com.hoc081098.kmp.viewmodel.MainThread
5 | import com.hoc081098.kmp.viewmodel.extendedAssertEquals
6 | import kotlin.test.assertTrue
7 |
8 | @MainThread
9 | internal fun LiveData.assertValue(expected: T?) {
10 | var received = false
11 | observeForever {
12 | received = true
13 | extendedAssertEquals(expected, it)
14 | }
15 | assertTrue { received }
16 | }
17 |
18 | @MainThread
19 | internal fun LiveData.assertValues(vararg expected: T?) {
20 | var received = false
21 | var index = 0
22 | observeForever {
23 | received = true
24 | extendedAssertEquals(expected[index++], it)
25 | }
26 | assertTrue { received }
27 | }
28 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonJvmMain/kotlin/com/hoc081098/kmp/viewmodel/serializable/JvmSerializable.commonJvm.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.serializable
2 |
3 | public actual typealias JvmSerializable = java.io.Serializable
4 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/SavedStateHandleFactory.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | /**
4 | * Responsible for creating a new [SavedStateHandle] instance.
5 | * This is used to override the default [SavedStateHandle] creation logic.
6 | *
7 | * @see SAVED_STATE_HANDLE_FACTORY_KEY
8 | * @see createSavedStateHandle
9 | */
10 | public fun interface SavedStateHandleFactory {
11 | public fun create(): SavedStateHandle
12 | }
13 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/SavedStateHandleSupport.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import kotlin.jvm.JvmField
4 |
5 | /**
6 | * Creates [SavedStateHandle] that can be used in your [ViewModel]s.
7 | *
8 | * ### On all platforms
9 | *
10 | * If this [CreationExtras] contains [SAVED_STATE_HANDLE_FACTORY_KEY],
11 | * the returned [SavedStateHandle] will be created by this factory.
12 | *
13 | * Basically, [SAVED_STATE_HANDLE_FACTORY_KEY] has higher priority
14 | * than the default creation logic on each platform.
15 | *
16 | * ### Otherwise
17 | *
18 | * #### Other platforms
19 | *
20 | * This function simply returns an empty [SavedStateHandle].
21 | *
22 | * #### On Android
23 | *
24 | * This function requires `enableSavedStateHandles` call during the component
25 | * initialization. Latest versions of androidx components like `ComponentActivity`, `Fragment`,
26 | * `NavBackStackEntry` makes this call automatically.
27 | *
28 | * This [CreationExtras] must contain `SAVED_STATE_REGISTRY_OWNER_KEY`,
29 | * `VIEW_MODEL_STORE_OWNER_KEY` and [VIEW_MODEL_KEY].
30 | *
31 | * @throws IllegalArgumentException on Android, if this `CreationExtras` are missing required keys:
32 | * `ViewModelStoreOwnerKey`, `SavedStateRegistryOwnerKey`, `VIEW_MODEL_KEY`.
33 | *
34 | */
35 | @MainThread
36 | public expect fun CreationExtras.createSavedStateHandle(): SavedStateHandle
37 |
38 | /**
39 | * A key for [SavedStateHandle] that should be passed to [SavedStateHandle] if needed.
40 | */
41 | @JvmField
42 | public val SAVED_STATE_HANDLE_FACTORY_KEY: CreationExtrasKey =
43 | object : CreationExtrasKey {
44 | override fun toString(): String = "CreationExtras.Key"
45 | }
46 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/IgnoredOnParcel.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | /**
4 | * A multiplatform alias for `kotlinx.parcelize.IgnoredOnParcel` from the `kotlin-parcelize` Gradle plugin.
5 | * Make sure you have the `kotlin-parcelize` Gradle plugin applied.
6 | *
7 | * The property annotated with [IgnoredOnParcel] will not be stored into parcel.
8 | */
9 | @Target(AnnotationTarget.PROPERTY)
10 | @Retention(AnnotationRetention.SOURCE)
11 | public expect annotation class IgnoredOnParcel()
12 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/Parcelable.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | /**
4 | * Represents an object that can be serialized and deserialized.
5 | *
6 | * Currently the actual serialization is only supported on Android.
7 | * It's a no-op on all other platforms.
8 | */
9 | public expect interface Parcelable
10 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/Parceler.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | /**
4 | * A multiplatform alias for `kotlinx.parcelize.Parceler` from the `kotlin-parcelize` Gradle plugin.
5 | * Make sure you have the `kotlin-parcelize` Gradle plugin applied.
6 | *
7 | * The base interface for custom [Parcelize] serializers.
8 | */
9 | public expect interface Parceler
10 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/Parcelize.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | /**
4 | * A multiplatform alias for `kotlinx.parcelize.Parcelize` from the `kotlin-parcelize` Gradle plugin.
5 | * Make sure you have the `kotlin-parcelize` Gradle plugin applied.
6 | */
7 | @Target(AnnotationTarget.CLASS)
8 | @Retention(AnnotationRetention.BINARY)
9 | public expect annotation class Parcelize()
10 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/TypeParceler.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | /**
4 | * A multiplatform alias for `kotlinx.parcelize.TypeParceler` from the `kotlin-parcelize` Gradle plugin.
5 | * Make sure you have the `kotlin-parcelize` Gradle plugin applied.
6 | *
7 | * Specifies what [Parceler] should be used for a particular type [T].
8 | */
9 | @Retention(AnnotationRetention.SOURCE)
10 | @Repeatable
11 | @Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
12 | public expect annotation class TypeParceler>()
13 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/WriteWith.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | /**
4 | * A multiplatform alias for `kotlinx.parcelize.WriteWith` from the `kotlin-parcelize` Gradle plugin.
5 | * Make sure you have the `kotlin-parcelize` Gradle plugin applied.
6 | *
7 | * Specifies what [Parceler] should be used for the annotated type.
8 | */
9 | @Retention(AnnotationRetention.SOURCE)
10 | @Target(AnnotationTarget.TYPE)
11 | public expect annotation class WriteWith>()
12 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/safe/InternalKmpViewModelApi.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.safe
2 |
3 | /**
4 | * Marks declarations in the `com.hoc081098.kmp.viewmodel.safe` that are **delicate** —
5 | * they have limited use-case and shall be used with care in general code.
6 | *
7 | * Carefully read documentation of any declaration marked as `DelicateSafeSavedStateHandleApi`.
8 | */
9 | @MustBeDocumented
10 | @Retention(value = AnnotationRetention.BINARY)
11 | @Target(
12 | AnnotationTarget.ANNOTATION_CLASS,
13 | AnnotationTarget.PROPERTY,
14 | AnnotationTarget.CONSTRUCTOR,
15 | AnnotationTarget.FUNCTION,
16 | )
17 | @RequiresOptIn(
18 | level = RequiresOptIn.Level.WARNING,
19 | message = "This is a delicate API and its use requires care." +
20 | " Make sure you fully read and understand documentation of the declaration that is marked as a delicate API.",
21 | )
22 | public annotation class DelicateSafeSavedStateHandleApi
23 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/safe/NonNullSavedStateHandleKey.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.safe
2 |
3 | import com.hoc081098.kmp.viewmodel.InternalKmpViewModelApi
4 | import com.hoc081098.kmp.viewmodel.SavedStateHandle
5 | import dev.drewhamilton.poko.ArrayContentBased
6 | import dev.drewhamilton.poko.Poko
7 |
8 | /**
9 | * Key for values stored in [SavedStateHandle].
10 | * The type of the value associated with this key is [T] (non-null)
11 | *
12 | * **NOTE**: When using [key] directly, you must ensure that the value associated with the key has **type `T`**.
13 | * Otherwise, it may throw [kotlin.ClassCastException] or [kotlin.NullPointerException] when using this key
14 | * with [SafeSavedStateHandle].
15 | *
16 | * @param key the key
17 | * @param defaultValue the default value is used if no value is associated with the key
18 | *
19 | * @see SafeSavedStateHandle
20 | */
21 | @Poko
22 | public class NonNullSavedStateHandleKey @InternalKmpViewModelApi
23 | @PublishedApi
24 | internal constructor(
25 | /**
26 | * **NOTE**: When using [key] directly, you must ensure that the value associated with the key has **type `T`**.
27 | * Otherwise, it may throw [kotlin.ClassCastException] or [kotlin.NullPointerException] when using this key
28 | * with [SafeSavedStateHandle].
29 | */
30 | @DelicateSafeSavedStateHandleApi
31 | public val key: String,
32 | @ArrayContentBased public val defaultValue: T,
33 | internal val transform: ((Any?) -> T)? = null,
34 | ) {
35 | public companion object
36 | }
37 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/safe/NullableSavedStateHandleKey.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.safe
2 |
3 | import com.hoc081098.kmp.viewmodel.InternalKmpViewModelApi
4 | import com.hoc081098.kmp.viewmodel.SavedStateHandle
5 | import dev.drewhamilton.poko.ArrayContentBased
6 | import dev.drewhamilton.poko.Poko
7 |
8 | /**
9 | * Key for values stored in [SavedStateHandle].
10 | * The type of the value associated with this key is [T]? (nullable, `null` is valid).
11 | *
12 | * **NOTE**: When using [key] directly, you must ensure that the value associated with the key has **type `T?`**.
13 | * Otherwise, it may throw [kotlin.ClassCastException] when using this key with [SafeSavedStateHandle].
14 | *
15 | * @param key the key
16 | * @param defaultValue the default value is used if no value is associated with the key
17 | *
18 | * @see SafeSavedStateHandle
19 | */
20 | @Poko
21 | public class NullableSavedStateHandleKey @PublishedApi
22 | @InternalKmpViewModelApi
23 | internal constructor(
24 | /**
25 | * **NOTE**: When using [key] directly, you must ensure that the value associated with the key has **type `T?`**.
26 | * Otherwise, it may throw [kotlin.ClassCastException] when using this key with [SafeSavedStateHandle].
27 | */
28 | @DelicateSafeSavedStateHandleApi
29 | public val key: String,
30 | @ArrayContentBased public val defaultValue: T?,
31 | internal val transform: ((Any?) -> T?)? = null,
32 | ) {
33 | public companion object
34 | }
35 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/safe/internal/mapState.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.safe.internal
2 |
3 | import kotlinx.coroutines.flow.FlowCollector
4 | import kotlinx.coroutines.flow.StateFlow
5 | import kotlinx.coroutines.flow.distinctUntilChanged
6 | import kotlinx.coroutines.flow.map
7 |
8 | /**
9 | * Map a [StateFlow] to another [StateFlow] with the given [transform] function.
10 | */
11 | internal fun StateFlow.mapState(transform: (T) -> R): StateFlow =
12 | object : StateFlow {
13 | override val replayCache: List
14 | get() = this@mapState.replayCache.map(transform)
15 |
16 | override val value: R
17 | get() = transform(this@mapState.value)
18 |
19 | override suspend fun collect(collector: FlowCollector): Nothing {
20 | this@mapState
21 | .map { transform(it) }
22 | .distinctUntilChanged()
23 | .collect(collector)
24 |
25 | error("StateFlow collection never ends.")
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/safe/internal/parcelableArrayTransform.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.safe.internal
2 |
3 | import com.hoc081098.kmp.viewmodel.parcelable.Parcelable
4 | import kotlin.jvm.JvmSynthetic
5 |
6 | @JvmSynthetic
7 | @PublishedApi
8 | internal expect inline fun parcelableArrayTransform(): ((Any?) -> Array)?
9 |
10 | @JvmSynthetic
11 | @PublishedApi
12 | internal inline fun nullableParcelableArrayTransform(): ((Any?) -> Array?)? =
13 | parcelableArrayTransform()
14 | ?.let { transform ->
15 | {
16 | it?.let(transform)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/serializable/JvmSerializable.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.serializable
2 |
3 | /**
4 | * Multiplatform reference to Java `java.io.Serializable` interface.
5 | *
6 | * Interface that indicates that the implementing object can be serialized using JVM specific serialization.
7 | * On non-JVM platforms, this interface is a no-op and does not provide any actual serialization capabilities.
8 | */
9 | public expect interface JvmSerializable
10 |
11 | @Deprecated(
12 | message = "JvmSerializable should be used instead to avoid confusion with KotlinX-Serialization @Serializable",
13 | replaceWith = ReplaceWith("com.hoc081098.kmp.viewmodel.serializable.JvmSerializable"),
14 | level = DeprecationLevel.WARNING,
15 | )
16 | public typealias Serializable = JvmSerializable
17 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonTest/kotlin/com/hoc081098/kmp/viewmodel/TestParcelable.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import com.hoc081098.kmp.viewmodel.parcelable.Parcelable
4 | import com.hoc081098.kmp.viewmodel.parcelable.Parcelize
5 |
6 | @Parcelize
7 | data class TestParcelable(val value: Int) : Parcelable
8 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonTest/kotlin/com/hoc081098/kmp/viewmodel/TestSerializable.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import com.hoc081098.kmp.viewmodel.serializable.JvmSerializable
4 |
5 | class TestSerializable(val value: Int) : JvmSerializable
6 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/commonTest/kotlin/com/hoc081098/kmp/viewmodel/extendedAssertEquals.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import kotlin.test.assertContentEquals
4 | import kotlin.test.assertEquals
5 |
6 | fun extendedAssertEquals(expected: T, actual: T, message: String? = null) = when (expected) {
7 | is Iterable<*> -> {
8 | check(actual is Iterable<*>) { "actual value is not an iterable" }
9 |
10 | fun Iterable.transformArrayToList(): List = map {
11 | if (it is Array<*>) {
12 | it.toList()
13 | } else {
14 | it
15 | }
16 | }
17 |
18 | assertContentEquals(
19 | expected = expected.transformArrayToList(),
20 | actual = actual.transformArrayToList(),
21 | message = message,
22 | )
23 | }
24 |
25 | is Array<*> -> {
26 | check(actual is Array<*>) { "actual value is not an array" }
27 |
28 | @Suppress("UNCHECKED_CAST")
29 | assertContentEquals(
30 | expected = expected as Array,
31 | actual = actual as Array,
32 | message = message,
33 | )
34 | }
35 |
36 | else -> {
37 | assertEquals(expected, actual, message)
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/nonAndroidMain/kotlin/com/hoc081098/kmp/viewmodel/SavedStateHandleSupport.nonAndroid.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | @MainThread
4 | public actual fun CreationExtras.createSavedStateHandle(): SavedStateHandle =
5 | this[SAVED_STATE_HANDLE_FACTORY_KEY]?.create() ?: SavedStateHandle()
6 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/nonAndroidMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/IgnoredOnParcel.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | @Target(AnnotationTarget.PROPERTY)
4 | @Retention(AnnotationRetention.SOURCE)
5 | public actual annotation class IgnoredOnParcel actual constructor()
6 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/nonAndroidMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/Parcelable.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | public actual interface Parcelable
4 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/nonAndroidMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/Parceler.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | @Suppress("unused")
4 | public actual interface Parceler
5 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/nonAndroidMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/Parcelize.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | @Target(AnnotationTarget.CLASS)
4 | @Retention(AnnotationRetention.BINARY)
5 | public actual annotation class Parcelize actual constructor()
6 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/nonAndroidMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/TypeParceler.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | @Retention(AnnotationRetention.SOURCE)
4 | @Repeatable
5 | @Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY)
6 | public actual annotation class TypeParceler> actual constructor()
7 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/nonAndroidMain/kotlin/com/hoc081098/kmp/viewmodel/parcelable/WriteWith.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.parcelable
2 |
3 | @Retention(AnnotationRetention.SOURCE)
4 | @Target(AnnotationTarget.TYPE)
5 | public actual annotation class WriteWith> actual constructor()
6 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/nonAndroidMain/kotlin/com/hoc081098/kmp/viewmodel/safe/internal/parcelableArrayTransform.nonAndroid.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.safe.internal
2 |
3 | import com.hoc081098.kmp.viewmodel.parcelable.Parcelable
4 | import kotlin.jvm.JvmSynthetic
5 |
6 | @JvmSynthetic
7 | @PublishedApi
8 | internal actual inline fun parcelableArrayTransform(): ((Any?) -> Array)? = null
9 |
--------------------------------------------------------------------------------
/viewmodel-savedstate/src/nonJvmMain/kotlin/com/hoc081098/kmp/viewmodel/serializable/JvmSerializable.nonJvm.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.serializable
2 |
3 | /**
4 | * No-op on non-JVM platforms.
5 | */
6 | public actual interface JvmSerializable
7 |
--------------------------------------------------------------------------------
/viewmodel/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_NAME=kmp-viewmodel
2 | POM_ARTIFACT_ID=kmp-viewmodel
3 | POM_DESCRIPTION=Shared ViewModel in Kotlin Multiplatform - A Kotlin Multiplatform library that provides shared MVVM for UI applications. Components are lifecycle-aware on Android.
4 |
--------------------------------------------------------------------------------
/viewmodel/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/viewmodel/src/androidMain/kotlin/com/hoc081098/kmp/viewmodel/Closeable.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | public actual typealias Closeable = java.io.Closeable
4 |
--------------------------------------------------------------------------------
/viewmodel/src/androidMain/kotlin/com/hoc081098/kmp/viewmodel/CreationExtras.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import androidx.lifecycle.ViewModelProvider
4 | import androidx.lifecycle.viewmodel.CreationExtras as AndroidXCreationExtras
5 | import androidx.lifecycle.viewmodel.CreationExtras.Key as AndroidXCreationExtrasKey
6 | import androidx.lifecycle.viewmodel.MutableCreationExtras as AndroidXMutableCreationExtras
7 |
8 | public actual typealias CreationExtras = AndroidXCreationExtras
9 |
10 | public actual typealias CreationExtrasKey = AndroidXCreationExtrasKey
11 |
12 | public actual typealias EmptyCreationExtras = AndroidXCreationExtras.Empty
13 |
14 | /**
15 | * With Kotlin 1.9.20, an expect with default arguments are no longer permitted when an actual is a typealias
16 | * (see [KT-57614](https://youtrack.jetbrains.com/issue/KT-57614)),
17 | * we cannot use `actual typealias MutableCreationExtras = androidx.lifecycle.viewmodel.MutableCreationExtras`.
18 | * So we have to use wrapper class instead.
19 | */
20 | public actual class MutableCreationExtrasBuilder public actual constructor(initialExtras: CreationExtras) {
21 | private val extras = AndroidXMutableCreationExtras(initialExtras)
22 |
23 | public actual operator fun set(key: Key, t: T) {
24 | extras[key] = t
25 | }
26 |
27 | public actual operator fun get(key: Key): T? = extras[key]
28 |
29 | public actual fun asCreationExtras(): CreationExtras = extras
30 | }
31 |
32 | /**
33 | * @see [ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY]
34 | */
35 | public actual val VIEW_MODEL_KEY: Key get() = ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY
36 |
--------------------------------------------------------------------------------
/viewmodel/src/androidMain/kotlin/com/hoc081098/kmp/viewmodel/MainThread.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | public actual typealias MainThread = androidx.annotation.MainThread
4 |
--------------------------------------------------------------------------------
/viewmodel/src/androidMain/kotlin/com/hoc081098/kmp/viewmodel/ViewModel.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel as AndroidXViewModel
4 | import androidx.lifecycle.viewModelScope as androidXViewModelScope
5 | import kotlinx.coroutines.CoroutineScope
6 |
7 | public actual abstract class ViewModel : AndroidXViewModel {
8 | public actual constructor() : super()
9 |
10 | @Suppress("SpreadOperator")
11 | public actual constructor(vararg closeables: Closeable) : super(*closeables)
12 |
13 | public actual val viewModelScope: CoroutineScope get() = androidXViewModelScope
14 |
15 | protected actual override fun onCleared(): Unit = super.onCleared()
16 |
17 | @Suppress("OVERRIDE_DEPRECATION")
18 | actual override fun addCloseable(closeable: Closeable): Unit = super.addCloseable(closeable)
19 | }
20 |
--------------------------------------------------------------------------------
/viewmodel/src/androidMain/kotlin/com/hoc081098/kmp/viewmodel/ViewModelFactory.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import androidx.lifecycle.ViewModelProvider
4 | import kotlin.reflect.KClass
5 |
6 | public fun ViewModelFactory.toAndroidX(): ViewModelProvider.Factory =
7 | object : ViewModelProvider.Factory {
8 | override fun create(modelClass: KClass, extras: CreationExtras): T {
9 | @Suppress("UNCHECKED_CAST")
10 | return this@toAndroidX.create(extras) as T
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/viewmodel/src/androidMain/kotlin/com/hoc081098/kmp/viewmodel/ViewModelStore.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import androidx.lifecycle.ViewModelStore as AndroidXViewModelStore
4 |
5 | @OptIn(InternalKmpViewModelApi::class)
6 | public fun ViewModelStore.toAndroidX(): AndroidXViewModelStore = this.platform
7 |
8 | @OptIn(InternalKmpViewModelApi::class)
9 | public fun AndroidXViewModelStore.toKmp(): ViewModelStore = ViewModelStore(this)
10 |
11 | public actual open class ViewModelStore
12 | @InternalKmpViewModelApi public constructor(
13 | @InternalKmpViewModelApi
14 | @JvmField public val platform: AndroidXViewModelStore,
15 | ) {
16 |
17 | /**
18 | * @hide
19 | */
20 | @Suppress("RestrictedApi")
21 | @InternalKmpViewModelApi
22 | public actual fun put(key: String, viewModel: ViewModel) {
23 | platform.put(key, viewModel)
24 | }
25 |
26 | /**
27 | * @hide
28 | */
29 | @Suppress("RestrictedApi")
30 | @InternalKmpViewModelApi
31 | public actual operator fun get(key: String): ViewModel? {
32 | return platform[key] as ViewModel?
33 | }
34 |
35 | /**
36 | * @hide
37 | */
38 | @Suppress("RestrictedApi")
39 | @InternalKmpViewModelApi
40 | public actual fun keys(): Set {
41 | return platform.keys()
42 | }
43 |
44 | @OptIn(InternalKmpViewModelApi::class)
45 | public actual fun clear() {
46 | platform.clear()
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/viewmodel/src/androidMain/kotlin/com/hoc081098/kmp/viewmodel/ViewModelStoreOwner.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import androidx.lifecycle.ViewModelStore as AndroidXViewModelStore
4 | import androidx.lifecycle.ViewModelStoreOwner as AndroidXViewModelStoreOwner
5 |
6 | private class AndroidXToKmpViewModelStoreOwner(
7 | @JvmField val platform: AndroidXViewModelStoreOwner,
8 | ) : ViewModelStoreOwner {
9 | override val viewModelStore: ViewModelStore = platform.viewModelStore.toKmp()
10 |
11 | override fun toString(): String = "AndroidXToKmpViewModelStoreOwner(platform=$platform)"
12 | }
13 |
14 | private class KmpToAndroidXViewModelStoreOwner(
15 | @JvmField val kmp: ViewModelStoreOwner,
16 | ) : AndroidXViewModelStoreOwner {
17 | override val viewModelStore: AndroidXViewModelStore = kmp.viewModelStore.toAndroidX()
18 |
19 | override fun toString(): String = "KmpToAndroidXViewModelStoreOwner(kmp=$kmp)"
20 | }
21 |
22 | public fun AndroidXViewModelStoreOwner.toKmp(): ViewModelStoreOwner =
23 | AndroidXToKmpViewModelStoreOwner(this)
24 |
25 | public fun ViewModelStoreOwner.toAndroidX(): AndroidXViewModelStoreOwner {
26 | if (this is AndroidXToKmpViewModelStoreOwner) {
27 | return platform
28 | }
29 | return KmpToAndroidXViewModelStoreOwner(this)
30 | }
31 |
--------------------------------------------------------------------------------
/viewmodel/src/androidUnitTest/kotlin/com/hoc081098/kmp/viewmodel/ViewModelStoreTest.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import androidx.lifecycle.ViewModelStore as AndroidXViewModelStore
4 |
5 | @OptIn(InternalKmpViewModelApi::class)
6 | actual fun createViewModelStore(): ViewModelStore = ViewModelStore(AndroidXViewModelStore())
7 |
--------------------------------------------------------------------------------
/viewmodel/src/androidUnitTest/kotlin/com/hoc081098/kmp/viewmodel/utils/TestAtomicBoolean.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.utils
2 |
3 | import java.util.concurrent.atomic.AtomicBoolean
4 |
5 | internal actual class TestAtomicBoolean actual constructor(value: Boolean) {
6 | private val ref = AtomicBoolean(value)
7 |
8 | actual var value: Boolean
9 | get() = ref.get()
10 | set(value) = ref.set(value)
11 | }
12 |
--------------------------------------------------------------------------------
/viewmodel/src/androidUnitTest/kotlin/com/hoc081098/kmp/viewmodel/utils/runBlockInNewThread.android.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.utils
2 |
3 | import java.util.concurrent.Executors
4 | import kotlinx.coroutines.suspendCancellableCoroutine
5 |
6 | actual suspend fun runBlockInNewThread(block: () -> Unit) = suspendCancellableCoroutine { cont ->
7 | val executor = Executors.newSingleThreadExecutor()
8 |
9 | cont.invokeOnCancellation { executor.shutdown() }
10 |
11 | executor.execute {
12 | if (cont.isActive) {
13 | cont.resumeWith(runCatching(block))
14 | executor.shutdown()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/Closeable.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | public expect fun interface Closeable {
4 | public fun close()
5 | }
6 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/InternalKmpViewModelApi.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | /**
4 | * Code marked with [InternalKmpViewModelApi] has no guarantees about API stability and can be changed
5 | * at any time.
6 | */
7 | @MustBeDocumented
8 | @Target(
9 | AnnotationTarget.ANNOTATION_CLASS,
10 | AnnotationTarget.PROPERTY,
11 | AnnotationTarget.CONSTRUCTOR,
12 | AnnotationTarget.FUNCTION,
13 | AnnotationTarget.CLASS,
14 | )
15 | @RequiresOptIn
16 | @Retention(AnnotationRetention.BINARY)
17 | public annotation class InternalKmpViewModelApi
18 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/MainThread.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.hoc081098.kmp.viewmodel
17 |
18 | /**
19 | * Denotes that the annotated method should only be called on the main thread. If the annotated
20 | * element is a class, then all methods in the class should be called on the main thread.
21 | *
22 | * Example:
23 | * ```
24 | * @MainThread
25 | * public void deliverResult(D data) { ... }
26 | * ```
27 | *
28 | * **Note:** Ordinarily, an app's main thread is also the UI thread. However, under special
29 | * circumstances, an app's main thread might not be its UI thread; for more information, see
30 | * [Thread annotations](https://developer.android.com/studio/write/annotations#thread-annotations).
31 | */
32 | @MustBeDocumented
33 | @Retention(AnnotationRetention.BINARY)
34 | @Target(
35 | AnnotationTarget.FUNCTION,
36 | AnnotationTarget.PROPERTY_GETTER,
37 | AnnotationTarget.PROPERTY_SETTER,
38 | AnnotationTarget.CONSTRUCTOR,
39 | AnnotationTarget.ANNOTATION_CLASS,
40 | AnnotationTarget.CLASS,
41 | AnnotationTarget.VALUE_PARAMETER,
42 | )
43 | public expect annotation class MainThread()
44 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/ViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import kotlin.reflect.KClass
4 |
5 | /**
6 | * Implementations of `Factory` interface are responsible to instantiate [ViewModel]s.
7 | */
8 | public interface ViewModelFactory {
9 | public val viewModelClass: KClass
10 |
11 | /**
12 | * Creates a new instance of [VM].
13 | *
14 | * @param extras an additional information for this creation request
15 | * @return a newly created ViewModel
16 | */
17 | public fun create(extras: CreationExtras): VM
18 | }
19 |
20 | /**
21 | * Creates a [ViewModelFactory] that returns the result of invoking [builder].
22 | */
23 | public inline fun viewModelFactory(
24 | crossinline builder: CreationExtras.() -> VM,
25 | ): ViewModelFactory = object : ViewModelFactory {
26 | override val viewModelClass: KClass = VM::class
27 | override fun create(extras: CreationExtras): VM = builder(extras)
28 | }
29 |
30 | /**
31 | * Creates a [ViewModelFactory] that returns the result of invoking [builder].
32 | */
33 | public inline fun viewModelFactory(
34 | viewModelClass: KClass,
35 | crossinline builder: CreationExtras.() -> VM,
36 | ): ViewModelFactory = object : ViewModelFactory {
37 | override val viewModelClass: KClass = viewModelClass
38 | override fun create(extras: CreationExtras): VM = builder(extras)
39 | }
40 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/ViewModelStoreOwner.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2017 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.hoc081098.kmp.viewmodel
17 |
18 | /**
19 | * A scope that owns [ViewModelStore].
20 | *
21 | * A responsibility of an implementation of this interface is to retain owned ViewModelStore
22 | * during the configuration changes and call [ViewModelStore.clear], when this scope is
23 | * going to be destroyed.
24 | */
25 | public interface ViewModelStoreOwner {
26 |
27 | /**
28 | * The owned [ViewModelStore]
29 | */
30 | public val viewModelStore: ViewModelStore
31 | }
32 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/creationExtrasKtx.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | /**
4 | * Builds a [CreationExtras] by populating a [MutableCreationExtrasBuilder] using the given [builderAction].
5 | * [initialExtras] will be copied into the resulting [MutableCreationExtrasBuilder] first.
6 | * Then the [builderAction] will be applied to the [MutableCreationExtrasBuilder].
7 | *
8 | * @param initialExtras extras that will be filled into the resulting [MutableCreationExtrasBuilder] first.
9 | * @param builderAction a lambda which will be applied to the [MutableCreationExtrasBuilder].
10 | */
11 | public inline fun buildCreationExtras(
12 | initialExtras: CreationExtras = EmptyCreationExtras,
13 | builderAction: MutableCreationExtrasBuilder.() -> Unit,
14 | ): CreationExtras = MutableCreationExtrasBuilder(initialExtras)
15 | .apply(builderAction)
16 | .asCreationExtras()
17 |
18 | /**
19 | * Edits this [CreationExtras] by populating a [MutableCreationExtrasBuilder] using the given [builderAction].
20 | * Content of this [CreationExtras] will be copied into the resulting [MutableCreationExtrasBuilder] first.
21 | * Then the [builderAction] will be applied to the [MutableCreationExtrasBuilder].
22 | *
23 | * @see buildCreationExtras
24 | */
25 | public inline fun CreationExtras.edit(
26 | builderAction: MutableCreationExtrasBuilder.() -> Unit,
27 | ): CreationExtras = buildCreationExtras(this, builderAction)
28 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/wrapper/AbstractFlowWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.wrapper
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | /**
7 | * A wrapper for [Flow] that provides a more convenient API for subscribing to the flow
8 | * when using Flow on native platforms.
9 | */
10 | // FIXME(overloads): https://youtrack.jetbrains.com/issue/KT-38685/Generate-overloaded-obj-c-functions-for-functions-with-default-parameter-values
11 | public abstract class AbstractFlowWrapper(private val flow: Flow) : Flow by flow {
12 | public open fun subscribe(
13 | scope: CoroutineScope,
14 | onValue: OnValue,
15 | onError: OnError,
16 | onComplete: OnComplete,
17 | ): JoinableAndCloseable = flow.subscribe(
18 | scope = scope,
19 | onValue = onValue,
20 | onError = onError,
21 | onComplete = onComplete,
22 | )
23 |
24 | public open fun subscribe(
25 | scope: CoroutineScope,
26 | onValue: OnValue,
27 | ): JoinableAndCloseable = flow.subscribe(
28 | scope = scope,
29 | onValue = onValue,
30 | onError = null,
31 | onComplete = null,
32 | )
33 |
34 | public open fun subscribe(
35 | scope: CoroutineScope,
36 | onValue: OnValue,
37 | onError: OnError,
38 | ): JoinableAndCloseable = flow.subscribe(
39 | scope = scope,
40 | onValue = onValue,
41 | onError = onError,
42 | onComplete = null,
43 | )
44 |
45 | public open fun subscribe(
46 | scope: CoroutineScope,
47 | onValue: OnValue,
48 | onComplete: OnComplete,
49 | ): JoinableAndCloseable = flow.subscribe(
50 | scope = scope,
51 | onValue = onValue,
52 | onError = null,
53 | onComplete = onComplete,
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/wrapper/Joinable.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("SpellCheckingInspection")
2 |
3 | package com.hoc081098.kmp.viewmodel.wrapper
4 |
5 | import com.hoc081098.kmp.viewmodel.Closeable
6 |
7 | /**
8 | * Has the same meaning as [kotlinx.coroutines.Job.join].
9 | */
10 | public interface Joinable {
11 | public suspend fun join()
12 | }
13 |
14 | public interface JoinableAndCloseable : Joinable, Closeable
15 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/wrapper/NonNullFlowWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.wrapper
2 |
3 | import kotlin.experimental.ExperimentalObjCName
4 | import kotlin.native.ObjCName
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | /**
9 | * A wrapper for [Flow] that provides a more convenient API for subscribing to the flow
10 | * when using Flow on native platforms.
11 | *
12 | * @param T the type of the flow's values, it is non-nullable.
13 | */
14 | @Suppress("RedundantOverride")
15 | // TODO: Override to make generated Objective-C code has the non-optional generic parameter.
16 | public open class NonNullFlowWrapper
17 | @OptIn(ExperimentalObjCName::class)
18 | constructor(
19 | @ObjCName("_")
20 | flow: Flow,
21 | ) : AbstractFlowWrapper(flow) {
22 | final override fun subscribe(
23 | scope: CoroutineScope,
24 | onValue: OnValue,
25 | onError: OnError,
26 | onComplete: OnComplete,
27 | ): JoinableAndCloseable = super.subscribe(scope, onValue, onError, onComplete)
28 |
29 | final override fun subscribe(
30 | scope: CoroutineScope,
31 | onValue: OnValue,
32 | ): JoinableAndCloseable = super.subscribe(scope, onValue)
33 |
34 | final override fun subscribe(
35 | scope: CoroutineScope,
36 | onValue: OnValue,
37 | onError: OnError,
38 | ): JoinableAndCloseable = super.subscribe(scope, onValue, onError)
39 |
40 | final override fun subscribe(
41 | scope: CoroutineScope,
42 | onValue: OnValue,
43 | onComplete: OnComplete,
44 | ): JoinableAndCloseable = super.subscribe(scope, onValue, onComplete)
45 | }
46 |
47 | public fun Flow.wrap(): NonNullFlowWrapper =
48 | this as? NonNullFlowWrapper ?: NonNullFlowWrapper(this)
49 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/wrapper/NonNullStateFlowWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.wrapper
2 |
3 | import kotlin.experimental.ExperimentalObjCName
4 | import kotlin.native.ObjCName
5 | import kotlinx.coroutines.flow.FlowCollector
6 | import kotlinx.coroutines.flow.StateFlow
7 |
8 | /**
9 | * Same as [NonNullFlowWrapper] but wraps and implements [StateFlow] and exposes the current value.
10 | */
11 | public class NonNullStateFlowWrapper
12 | @OptIn(ExperimentalObjCName::class)
13 | constructor(
14 | @ObjCName("_")
15 | private val flow: StateFlow,
16 | ) :
17 | NonNullFlowWrapper(flow),
18 | StateFlow by flow {
19 | override suspend fun collect(collector: FlowCollector): Nothing = flow.collect(collector)
20 | }
21 |
22 | public fun StateFlow.wrap(): NonNullStateFlowWrapper =
23 | this as? NonNullStateFlowWrapper ?: NonNullStateFlowWrapper(this)
24 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/wrapper/NullableFlowWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.wrapper
2 |
3 | import kotlin.experimental.ExperimentalObjCName
4 | import kotlin.native.ObjCName
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | /**
8 | * A wrapper for [Flow] that provides a more convenient API for subscribing to the flow
9 | * when using Flow on native platforms.
10 | *
11 | * @param T the type of the flow's values, it is nullable.
12 | */
13 | public open class NullableFlowWrapper
14 | @OptIn(ExperimentalObjCName::class)
15 | constructor(
16 | @ObjCName("_")
17 | flow: Flow,
18 | ) : AbstractFlowWrapper(flow)
19 |
20 | public fun Flow.wrap(): NullableFlowWrapper =
21 | this as? NullableFlowWrapper ?: NullableFlowWrapper(this)
22 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/wrapper/NullableStateFlowWrapper.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel.wrapper
2 |
3 | import kotlin.experimental.ExperimentalObjCName
4 | import kotlin.native.ObjCName
5 | import kotlinx.coroutines.flow.FlowCollector
6 | import kotlinx.coroutines.flow.StateFlow
7 |
8 | /**
9 | * Same as [NullableFlowWrapper] but wraps and implements [StateFlow] and exposes the current value.
10 | */
11 | public class NullableStateFlowWrapper
12 | @OptIn(ExperimentalObjCName::class)
13 | constructor(
14 | @ObjCName("_")
15 | private val flow: StateFlow,
16 | ) :
17 | NullableFlowWrapper(flow),
18 | StateFlow by flow {
19 | override suspend fun collect(collector: FlowCollector): Nothing = flow.collect(collector)
20 | }
21 |
22 | public fun StateFlow.wrap(): NullableStateFlowWrapper =
23 | this as? NullableStateFlowWrapper ?: NullableStateFlowWrapper(this)
24 |
--------------------------------------------------------------------------------
/viewmodel/src/commonMain/kotlin/com/hoc081098/kmp/viewmodel/wrapper/flow.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("SpellCheckingInspection")
2 |
3 | package com.hoc081098.kmp.viewmodel.wrapper
4 |
5 | import kotlinx.coroutines.CoroutineScope
6 | import kotlinx.coroutines.Job
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.catch
9 | import kotlinx.coroutines.flow.launchIn
10 | import kotlinx.coroutines.flow.onCompletion
11 | import kotlinx.coroutines.flow.onEach
12 |
13 | internal typealias OnValue = (value: T) -> Unit
14 | internal typealias OnError = (throwable: Throwable) -> Unit
15 | internal typealias OnComplete = () -> Unit
16 |
17 | /**
18 | * RxJava-like subscribe for Kotlin Flow.
19 | */
20 | internal fun Flow.subscribe(
21 | scope: CoroutineScope,
22 | onValue: OnValue,
23 | onError: OnError?,
24 | onComplete: OnComplete?,
25 | ): JoinableAndCloseable {
26 | val job = this
27 | .onEach(onValue)
28 | .run {
29 | if (onComplete !== null) {
30 | onCompletion { if (it === null) onComplete() else throw it }
31 | } else {
32 | this
33 | }
34 | }
35 | .run {
36 | if (onError !== null) {
37 | catch { onError(it) }
38 | } else {
39 | this
40 | }
41 | }
42 | .launchIn(scope)
43 |
44 | return JoinableAndCloseableJobImpl(job)
45 | }
46 |
47 | private class JoinableAndCloseableJobImpl(private val job: Job) : JoinableAndCloseable {
48 | override fun close() = job.cancel()
49 | override suspend fun join() = job.join()
50 | }
51 |
--------------------------------------------------------------------------------
/viewmodel/src/commonTest/kotlin/com/hoc081098/kmp/viewmodel/CreationExtrasTest.kt:
--------------------------------------------------------------------------------
1 | package com.hoc081098.kmp.viewmodel
2 |
3 | import kotlin.test.Test
4 | import kotlin.test.assertEquals
5 |
6 | // Copied and edit from https://github.com/androidx/androidx/blob/37df1f7745e04a9c7e2a7fb60f7449491276916f/lifecycle/lifecycle-viewmodel/src/androidTest/java/androidx/lifecycle/CreationExtrasTest.kt#L31
7 | class CreationExtrasTest {
8 | @Test
9 | fun testInitialCreationExtras() {
10 | val initial = MutableCreationExtrasBuilder()
11 | val key = object : CreationExtrasKey