├── lifecycle ├── .gitignore ├── src │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── lifecycle │ │ │ └── AndroidExt.kt │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── lifecycle │ │ │ ├── LifecycleOwner.kt │ │ │ ├── LifecycleRegistry.kt │ │ │ ├── Lifecycle.kt │ │ │ ├── LifecycleRegistryExt.kt │ │ │ ├── LifecycleRegistryImpl.kt │ │ │ └── LifecycleExt.kt │ ├── itvosTest │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── lifecycle │ │ │ └── ApplicationLifecyclePlatformTest.kt │ ├── commonTest │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── lifecycle │ │ │ ├── LifecycleRegistryTest.kt │ │ │ └── LifecycleExtTest.kt │ └── itvosMain │ │ └── kotlin │ │ └── com │ │ └── arkivanov │ │ └── essenty │ │ └── lifecycle │ │ └── ApplicationLifecycle.kt └── build.gradle.kts ├── back-handler ├── .gitignore ├── src │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── backhandler │ │ │ └── AndroidBackHandler.kt │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── backhandler │ │ │ ├── Utils.kt │ │ │ ├── BackHandlerOwner.kt │ │ │ ├── BackHandler.kt │ │ │ ├── BackEvent.kt │ │ │ ├── BackDispatcher.kt │ │ │ ├── BackCallback.kt │ │ │ └── DefaultBackDispatcher.kt │ └── androidUnitTest │ │ └── kotlin │ │ └── com │ │ └── arkivanov │ │ └── essenty │ │ └── backhandler │ │ └── AndroidBackHandlerWithLifecycleTest.kt ├── build.gradle.kts └── api │ ├── jvm │ └── back-handler.api │ └── android │ └── back-handler.api ├── instance-keeper ├── .gitignore ├── src │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── instancekeeper │ │ │ └── AndroidExt.kt │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── instancekeeper │ │ │ ├── InstanceKeeperOwner.kt │ │ │ ├── ExperimentalInstanceKeeperApi.kt │ │ │ ├── InstanceKeeperDispatcher.kt │ │ │ ├── DefaultInstanceKeeperDispatcher.kt │ │ │ └── InstanceKeeper.kt │ ├── androidUnitTest │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── instancekeeper │ │ │ └── AndroidInstanceKeeperTest.kt │ └── commonTest │ │ └── kotlin │ │ └── com │ │ └── arkivanov │ │ └── essenty │ │ └── instancekeeper │ │ └── InstanceKeeperExtTest.kt ├── build.gradle.kts └── api │ ├── jvm │ └── instance-keeper.api │ └── android │ └── instance-keeper.api ├── state-keeper ├── .gitignore ├── src │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── statekeeper │ │ │ ├── PersistableBundleExt.kt │ │ │ ├── BundleExt.kt │ │ │ └── AndroidExt.kt │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── statekeeper │ │ │ ├── StateKeeperOwner.kt │ │ │ ├── base64 │ │ │ ├── README.md │ │ │ ├── Dictionaries.kt │ │ │ ├── Encoder.kt │ │ │ └── Decoder.kt │ │ │ ├── ExperimentalStateKeeperApi.kt │ │ │ ├── Utils.kt │ │ │ ├── StateKeeperDispatcher.kt │ │ │ ├── StateKeeper.kt │ │ │ ├── DefaultStateKeeperDispatcher.kt │ │ │ ├── SerializableContainer.kt │ │ │ └── PolymorphicSerializer.kt │ ├── commonTest │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── statekeeper │ │ │ ├── base64 │ │ │ ├── README.md │ │ │ └── Base64ImplTest.kt │ │ │ ├── CodingTest.kt │ │ │ ├── TestUtils.kt │ │ │ ├── PolymorphicSerializerTest.kt │ │ │ ├── StateKeeperExtTest.kt │ │ │ ├── DefaultStateKeeperDispatcherTest.kt │ │ │ └── SerializableContainerTest.kt │ ├── androidUnitTest │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── statekeeper │ │ │ ├── TestUtils.android.kt │ │ │ ├── BundleExtTest.kt │ │ │ └── AndroidStateKeeperTest.kt │ ├── nonJavaMain │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── statekeeper │ │ │ └── Utils.kt │ ├── jsTest │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── statekeeper │ │ │ └── DefaultStateKeeperDispatcherJsTest.kt │ └── javaMain │ │ └── kotlin │ │ └── com │ │ └── arkivanov │ │ └── essenty │ │ └── statekeeper │ │ └── Utils.java.kt ├── build.gradle.kts └── api │ └── jvm │ └── state-keeper.api ├── utils-internal ├── .gitignore ├── src │ ├── androidMain │ │ └── AndroidManifest.xml │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── arkivanov │ │ └── essenty │ │ └── utils │ │ └── internal │ │ ├── InternalEssentyApi.kt │ │ └── ExperimentalEssentyApi.kt └── build.gradle.kts ├── lifecycle-coroutines ├── .gitignore ├── src │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── arkivanov │ │ │ └── essenty │ │ │ └── lifecycle │ │ │ └── coroutines │ │ │ ├── DispatchersExt.kt │ │ │ ├── CoroutineScopeWithLifecycle.kt │ │ │ ├── FlowWithLifecycle.kt │ │ │ └── RepeatOnLifecycle.kt │ └── commonTest │ │ └── kotlin │ │ └── com │ │ └── arkivanov │ │ └── essenty │ │ └── lifecycle │ │ └── coroutines │ │ ├── DispatchersExtTest.kt │ │ ├── CoroutineScopeWithLifecycleTest.kt │ │ └── LifecycleCoroutinesExtTest.kt ├── build.gradle.kts └── api │ ├── jvm │ └── lifecycle-coroutines.api │ ├── android │ └── lifecycle-coroutines.api │ └── lifecycle-coroutines.klib.api ├── lifecycle-reaktive ├── .gitignore ├── api │ ├── android │ │ └── lifecycle-reaktive.api │ ├── jvm │ │ └── lifecycle-reaktive.api │ └── lifecycle-reaktive.klib.api ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── arkivanov │ │ └── essenty │ │ └── lifecycle │ │ └── reaktive │ │ └── DisposableWithLifecycle.kt │ └── commonTest │ └── kotlin │ └── com │ └── arkivanov │ └── essenty │ └── lifecycle │ └── reaktive │ └── DisposableWithLifecycleTest.kt ├── state-keeper-benchmarks ├── .gitignore ├── src │ ├── main │ │ └── res │ │ │ └── AndroidManifest.xml │ └── test │ │ └── kotlin │ │ └── com │ │ └── arkivanov │ │ └── essenty │ │ └── statekeeper │ │ └── benchmarks │ │ └── Benchmarks.kt └── build.gradle.kts ├── .gitignore ├── tools └── check-publication │ ├── .gitignore │ ├── src │ ├── androidMain │ │ └── AndroidManifest.xml │ └── commonMain │ │ └── kotlin │ │ └── com │ │ └── arkivanov │ │ └── essenty │ │ └── tools │ │ └── checkpublication │ │ └── Dummy.kt │ └── build.gradle.kts ├── docs └── media │ └── LifecycleStates.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github ├── FUNDING.yml └── workflows │ ├── build.yml │ └── publish.yml ├── gradle.properties ├── detekt.yml ├── settings.gradle.kts ├── deps.versions.toml ├── gradlew.bat └── .editorconfig /lifecycle/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /back-handler/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /instance-keeper/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /state-keeper/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /utils-internal/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /lifecycle-coroutines/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /lifecycle-reaktive/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /state-keeper-benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | local.properties 4 | .idea 5 | /build 6 | .DS_Store 7 | .kotlin 8 | -------------------------------------------------------------------------------- /tools/check-publication/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | .idea 5 | /build 6 | -------------------------------------------------------------------------------- /back-handler/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /lifecycle/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /state-keeper/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/media/LifecycleStates.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arkivanov/Essenty/HEAD/docs/media/LifecycleStates.png -------------------------------------------------------------------------------- /instance-keeper/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /utils-internal/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /state-keeper-benchmarks/src/main/res/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /tools/check-publication/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arkivanov/Essenty/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: arkivanov 4 | custom: ["https://www.buymeacoffee.com/arkivanov"] 5 | -------------------------------------------------------------------------------- /tools/check-publication/src/commonMain/kotlin/com/arkivanov/essenty/tools/checkpublication/Dummy.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.tools.checkpublication 2 | 3 | fun dummy() { 4 | // no-op 5 | } 6 | -------------------------------------------------------------------------------- /lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleOwner.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle 2 | 3 | /** 4 | * Represents a holder of [Lifecycle]. 5 | */ 6 | interface LifecycleOwner { 7 | 8 | val lifecycle: Lifecycle 9 | } 10 | -------------------------------------------------------------------------------- /back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.backhandler 2 | 3 | internal fun Iterable.findMostImportant(): BackCallback? = 4 | sortedBy(BackCallback::priority).lastOrNull(BackCallback::isEnabled) 5 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperOwner.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | /** 4 | * Represents a holder of [StateKeeper]. 5 | */ 6 | interface StateKeeperOwner { 7 | 8 | val stateKeeper: StateKeeper 9 | } 10 | -------------------------------------------------------------------------------- /instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeperOwner.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.instancekeeper 2 | 3 | /** 4 | * Represents a holder of [InstanceKeeper]. 5 | */ 6 | interface InstanceKeeperOwner { 7 | 8 | val instanceKeeper: InstanceKeeper 9 | } 10 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/base64/README.md: -------------------------------------------------------------------------------- 1 | The content of this package was copied from https://github.com/cy6erGn0m/kotlinx.serialization/tree/cy/base64/formats/base64/commonTest/src/kotlinx/serialization/base64. 2 | 3 | Waiting for https://github.com/Kotlin/kotlinx.serialization/issues/1633. 4 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/README.md: -------------------------------------------------------------------------------- 1 | The content of this package was copied from https://github.com/cy6erGn0m/kotlinx.serialization/tree/cy/base64/formats/base64/commonMain/src/kotlinx.serialization.base64/impl. 2 | 3 | Waiting for https://github.com/Kotlin/kotlinx.serialization/issues/1633. 4 | -------------------------------------------------------------------------------- /back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackHandlerOwner.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.backhandler 2 | 3 | /** 4 | * Represents a holder of [BackHandler]. It may be implemented by an arbitrary class, to provide convenient API. 5 | */ 6 | interface BackHandlerOwner { 7 | 8 | val backHandler: BackHandler 9 | } 10 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/Dictionaries.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper.base64 2 | 3 | internal val dictionary: CharArray = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray() 4 | 5 | internal val backDictionary: IntArray = IntArray(0x80) { code -> 6 | dictionary.indexOf(code.toChar()) 7 | } 8 | -------------------------------------------------------------------------------- /utils-internal/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.arkivanov.gradle.setupMultiplatform 2 | import com.arkivanov.gradle.setupPublication 3 | 4 | plugins { 5 | id("kotlin-multiplatform") 6 | id("com.android.library") 7 | id("com.arkivanov.gradle.setup") 8 | } 9 | 10 | setupMultiplatform() 11 | setupPublication() 12 | 13 | android { 14 | namespace = "com.arkivanov.essenty.utils.internal" 15 | } 16 | -------------------------------------------------------------------------------- /utils-internal/src/commonMain/kotlin/com/arkivanov/essenty/utils/internal/InternalEssentyApi.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.utils.internal 2 | 3 | @RequiresOptIn(message = "This API is internal, please don't use it.", level = RequiresOptIn.Level.ERROR) 4 | @Retention(AnnotationRetention.BINARY) 5 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY) 6 | annotation class InternalEssentyApi 7 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | org.gradle.jvmargs=-Xmx2g 3 | org.gradle.parallel=true 4 | org.gradle.caching=true 5 | systemProp.org.gradle.internal.publish.checksums.insecure=true 6 | android.useAndroidX=true 7 | android.enableJetifier=true 8 | kotlin.mpp.androidSourceSetLayoutVersion=2 9 | kotlin.mpp.applyDefaultHierarchyTemplate=false 10 | 11 | # For compatibility with Kotlin 1.9.0 12 | android.experimental.lint.version=8.1.0 13 | -------------------------------------------------------------------------------- /lifecycle-reaktive/api/android/lifecycle-reaktive.api: -------------------------------------------------------------------------------- 1 | public final class com/arkivanov/essenty/lifecycle/reaktive/DisposableWithLifecycleKt { 2 | public static final fun disposableScope (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;)Lcom/badoo/reaktive/disposable/scope/DisposableScope; 3 | public static final fun withLifecycle (Lcom/badoo/reaktive/disposable/Disposable;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Lcom/badoo/reaktive/disposable/Disposable; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /lifecycle-reaktive/api/jvm/lifecycle-reaktive.api: -------------------------------------------------------------------------------- 1 | public final class com/arkivanov/essenty/lifecycle/reaktive/DisposableWithLifecycleKt { 2 | public static final fun disposableScope (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;)Lcom/badoo/reaktive/disposable/scope/DisposableScope; 3 | public static final fun withLifecycle (Lcom/badoo/reaktive/disposable/Disposable;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Lcom/badoo/reaktive/disposable/Disposable; 4 | } 5 | 6 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/ExperimentalStateKeeperApi.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | /** 4 | * Marks experimental API in Essenty. An experimental API can be changed or removed at any time. 5 | */ 6 | @RequiresOptIn(level = RequiresOptIn.Level.WARNING) 7 | @Retention(AnnotationRetention.BINARY) 8 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 9 | annotation class ExperimentalStateKeeperApi 10 | -------------------------------------------------------------------------------- /utils-internal/src/commonMain/kotlin/com/arkivanov/essenty/utils/internal/ExperimentalEssentyApi.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.utils.internal 2 | 3 | /** 4 | * Marks experimental API in Essenty. An experimental API can be changed or removed at any time. 5 | */ 6 | @RequiresOptIn(level = RequiresOptIn.Level.WARNING) 7 | @Retention(AnnotationRetention.BINARY) 8 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 9 | annotation class ExperimentalEssentyApi 10 | -------------------------------------------------------------------------------- /instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/ExperimentalInstanceKeeperApi.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.instancekeeper 2 | 3 | /** 4 | * Marks experimental API in Essenty. An experimental API can be changed or removed at any time. 5 | */ 6 | @RequiresOptIn(level = RequiresOptIn.Level.WARNING) 7 | @Retention(AnnotationRetention.BINARY) 8 | @Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION) 9 | annotation class ExperimentalInstanceKeeperApi 10 | -------------------------------------------------------------------------------- /state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/CodingTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class CodingTest { 7 | 8 | @Test 9 | fun serializes_and_deserializes() { 10 | val data = SerializableData() 11 | 12 | val newData = data.serialize(SerializableData.serializer()).deserialize(SerializableData.serializer()) 13 | 14 | assertEquals(data, newData) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlinx.serialization.KSerializer 4 | 5 | internal fun T.serializeAndDeserialize(serializer: KSerializer): T = 6 | serialize(strategy = serializer) 7 | .deserialize(strategy = serializer) 8 | 9 | internal fun SerializableContainer.serializeAndDeserialize(): SerializableContainer = 10 | serializeAndDeserialize(SerializableContainer.serializer()) 11 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlinx.serialization.DeserializationStrategy 4 | import kotlinx.serialization.SerializationStrategy 5 | import kotlinx.serialization.json.Json 6 | 7 | internal val essentyJson: Json = 8 | Json { 9 | allowStructuredMapKeys = true 10 | } 11 | 12 | internal expect fun T.serialize(strategy: SerializationStrategy): ByteArray 13 | 14 | internal expect fun ByteArray.deserialize(strategy: DeserializationStrategy): T 15 | -------------------------------------------------------------------------------- /state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/TestUtils.android.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import android.os.Bundle 4 | import android.os.Parcel 5 | 6 | internal fun Bundle.parcelize(): ByteArray { 7 | val parcel = Parcel.obtain() 8 | parcel.writeBundle(this) 9 | return parcel.marshall() 10 | } 11 | 12 | internal fun ByteArray.deparcelize(): Bundle { 13 | val parcel = Parcel.obtain() 14 | parcel.unmarshall(this, 0, size) 15 | parcel.setDataPosition(0) 16 | 17 | return requireNotNull(parcel.readBundle()) 18 | } 19 | -------------------------------------------------------------------------------- /state-keeper/src/nonJavaMain/kotlin/com/arkivanov/essenty/statekeeper/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlinx.serialization.DeserializationStrategy 4 | import kotlinx.serialization.SerializationStrategy 5 | 6 | internal actual fun T.serialize(strategy: SerializationStrategy): ByteArray = 7 | essentyJson.encodeToString(serializer = strategy, value = this).encodeToByteArray() 8 | 9 | internal actual fun ByteArray.deserialize(strategy: DeserializationStrategy): T = 10 | essentyJson.decodeFromString(deserializer = strategy, string = decodeToString()) 11 | -------------------------------------------------------------------------------- /back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackHandler.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.backhandler 2 | 3 | /** 4 | * A handler for back button presses. 5 | */ 6 | interface BackHandler { 7 | 8 | /** 9 | * Checks whether the provided [BackCallback] is registered or not. 10 | */ 11 | fun isRegistered(callback: BackCallback): Boolean 12 | 13 | /** 14 | * Registers the specified [callback] to be called when the back button is invoked. 15 | */ 16 | fun register(callback: BackCallback) 17 | 18 | /** 19 | * Unregisters the specified [callback]. 20 | */ 21 | fun unregister(callback: BackCallback) 22 | } 23 | -------------------------------------------------------------------------------- /state-keeper-benchmarks/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.arkivanov.gradle.setupAndroidLibrary 2 | 3 | plugins { 4 | id("kotlin-android") 5 | id("com.android.library") 6 | id("kotlinx-serialization") 7 | id("kotlin-parcelize") 8 | id("com.arkivanov.gradle.setup") 9 | } 10 | 11 | setupAndroidLibrary() 12 | 13 | android { 14 | namespace = "com.arkivanov.essenty.statekeeper.benchmarks" 15 | testOptions.unitTests.isIncludeAndroidResources = true 16 | } 17 | 18 | dependencies { 19 | implementation(project(":state-keeper")) 20 | implementation(deps.jetbrains.kotlinx.kotlinxSerializationJson) 21 | testImplementation(kotlin("test")) 22 | testImplementation(deps.robolectric.robolectric) 23 | } 24 | -------------------------------------------------------------------------------- /state-keeper/src/jsTest/kotlin/com/arkivanov/essenty/statekeeper/DefaultStateKeeperDispatcherJsTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertTrue 5 | 6 | @Suppress("TestFunctionName") 7 | class DefaultStateKeeperDispatcherJsTest { 8 | 9 | // Verifies the workaround for https://youtrack.jetbrains.com/issue/KT-49186 10 | @Test 11 | fun WHEN_save_THEN_returns_SerializableContainer() { 12 | val stateKeeper = DefaultStateKeeperDispatcher(null) 13 | 14 | val serializableContainer = stateKeeper.save() 15 | 16 | @Suppress("USELESS_IS_CHECK") 17 | assertTrue(serializableContainer is SerializableContainer) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.instancekeeper 2 | 3 | import kotlin.js.JsName 4 | 5 | /** 6 | * Represents a destroyable [InstanceKeeper]. 7 | */ 8 | interface InstanceKeeperDispatcher : InstanceKeeper { 9 | 10 | /** 11 | * Destroys all existing instances. Instances are not cleared, so that they can be 12 | * accessed later. Any new instances will be immediately destroyed. 13 | */ 14 | fun destroy() 15 | } 16 | 17 | /** 18 | * Creates a default implementation of [InstanceKeeperDispatcher]. 19 | */ 20 | @JsName("instanceKeeperDispatcher") 21 | fun InstanceKeeperDispatcher(): InstanceKeeperDispatcher = DefaultInstanceKeeperDispatcher() 22 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlin.js.JsName 4 | 5 | /** 6 | * Represents a savable [StateKeeper]. 7 | */ 8 | interface StateKeeperDispatcher : StateKeeper { 9 | 10 | /** 11 | * Calls all registered `suppliers` and saves the data into a [SerializableContainer]. 12 | */ 13 | fun save(): SerializableContainer 14 | } 15 | 16 | /** 17 | * Creates a default implementation of [StateKeeperDispatcher] with the provided [savedState]. 18 | */ 19 | @JsName("stateKeeperDispatcher") 20 | fun StateKeeperDispatcher(savedState: SerializableContainer? = null): StateKeeperDispatcher = 21 | DefaultStateKeeperDispatcher(savedState) 22 | -------------------------------------------------------------------------------- /lifecycle-coroutines/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/coroutines/DispatchersExt.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle.coroutines 2 | 3 | import kotlinx.coroutines.MainCoroutineDispatcher 4 | import kotlin.concurrent.Volatile 5 | 6 | @Volatile 7 | private var isImmediateSupported: Boolean = true 8 | 9 | internal val MainCoroutineDispatcher.immediateOrFallback: MainCoroutineDispatcher 10 | get() { 11 | if (isImmediateSupported) { 12 | try { 13 | return immediate 14 | } catch (ignored: UnsupportedOperationException) { 15 | } catch (ignored: NotImplementedError) { 16 | } 17 | 18 | isImmediateSupported = false 19 | } 20 | 21 | return this 22 | } 23 | -------------------------------------------------------------------------------- /lifecycle-reaktive/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.arkivanov.gradle.setupBinaryCompatibilityValidator 2 | import com.arkivanov.gradle.setupMultiplatform 3 | import com.arkivanov.gradle.setupPublication 4 | import com.arkivanov.gradle.setupSourceSets 5 | 6 | plugins { 7 | id("kotlin-multiplatform") 8 | id("com.android.library") 9 | id("com.arkivanov.gradle.setup") 10 | } 11 | 12 | setupMultiplatform() 13 | setupPublication() 14 | setupBinaryCompatibilityValidator() 15 | 16 | android { 17 | namespace = "com.arkivanov.essenty.lifecycle.reaktive" 18 | } 19 | 20 | kotlin { 21 | setupSourceSets { 22 | common.main.dependencies { 23 | implementation(project(":lifecycle")) 24 | implementation(deps.reaktive.reaktive) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleRegistry.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle 2 | 3 | import kotlin.js.JsName 4 | 5 | /** 6 | * Represents [Lifecycle] and [Lifecycle.Callbacks] at the same time. 7 | * Can be used to manually control the [Lifecycle]. 8 | */ 9 | interface LifecycleRegistry : Lifecycle, Lifecycle.Callbacks 10 | 11 | /** 12 | * Creates a default implementation of [LifecycleRegistry]. 13 | */ 14 | @JsName("lifecycleRegistry") 15 | fun LifecycleRegistry(): LifecycleRegistry = LifecycleRegistry(initialState = Lifecycle.State.INITIALIZED) 16 | 17 | /** 18 | * Creates a default implementation of [LifecycleRegistry] with the specified [initialState]. 19 | */ 20 | fun LifecycleRegistry( 21 | initialState: Lifecycle.State, 22 | ): LifecycleRegistry = 23 | LifecycleRegistryImpl(initialState) 24 | -------------------------------------------------------------------------------- /lifecycle-coroutines/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.arkivanov.gradle.setupBinaryCompatibilityValidator 2 | import com.arkivanov.gradle.setupMultiplatform 3 | import com.arkivanov.gradle.setupPublication 4 | import com.arkivanov.gradle.setupSourceSets 5 | 6 | plugins { 7 | id("kotlin-multiplatform") 8 | id("com.android.library") 9 | id("com.arkivanov.gradle.setup") 10 | } 11 | 12 | setupMultiplatform() 13 | setupPublication() 14 | setupBinaryCompatibilityValidator() 15 | 16 | android { 17 | namespace = "com.arkivanov.essenty.lifecycle.coroutines" 18 | } 19 | 20 | kotlin { 21 | setupSourceSets { 22 | common.main.dependencies { 23 | implementation(project(":lifecycle")) 24 | implementation(deps.kotlinx.coroutinesCore) 25 | } 26 | 27 | common.test.dependencies { 28 | implementation(deps.kotlinx.coroutinesTest) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | complexity: 2 | CyclomaticComplexMethod: 3 | threshold: 15 4 | ignoreSingleWhenExpression: true 5 | ignoreSimpleWhenEntries: true 6 | ignoreNestingFunctions: false 7 | CognitiveComplexMethod: 8 | active: true 9 | threshold: 15 10 | LongParameterList: 11 | active: false 12 | TooManyFunctions: 13 | active: false 14 | 15 | exceptions: 16 | PrintStackTrace: 17 | active: false 18 | TooGenericExceptionCaught: 19 | active: false 20 | 21 | naming: 22 | FunctionNaming: 23 | excludes: ['**/test/**', '**/*Test/**'] 24 | ignoreAnnotated: [ 'Composable' ] 25 | MemberNameEqualsClassName: 26 | active: false 27 | 28 | style: 29 | ForbiddenComment: 30 | values: ['FIXME:', 'STOPSHIP:'] 31 | MagicNumber: 32 | active: false 33 | MaxLineLength: 34 | maxLineLength: 140 35 | excludes: ['**/test/**', '**/*Test/**'] 36 | ReturnCount: 37 | active: false 38 | -------------------------------------------------------------------------------- /back-handler/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.arkivanov.gradle.bundle 2 | import com.arkivanov.gradle.setupBinaryCompatibilityValidator 3 | import com.arkivanov.gradle.setupMultiplatform 4 | import com.arkivanov.gradle.setupPublication 5 | import com.arkivanov.gradle.setupSourceSets 6 | 7 | plugins { 8 | id("kotlin-multiplatform") 9 | id("com.android.library") 10 | id("com.arkivanov.gradle.setup") 11 | } 12 | 13 | setupMultiplatform() 14 | setupPublication() 15 | setupBinaryCompatibilityValidator() 16 | 17 | android { 18 | namespace = "com.arkivanov.essenty.backhandler" 19 | } 20 | 21 | kotlin { 22 | setupSourceSets { 23 | val android by bundle() 24 | 25 | common.main.dependencies { 26 | implementation(project(":utils-internal")) 27 | } 28 | 29 | android.main.dependencies { 30 | implementation(deps.androidx.activity.activityKtx) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /instance-keeper/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.arkivanov.gradle.bundle 2 | import com.arkivanov.gradle.setupBinaryCompatibilityValidator 3 | import com.arkivanov.gradle.setupMultiplatform 4 | import com.arkivanov.gradle.setupPublication 5 | import com.arkivanov.gradle.setupSourceSets 6 | 7 | plugins { 8 | id("kotlin-multiplatform") 9 | id("com.android.library") 10 | id("com.arkivanov.gradle.setup") 11 | } 12 | 13 | setupMultiplatform() 14 | setupPublication() 15 | setupBinaryCompatibilityValidator() 16 | 17 | android { 18 | namespace = "com.arkivanov.essenty.instancekeeper" 19 | } 20 | 21 | kotlin { 22 | setupSourceSets { 23 | val android by bundle() 24 | 25 | common.main.dependencies { 26 | implementation(project(":utils-internal")) 27 | } 28 | 29 | android.main.dependencies { 30 | implementation(deps.androidx.lifecycle.lifecycleViewmodelKtx) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackEvent.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.backhandler 2 | 3 | /** 4 | * Represents an event of the predictive back gesture. 5 | * 6 | * @param progress progress factor of the back gesture, must be between 0 and 1. 7 | * @param swipeEdge Indicates which edge the gesture is being performed from. 8 | * @param touchX absolute X location of the touch point of this event. 9 | * @param touchY absolute Y location of the touch point of this event. 10 | */ 11 | data class BackEvent( 12 | val progress: Float = 0F, 13 | val swipeEdge: SwipeEdge = SwipeEdge.UNKNOWN, 14 | val touchX: Float = 0F, 15 | val touchY: Float = 0F, 16 | ) { 17 | 18 | init { 19 | require(progress in 0F..1F) { "The 'progress' argument must be between 0 and 1 (both inclusive)" } 20 | } 21 | 22 | enum class SwipeEdge { 23 | UNKNOWN, 24 | LEFT, 25 | RIGHT, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lifecycle-reaktive/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/reaktive/DisposableWithLifecycle.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle.reaktive 2 | 3 | import com.arkivanov.essenty.lifecycle.Lifecycle 4 | import com.arkivanov.essenty.lifecycle.LifecycleOwner 5 | import com.arkivanov.essenty.lifecycle.doOnDestroy 6 | import com.badoo.reaktive.disposable.Disposable 7 | import com.badoo.reaktive.disposable.scope.DisposableScope 8 | 9 | /** 10 | * Creates and returns a new [DisposableScope], which is automatically 11 | * disposed when the [Lifecycle] is destroyed. 12 | */ 13 | fun LifecycleOwner.disposableScope(): DisposableScope = 14 | DisposableScope().withLifecycle(lifecycle) 15 | 16 | /** 17 | * Automatically disposes this [Disposable] when the specified [lifecycle] is destroyed. 18 | * 19 | * @return the same (this) [Disposable]. 20 | */ 21 | fun T.withLifecycle(lifecycle: Lifecycle): T { 22 | lifecycle.doOnDestroy(::dispose) 23 | 24 | return this 25 | } 26 | -------------------------------------------------------------------------------- /instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/DefaultInstanceKeeperDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.instancekeeper 2 | 3 | import com.arkivanov.essenty.instancekeeper.InstanceKeeper.Instance 4 | 5 | internal class DefaultInstanceKeeperDispatcher : InstanceKeeperDispatcher { 6 | 7 | private val map = HashMap() 8 | private var isDestroyed = false 9 | 10 | override fun get(key: Any): Instance? = 11 | map[key] 12 | 13 | override fun put(key: Any, instance: Instance) { 14 | check(key !in map) { "Another instance is already associated with the key: $key" } 15 | 16 | map[key] = instance 17 | 18 | if (isDestroyed) { 19 | instance.onDestroy() 20 | } 21 | } 22 | 23 | override fun remove(key: Any): Instance? = 24 | map.remove(key) 25 | 26 | override fun destroy() { 27 | if (!isDestroyed) { 28 | isDestroyed = true 29 | map.values.toList().forEach(Instance::onDestroy) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lifecycle-reaktive/api/lifecycle-reaktive.klib.api: -------------------------------------------------------------------------------- 1 | // Klib ABI Dump 2 | // Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] 3 | // Rendering settings: 4 | // - Signature version: 2 5 | // - Show manifest properties: true 6 | // - Show declarations: true 7 | 8 | // Library unique name: 9 | final fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle.reaktive/disposableScope(): com.badoo.reaktive.disposable.scope/DisposableScope // com.arkivanov.essenty.lifecycle.reaktive/disposableScope|disposableScope@com.arkivanov.essenty.lifecycle.LifecycleOwner(){}[0] 10 | final fun <#A: com.badoo.reaktive.disposable/Disposable> (#A).com.arkivanov.essenty.lifecycle.reaktive/withLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle): #A // com.arkivanov.essenty.lifecycle.reaktive/withLifecycle|withLifecycle@0:0(com.arkivanov.essenty.lifecycle.Lifecycle){0§}[0] 11 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | versionCatalogs { 3 | create("deps") { 4 | from(files("deps.versions.toml")) 5 | } 6 | } 7 | } 8 | 9 | pluginManagement { 10 | repositories { 11 | gradlePluginPortal() 12 | maven("https://jitpack.io") 13 | } 14 | 15 | resolutionStrategy { 16 | eachPlugin { 17 | if (requested.id.toString() == "com.arkivanov.gradle.setup") { 18 | useModule("com.github.arkivanov:gradle-setup-plugin:4ae41e7b6a") 19 | } 20 | } 21 | } 22 | 23 | plugins { 24 | id("com.arkivanov.gradle.setup") 25 | } 26 | } 27 | 28 | if (!startParameter.projectProperties.containsKey("check_publication")) { 29 | include(":utils-internal") 30 | include(":lifecycle") 31 | include(":lifecycle-coroutines") 32 | include(":lifecycle-reaktive") 33 | include(":state-keeper") 34 | include(":state-keeper-benchmarks") 35 | include(":instance-keeper") 36 | include(":back-handler") 37 | } else { 38 | include(":tools:check-publication") 39 | } 40 | -------------------------------------------------------------------------------- /lifecycle/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.arkivanov.gradle.bundle 2 | import com.arkivanov.gradle.dependsOn 3 | import com.arkivanov.gradle.setupBinaryCompatibilityValidator 4 | import com.arkivanov.gradle.setupMultiplatform 5 | import com.arkivanov.gradle.setupPublication 6 | import com.arkivanov.gradle.setupSourceSets 7 | 8 | plugins { 9 | id("kotlin-multiplatform") 10 | id("com.android.library") 11 | id("com.arkivanov.gradle.setup") 12 | } 13 | 14 | setupMultiplatform() 15 | setupPublication() 16 | setupBinaryCompatibilityValidator() 17 | 18 | android { 19 | namespace = "com.arkivanov.essenty.lifecycle" 20 | } 21 | 22 | kotlin { 23 | setupSourceSets { 24 | val android by bundle() 25 | val itvos by bundle() 26 | 27 | (iosSet + tvosSet) dependsOn itvos 28 | itvos dependsOn common 29 | 30 | common.main.dependencies { 31 | implementation(project(":utils-internal")) 32 | } 33 | 34 | android.main.dependencies { 35 | implementation(deps.androidx.lifecycle.lifecycleCommonJava8) 36 | implementation(deps.androidx.lifecycle.lifecycleRuntime) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - 'docs/**' 7 | 8 | jobs: 9 | linux-build: 10 | name: Build on Linux 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Install Java 16 | uses: actions/setup-java@v3 17 | with: 18 | distribution: 'zulu' 19 | java-version: 17 20 | - name: Update dependencies 21 | run: sudo apt-get update 22 | - name: Install dependencies 23 | run: sudo apt-get install nodejs chromium-browser 24 | - name: Build 25 | uses: gradle/gradle-build-action@v2 26 | with: 27 | arguments: build -Dsplit_targets 28 | macos-build: 29 | name: Build on macOS 30 | runs-on: macos-14 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | - name: Install Java 35 | uses: actions/setup-java@v3 36 | with: 37 | distribution: 'zulu' 38 | java-version: 17 39 | - name: Build project 40 | uses: gradle/gradle-build-action@v2 41 | with: 42 | arguments: build -Dsplit_targets 43 | -------------------------------------------------------------------------------- /tools/check-publication/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.arkivanov.gradle.setupMultiplatform 2 | import com.arkivanov.gradle.setupSourceSets 3 | 4 | plugins { 5 | id("kotlin-multiplatform") 6 | id("com.android.library") 7 | id("com.arkivanov.gradle.setup") 8 | } 9 | 10 | setupMultiplatform() 11 | 12 | repositories { 13 | maven("https://s01.oss.sonatype.org/content/groups/staging/") { 14 | credentials { 15 | username = "arkivanov" 16 | password = System.getenv("SONATYPE_PASSWORD") 17 | } 18 | } 19 | } 20 | 21 | android { 22 | namespace = "com.arkivanov.essenty.tools.checkpublication" 23 | } 24 | 25 | kotlin { 26 | setupSourceSets { 27 | common.main.dependencies { 28 | val version = deps.versions.essenty.get() 29 | implementation("com.arkivanov.essenty:back-handler:$version") 30 | implementation("com.arkivanov.essenty:instance-keeper:$version") 31 | implementation("com.arkivanov.essenty:lifecycle:$version") 32 | implementation("com.arkivanov.essenty:lifecycle-coroutines:$version") 33 | implementation("com.arkivanov.essenty:lifecycle-reaktive:$version") 34 | implementation("com.arkivanov.essenty:state-keeper:$version") 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lifecycle-coroutines/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/coroutines/CoroutineScopeWithLifecycle.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle.coroutines 2 | 3 | import com.arkivanov.essenty.lifecycle.Lifecycle 4 | import com.arkivanov.essenty.lifecycle.LifecycleOwner 5 | import com.arkivanov.essenty.lifecycle.doOnDestroy 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.Dispatchers 8 | import kotlinx.coroutines.cancel 9 | import kotlin.coroutines.CoroutineContext 10 | 11 | /** 12 | * Creates and returns a new [CoroutineScope] with the specified [context]. 13 | * The returned [CoroutineScope] is automatically cancelled when the [Lifecycle] is destroyed. 14 | * 15 | * @param context a [CoroutineContext] to be used for creating the [CoroutineScope], default 16 | * is [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] 17 | * if available on the current platform, or [Dispatchers.Main] otherwise. 18 | */ 19 | fun LifecycleOwner.coroutineScope( 20 | context: CoroutineContext = Dispatchers.Main.immediateOrFallback, 21 | ): CoroutineScope = 22 | CoroutineScope(context = context).withLifecycle(lifecycle) 23 | 24 | /** 25 | * Automatically cancels this [CoroutineScope] when the specified [lifecycle] is destroyed. 26 | * 27 | * @return the same (this) [CoroutineScope]. 28 | */ 29 | fun CoroutineScope.withLifecycle(lifecycle: Lifecycle): CoroutineScope { 30 | lifecycle.doOnDestroy(::cancel) 31 | 32 | return this 33 | } 34 | -------------------------------------------------------------------------------- /instance-keeper/src/commonMain/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeper.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.instancekeeper 2 | 3 | /** 4 | * A generic keyed store of [Instance] objects. Instances are destroyed at the end of the 5 | * [InstanceKeeper]'s scope, which is typically tied to the scope of a back stack entry. 6 | * E.g. instances are retained over Android configuration changes, and destroyed when the 7 | * corresponding back stack entry is popped. 8 | */ 9 | interface InstanceKeeper { 10 | 11 | /** 12 | * Returns an instance with the given [key], or `null` if no instance with the given key exists. 13 | */ 14 | fun get(key: Any): Instance? 15 | 16 | /** 17 | * Stores the given [instance] with the given [key]. Throws [IllegalStateException] if another 18 | * instance is already registered with the given [key]. 19 | */ 20 | fun put(key: Any, instance: Instance) 21 | 22 | /** 23 | * Removes an instance with the given [key]. This does not destroy the instance. 24 | */ 25 | fun remove(key: Any): Instance? 26 | 27 | /** 28 | * Represents a destroyable instance. 29 | */ 30 | interface Instance { 31 | 32 | /** 33 | * Called at the end of the [InstanceKeeper]'s scope. 34 | */ 35 | fun onDestroy() {} 36 | } 37 | 38 | /** 39 | * Are simple [Instance] wrapper for cases when destroying is not required. 40 | */ 41 | class SimpleInstance(val instance: T) : Instance 42 | } 43 | -------------------------------------------------------------------------------- /lifecycle/src/itvosTest/kotlin/com/arkivanov/essenty/lifecycle/ApplicationLifecyclePlatformTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle 2 | 3 | import platform.Foundation.NSNotificationCenter 4 | import platform.UIKit.UIApplicationWillEnterForegroundNotification 5 | import kotlin.test.Test 6 | import kotlin.test.assertContentEquals 7 | import kotlin.test.assertEquals 8 | 9 | @Suppress("TestFunctionName") 10 | class ApplicationLifecyclePlatformTest { 11 | 12 | private val notificationName = UIApplicationWillEnterForegroundNotification 13 | private val platform = ApplicationLifecycle.DefaultPlatform 14 | 15 | @Test 16 | fun WHEN_addObserver_and_notification_posted_THEN_notification_received() { 17 | val objects = ArrayList() 18 | 19 | platform.addObserver(notificationName) { objects += it?.`object` } 20 | NSNotificationCenter.defaultCenter.postNotificationName(aName = notificationName, `object` = "str") 21 | 22 | assertContentEquals(listOf("str"), objects) 23 | } 24 | 25 | @Test 26 | fun GIVEN_observer_added_WHEN_removeObserver_and_notification_posted_THEN_notification_not_received() { 27 | val objects = ArrayList() 28 | val observer = platform.addObserver(notificationName) { objects += it?.`object` } 29 | 30 | platform.removeObserver(observer) 31 | NSNotificationCenter.defaultCenter.postNotificationName(aName = notificationName, `object` = "str") 32 | 33 | assertEquals(emptyList(), objects) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/StateKeeper.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlinx.serialization.DeserializationStrategy 4 | import kotlinx.serialization.SerializationStrategy 5 | 6 | /** 7 | * A key-value storage, typically used to persist data after process death or Android configuration changes. 8 | */ 9 | interface StateKeeper { 10 | 11 | /** 12 | * Removes and returns a previously saved value for the given [key]. 13 | * 14 | * @param key a key to look up. 15 | * @param strategy a [DeserializationStrategy] for deserializing the value. 16 | * @return the value for the given [key] or `null` if no value is found. 17 | */ 18 | fun consume(key: String, strategy: DeserializationStrategy): T? 19 | 20 | /** 21 | * Registers the value [supplier] to be called when it's time to persist the data. 22 | * 23 | * @param key a key to be associated with the value. 24 | * @param strategy a [SerializationStrategy] for serializing the value. 25 | * @param supplier a supplier of the value. 26 | */ 27 | fun register(key: String, strategy: SerializationStrategy, supplier: () -> T?) 28 | 29 | /** 30 | * Unregisters a previously registered `supplier` for the given [key]. 31 | */ 32 | fun unregister(key: String) 33 | 34 | /** 35 | * Checks if a `supplier` is registered for the given [key]. 36 | */ 37 | fun isRegistered(key: String): Boolean 38 | } 39 | -------------------------------------------------------------------------------- /lifecycle-reaktive/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/reaktive/DisposableWithLifecycleTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle.reaktive 2 | 3 | import com.arkivanov.essenty.lifecycle.LifecycleRegistry 4 | import com.arkivanov.essenty.lifecycle.create 5 | import com.arkivanov.essenty.lifecycle.destroy 6 | import com.badoo.reaktive.disposable.Disposable 7 | import kotlin.test.Test 8 | import kotlin.test.assertFalse 9 | import kotlin.test.assertTrue 10 | 11 | @Suppress("TestFunctionName") 12 | class DisposableWithLifecycleTest { 13 | 14 | @Test 15 | fun GIVEN_lifecycle_not_destroyed_WHEN_disposable_created_THEN_disposable_is_not_disposed() { 16 | val lifecycle = LifecycleRegistry() 17 | 18 | val disposable = Disposable().withLifecycle(lifecycle) 19 | 20 | assertFalse(disposable.isDisposed) 21 | } 22 | 23 | @Test 24 | fun GIVEN_lifecycle_destroyed_WHEN_disposable_created_THEN_disposable_is_disposed() { 25 | val lifecycle = LifecycleRegistry() 26 | lifecycle.create() 27 | lifecycle.destroy() 28 | 29 | val scope = Disposable().withLifecycle(lifecycle) 30 | 31 | assertTrue(scope.isDisposed) 32 | } 33 | 34 | @Test 35 | fun WHEN_lifecycle_destroyed_THEN_disposable_is_disposed() { 36 | val lifecycle = LifecycleRegistry() 37 | val scope = Disposable().withLifecycle(lifecycle) 38 | lifecycle.create() 39 | 40 | lifecycle.destroy() 41 | 42 | assertTrue(scope.isDisposed) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /state-keeper/src/javaMain/kotlin/com/arkivanov/essenty/statekeeper/Utils.java.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlinx.serialization.DeserializationStrategy 4 | import kotlinx.serialization.ExperimentalSerializationApi 5 | import kotlinx.serialization.SerializationStrategy 6 | import kotlinx.serialization.json.decodeFromStream 7 | import kotlinx.serialization.json.encodeToStream 8 | import java.io.ByteArrayInputStream 9 | import java.io.ByteArrayOutputStream 10 | import java.util.zip.ZipEntry 11 | import java.util.zip.ZipInputStream 12 | import java.util.zip.ZipOutputStream 13 | 14 | internal actual fun T.serialize(strategy: SerializationStrategy): ByteArray = 15 | ByteArrayOutputStream().use { output -> 16 | ZipOutputStream(output).use { zip -> 17 | zip.setLevel(7) 18 | zip.putNextEntry(ZipEntry("Entry")) 19 | 20 | zip.buffered().use { bufferedOutput -> 21 | @OptIn(ExperimentalSerializationApi::class) 22 | essentyJson.encodeToStream(serializer = strategy, value = this, stream = bufferedOutput) 23 | } 24 | } 25 | 26 | output.toByteArray() 27 | } 28 | 29 | internal actual fun ByteArray.deserialize(strategy: DeserializationStrategy): T = 30 | ZipInputStream(ByteArrayInputStream(this)).use { zip -> 31 | zip.nextEntry 32 | 33 | zip.buffered().use { bufferedInput -> 34 | @OptIn(ExperimentalSerializationApi::class) 35 | essentyJson.decodeFromStream(deserializer = strategy, stream = bufferedInput) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /state-keeper/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.arkivanov.gradle.bundle 2 | import com.arkivanov.gradle.dependsOn 3 | import com.arkivanov.gradle.setupBinaryCompatibilityValidator 4 | import com.arkivanov.gradle.setupMultiplatform 5 | import com.arkivanov.gradle.setupPublication 6 | import com.arkivanov.gradle.setupSourceSets 7 | 8 | plugins { 9 | id("kotlin-multiplatform") 10 | id("com.android.library") 11 | id("kotlinx-serialization") 12 | id("com.arkivanov.gradle.setup") 13 | } 14 | 15 | setupMultiplatform() 16 | setupPublication() 17 | setupBinaryCompatibilityValidator() 18 | 19 | android { 20 | namespace = "com.arkivanov.essenty.statekeeper" 21 | } 22 | 23 | kotlin { 24 | setupSourceSets { 25 | val java by bundle() 26 | val nonJava by bundle() 27 | val android by bundle() 28 | val macosArm64 by bundle() 29 | 30 | java dependsOn common 31 | javaSet dependsOn java 32 | nonJava dependsOn common 33 | (allSet - javaSet) dependsOn nonJava 34 | 35 | common.main.dependencies { 36 | implementation(project(":utils-internal")) 37 | api(deps.jetbrains.kotlinx.kotlinxSerializationCore) 38 | implementation(deps.jetbrains.kotlinx.kotlinxSerializationJson) 39 | } 40 | 41 | android.main.dependencies { 42 | implementation(deps.androidx.savedstate.savedstateKtx) 43 | implementation(deps.androidx.lifecycle.lifecycleRuntime) 44 | } 45 | 46 | android.test.dependencies { 47 | implementation(deps.robolectric.robolectric) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/PolymorphicSerializerTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.Serializable 6 | import kotlinx.serialization.builtins.ListSerializer 7 | import kotlinx.serialization.modules.SerializersModule 8 | import kotlinx.serialization.modules.polymorphic 9 | import kotlin.test.Test 10 | import kotlin.test.assertEquals 11 | 12 | class PolymorphicSerializerTest { 13 | 14 | @Test 15 | fun serialize_and_deserialize() { 16 | val someListSerializer = ListSerializer(SomeSerializer) 17 | val originalSome = listOf(Some1(data = SerializableData()), Some2(data = SerializableData())) 18 | 19 | val newSome = originalSome.serialize(someListSerializer).deserialize(someListSerializer) 20 | 21 | assertEquals(originalSome, newSome) 22 | } 23 | 24 | private interface Some 25 | 26 | @Serializable 27 | private data class Some1(val data: SerializableData) : Some 28 | 29 | @Serializable 30 | private data class Some2(val data: SerializableData) : Some 31 | 32 | @OptIn(ExperimentalStateKeeperApi::class, ExperimentalSerializationApi::class) 33 | private object SomeSerializer : KSerializer by polymorphicSerializer( 34 | SerializersModule { 35 | polymorphic(Some::class) { 36 | subclass(Some1::class, Some1.serializer()) 37 | subclass(Some2::class, Some2.serializer()) 38 | } 39 | } 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/Lifecycle.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle 2 | 3 | /** 4 | * A holder of [Lifecycle.State] that can be observed for changes. 5 | * 6 | * Possible transitions: 7 | * 8 | * ``` 9 | * [INITIALIZED] ──┐ 10 | * ↓ 11 | * ┌── [CREATED] ──┐ 12 | * ↓ ↑ ↓ 13 | * [DESTROYED] └── [STARTED] ──┐ 14 | * ↑ ↓ 15 | * └── [RESUMED] 16 | * ``` 17 | */ 18 | interface Lifecycle { 19 | 20 | /** 21 | * The current state of the [Lifecycle]. 22 | */ 23 | val state: State 24 | 25 | /** 26 | * Subscribes the given [callbacks] to state changes. 27 | */ 28 | fun subscribe(callbacks: Callbacks) 29 | 30 | /** 31 | * Unsubscribes the given [callbacks] from state changes. 32 | */ 33 | fun unsubscribe(callbacks: Callbacks) 34 | 35 | /** 36 | * Defines the possible states of the [Lifecycle]. 37 | */ 38 | enum class State { 39 | DESTROYED, 40 | INITIALIZED, 41 | CREATED, 42 | STARTED, 43 | RESUMED 44 | } 45 | 46 | /** 47 | * The callbacks of the [Lifecycle]. Each callback is called on the corresponding state change. 48 | */ 49 | interface Callbacks { 50 | fun onCreate() { 51 | } 52 | 53 | fun onStart() { 54 | } 55 | 56 | fun onResume() { 57 | } 58 | 59 | fun onPause() { 60 | } 61 | 62 | fun onStop() { 63 | } 64 | 65 | fun onDestroy() { 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lifecycle-coroutines/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/coroutines/DispatchersExtTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle.coroutines 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.StandardTestDispatcher 6 | import kotlinx.coroutines.test.TestDispatcher 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import kotlin.test.* 10 | 11 | @OptIn(ExperimentalCoroutinesApi::class) 12 | @Suppress("TestFunctionName") 13 | class DispatchersExtTest { 14 | 15 | @AfterTest 16 | fun after() { 17 | Dispatchers.resetMain() 18 | } 19 | 20 | @Test 21 | fun WHEN_immediateOrDefault_called_multiple_times_THEN_returns_same_dispatcher() { 22 | val dispatcher1 = Dispatchers.Main.immediateOrFallback 23 | val dispatcher2 = Dispatchers.Main.immediateOrFallback 24 | 25 | assertSame(dispatcher1, dispatcher2) 26 | } 27 | 28 | @Test 29 | fun GIVEN_Main_dispatcher_changed_WHEN_immediateOrDefault_called_THEN_returns_updated_dispatcher() { 30 | try { 31 | Dispatchers.Main.immediate 32 | } catch (e: NotImplementedError) { 33 | return // Only test on platforms where Main dispatcher is supported 34 | } 35 | 36 | val oldDispatcher = Dispatchers.Main.immediateOrFallback 37 | val testDispatcher = StandardTestDispatcher() 38 | Dispatchers.setMain(testDispatcher) 39 | 40 | val newDispatcher = Dispatchers.Main.immediateOrFallback 41 | 42 | assertNotSame(oldDispatcher, newDispatcher) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /lifecycle-coroutines/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/coroutines/CoroutineScopeWithLifecycleTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle.coroutines 2 | 3 | import com.arkivanov.essenty.lifecycle.LifecycleRegistry 4 | import com.arkivanov.essenty.lifecycle.create 5 | import com.arkivanov.essenty.lifecycle.destroy 6 | import kotlinx.coroutines.CoroutineScope 7 | import kotlinx.coroutines.isActive 8 | import kotlinx.coroutines.test.StandardTestDispatcher 9 | import kotlin.test.Test 10 | import kotlin.test.assertFalse 11 | import kotlin.test.assertTrue 12 | 13 | @Suppress("TestFunctionName") 14 | class CoroutineScopeWithLifecycleTest { 15 | 16 | @Test 17 | fun GIVEN_lifecycle_not_destroyed_WHEN_scope_created_THEN_scope_is_active() { 18 | val lifecycle = LifecycleRegistry() 19 | 20 | val scope = CoroutineScope(StandardTestDispatcher()).withLifecycle(lifecycle) 21 | 22 | assertTrue(scope.isActive) 23 | } 24 | 25 | @Test 26 | fun GIVEN_lifecycle_destroyed_WHEN_scope_created_THEN_scope_is_not_active() { 27 | val lifecycle = LifecycleRegistry() 28 | lifecycle.create() 29 | lifecycle.destroy() 30 | 31 | val scope = CoroutineScope(StandardTestDispatcher()).withLifecycle(lifecycle) 32 | 33 | assertFalse(scope.isActive) 34 | } 35 | 36 | @Test 37 | fun WHEN_lifecycle_destroyed_THEN_scope_is_not_active() { 38 | val lifecycle = LifecycleRegistry() 39 | val scope = CoroutineScope(StandardTestDispatcher()).withLifecycle(lifecycle) 40 | lifecycle.create() 41 | 42 | lifecycle.destroy() 43 | 44 | assertFalse(scope.isActive) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/Encoder.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper.base64 2 | 3 | internal fun ByteArray.toBase64(): String = 4 | encode(this) 5 | 6 | internal fun encode(array: ByteArray): String = buildString(capacity = (array.size / 3) * 4 + 1) { 7 | var index = 0 8 | 9 | while (index < array.size) { 10 | if (index + 3 > array.size) break 11 | 12 | val buffer = array[index].toInt() and 0xff shl 16 or 13 | (array[index + 1].toInt() and 0xff shl 8) or 14 | (array[index + 2].toInt() and 0xff shl 0) 15 | 16 | append(dictionary[buffer shr 18]) 17 | append(dictionary[buffer shr 12 and 0x3f]) 18 | append(dictionary[buffer shr 6 and 0x3f]) 19 | append(dictionary[buffer and 0x3f]) 20 | 21 | index += 3 22 | } 23 | 24 | if (index < array.size) { 25 | var buffer = 0 26 | while (index < array.size) { 27 | buffer = buffer shl 8 or (array[index].toInt() and 0xff) 28 | index++ 29 | } 30 | val padding = 3 - (index % 3) 31 | buffer = buffer shl (padding * 8) 32 | 33 | append(dictionary[buffer shr 18]) 34 | append(dictionary[buffer shr 12 and 0x3f]) 35 | 36 | val a = dictionary[buffer shr 6 and 0x3f] 37 | val b = dictionary[buffer and 0x3f] 38 | 39 | when (padding) { 40 | 0 -> { 41 | append(a) 42 | append(b) 43 | } 44 | 1 -> { 45 | append(a) 46 | append('=') 47 | } 48 | 2 -> { 49 | append("==") 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/base64/Base64ImplTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper.base64 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class Base64ImplTest { 7 | 8 | @Test 9 | fun encodeSmokeTests() { 10 | testEncode("123", "MTIz") 11 | testEncode("abcdef", "YWJjZGVm") 12 | 13 | testEncode("1", "MQ==") 14 | testEncode("2", "Mg==") 15 | testEncode("12", "MTI=") 16 | 17 | testEncode("abcd", "YWJjZA==") 18 | testEncode("abcde", "YWJjZGU=") 19 | 20 | // RFC's testcases 21 | testEncode("", "") 22 | testEncode("f", "Zg==") 23 | testEncode("fo", "Zm8=") 24 | testEncode("foo", "Zm9v") 25 | testEncode("foob", "Zm9vYg==") 26 | testEncode("fooba", "Zm9vYmE=") 27 | testEncode("foobar", "Zm9vYmFy") 28 | } 29 | 30 | @Test 31 | fun decodeSmokeTests() { 32 | testDecode("123", "MTIz") 33 | testDecode("abcdef", "YWJjZGVm") 34 | 35 | testDecode("1", "MQ==") 36 | testDecode("2", "Mg==") 37 | testDecode("12", "MTI=") 38 | 39 | testDecode("abcd", "YWJjZA==") 40 | testDecode("abcde", "YWJjZGU=") 41 | 42 | // RFC 43 | // RFC's testcases 44 | testDecode("", "") 45 | testDecode("f", "Zg==") 46 | testDecode("fo", "Zm8=") 47 | testDecode("foo", "Zm9v") 48 | testDecode("foob", "Zm9vYg==") 49 | testDecode("fooba", "Zm9vYmE=") 50 | testDecode("foobar", "Zm9vYmFy") 51 | } 52 | 53 | private fun testEncode(input: String, expected: String) { 54 | val result = encode(input.encodeToByteArray()) 55 | assertEquals(expected, result) 56 | } 57 | 58 | private fun testDecode(expected: String, encoded: String) { 59 | val result = decode(encoded).decodeToString() 60 | assertEquals(expected, result) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/BundleExtTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import android.os.Bundle 4 | import kotlinx.serialization.Serializable 5 | import org.junit.runner.RunWith 6 | import org.robolectric.RobolectricTestRunner 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | 10 | @RunWith(RobolectricTestRunner::class) 11 | class BundleExtTest { 12 | 13 | @Test 14 | fun getSerializable_returns_same_value_after_putSerializable_without_serialization() { 15 | val value = Value(value = "123") 16 | val bundle = Bundle() 17 | bundle.putSerializable(key = "key", value = value, strategy = Value.serializer()) 18 | val newValue = bundle.getSerializable(key = "key", strategy = Value.serializer()) 19 | 20 | assertEquals(value, newValue) 21 | } 22 | 23 | @Test 24 | fun getSerializable_returns_same_value_after_putSerializable_with_serialization() { 25 | val value = Value(value = "123") 26 | val bundle = Bundle() 27 | bundle.putSerializable(key = "key", value = value, strategy = Value.serializer()) 28 | val newValue = bundle.parcelize().deparcelize().getSerializable(key = "key", strategy = Value.serializer()) 29 | 30 | assertEquals(value, newValue) 31 | } 32 | 33 | @Test 34 | fun getSerializable_returns_same_value_after_putSerializable_with_double_serialization() { 35 | val value = Value(value = "123") 36 | val bundle = Bundle() 37 | bundle.putSerializable(key = "key", value = value, strategy = Value.serializer()) 38 | bundle.putInt("int", 123) 39 | val newBundle = bundle.parcelize().deparcelize() 40 | newBundle.getInt("int") // Force partial deserialization of the Bundle 41 | val newValue = newBundle.parcelize().deparcelize().getSerializable(key = "key", strategy = Value.serializer()) 42 | 43 | assertEquals(value, newValue) 44 | } 45 | 46 | @Serializable 47 | data class Value(val value: String) 48 | } 49 | -------------------------------------------------------------------------------- /lifecycle-coroutines/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/coroutines/FlowWithLifecycle.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle.coroutines 2 | 3 | import com.arkivanov.essenty.lifecycle.Lifecycle 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.callbackFlow 7 | import kotlin.coroutines.CoroutineContext 8 | 9 | /** 10 | * [Flow] operator that emits values from this upstream [Flow] when the [lifecycle] 11 | * is at least at [minActiveState] state. The emissions will be stopped when the 12 | * [lifecycle] state falls below [minActiveState] state. 13 | * 14 | * The [Flow] is collected on the specified [context], which defaults to 15 | * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] 16 | * if available on the current platform, or to [Dispatchers.Main] otherwise. 17 | * 18 | * See the [AndroidX documentation](https://developer.android.com/reference/kotlin/androidx/lifecycle/package-summary#(kotlinx.coroutines.flow.Flow).flowWithLifecycle(androidx.lifecycle.Lifecycle,androidx.lifecycle.Lifecycle.State)) 19 | * for more information. 20 | */ 21 | fun Flow.withLifecycle( 22 | lifecycle: Lifecycle, 23 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED, 24 | context: CoroutineContext = Dispatchers.Main.immediateOrFallback, 25 | ): Flow = callbackFlow { 26 | lifecycle.repeatOnLifecycle(minActiveState, context) { 27 | this@withLifecycle.collect { 28 | send(it) 29 | } 30 | } 31 | close() 32 | } 33 | 34 | @Deprecated( 35 | message = "Use 'withLifecycle' instead", 36 | replaceWith = ReplaceWith("withLifecycle(lifecycle, minActiveState)"), 37 | level = DeprecationLevel.ERROR 38 | ) 39 | fun Flow.flowWithLifecycle( 40 | lifecycle: Lifecycle, 41 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED, 42 | context: CoroutineContext = Dispatchers.Main.immediateOrFallback, 43 | ): Flow { 44 | return withLifecycle(lifecycle, minActiveState, context) 45 | } 46 | -------------------------------------------------------------------------------- /lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleRegistryExt.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle 2 | 3 | /** 4 | * Drives the state of the [Lifecycle] forward to [Lifecycle.State.CREATED]. 5 | * Does nothing if the state is already [Lifecycle.State.CREATED] or greater, or [Lifecycle.State.DESTROYED]. 6 | */ 7 | fun LifecycleRegistry.create() { 8 | if (state == Lifecycle.State.INITIALIZED) { 9 | onCreate() 10 | } 11 | } 12 | 13 | /** 14 | * Drives the state of the [Lifecycle] forward to [Lifecycle.State.STARTED]. 15 | * Does nothing if the state is already [Lifecycle.State.STARTED] or greater, or [Lifecycle.State.DESTROYED]. 16 | */ 17 | fun LifecycleRegistry.start() { 18 | create() 19 | 20 | if (state == Lifecycle.State.CREATED) { 21 | onStart() 22 | } 23 | } 24 | 25 | /** 26 | * Drives the state of the [Lifecycle] forward to [Lifecycle.State.RESUMED]. 27 | * Does nothing if the state is already [Lifecycle.State.RESUMED] or greater, or [Lifecycle.State.DESTROYED]. 28 | */ 29 | fun LifecycleRegistry.resume() { 30 | start() 31 | 32 | if (state == Lifecycle.State.STARTED) { 33 | onResume() 34 | } 35 | } 36 | 37 | /** 38 | * Drives the state of the [Lifecycle] backward to [Lifecycle.State.STARTED]. 39 | * Does nothing if the state is already [Lifecycle.State.STARTED] or lower. 40 | */ 41 | fun LifecycleRegistry.pause() { 42 | if (state == Lifecycle.State.RESUMED) { 43 | onPause() 44 | } 45 | } 46 | 47 | /** 48 | * Drives the state of the [Lifecycle] backward to [Lifecycle.State.CREATED]. 49 | * Does nothing if the state is already [Lifecycle.State.CREATED] or lower. 50 | */ 51 | fun LifecycleRegistry.stop() { 52 | pause() 53 | 54 | if (state == Lifecycle.State.STARTED) { 55 | onStop() 56 | } 57 | } 58 | 59 | /** 60 | * Drives the state of the [Lifecycle] backward to [Lifecycle.State.DESTROYED]. 61 | * Does nothing if the state is already [Lifecycle.State.DESTROYED]. 62 | */ 63 | fun LifecycleRegistry.destroy() { 64 | stop() 65 | 66 | if (state == Lifecycle.State.CREATED) { 67 | onDestroy() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.backhandler 2 | 3 | import kotlin.js.JsName 4 | 5 | /** 6 | * Provides a way to manually trigger back button handlers. 7 | */ 8 | interface BackDispatcher : BackHandler { 9 | 10 | /** 11 | * Returns `true` if there is at least one enabled handler, `false` otherwise. 12 | */ 13 | val isEnabled: Boolean 14 | 15 | /** 16 | * Adds the provided [listener], which will be called every time the enabled state of 17 | * this [BackDispatcher] changes. 18 | */ 19 | fun addEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) 20 | 21 | /** 22 | * Removes the provided enabled state changed [listener]. 23 | */ 24 | fun removeEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) 25 | 26 | /** 27 | * If no predictive back gesture is currently in progress, finds the last enabled 28 | * callback with the highest priority and calls [BackCallback.onBack]. 29 | * 30 | * If the predictive back gesture is currently in progress, calls [BackCallback.onBack] on 31 | * the previously selected callback. 32 | * 33 | * @return `true` if any callback was triggered, `false` otherwise. 34 | */ 35 | fun back(): Boolean 36 | 37 | /** 38 | * Starts handling the predictive back gesture. Picks one of the enabled callback (if any) 39 | * that will be handling the gesture and calls [BackCallback.onBackStarted]. 40 | * 41 | * @return `true` if any callback was triggered, `false` otherwise. 42 | */ 43 | fun startPredictiveBack(backEvent: BackEvent): Boolean 44 | 45 | /** 46 | * Calls [BackCallback.onBackProgressed] on the previously selected callback. 47 | */ 48 | fun progressPredictiveBack(backEvent: BackEvent) 49 | 50 | /** 51 | * Calls [BackCallback.onBackCancelled] on the previously selected callback. 52 | */ 53 | fun cancelPredictiveBack() 54 | } 55 | 56 | /** 57 | * Creates and returns a default implementation of [BackDispatcher]. 58 | */ 59 | @JsName("backDispatcher") 60 | fun BackDispatcher(): BackDispatcher = 61 | DefaultBackDispatcher() 62 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/base64/Decoder.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper.base64 2 | 3 | internal fun String.base64ToByteArray(): ByteArray = 4 | decode(this) 5 | 6 | @Suppress("CognitiveComplexMethod", "LoopWithTooManyJumpStatements") // Keep the original 7 | internal fun decode(encoded: String): ByteArray { 8 | if (encoded.isBlank()) return ByteArray(0) 9 | val result = ByteArray(encoded.length) 10 | var resultSize = 0 11 | 12 | val backDictionary = backDictionary 13 | var buffer = 0 14 | var buffered = 0 15 | var index = 0 16 | 17 | while (index < encoded.length) { 18 | val ch = encoded[index++] 19 | if (ch <= ' ') continue 20 | if (ch == '=') { 21 | index-- 22 | break 23 | } 24 | val value = backDictionary.getOrElse(ch.code) { -1 } 25 | if (value == -1) error("Unexpected character $ch (${ch.code})) in $encoded") 26 | 27 | buffer = buffer shl 6 or value 28 | buffered++ 29 | 30 | if (buffered == 4) { 31 | result[resultSize] = (buffer shr 16).toByte() 32 | result[resultSize + 1] = (buffer shr 8 and 0xff).toByte() 33 | result[resultSize + 2] = (buffer and 0xff).toByte() 34 | resultSize += 3 35 | buffered = 0 36 | buffer = 0 37 | } 38 | } 39 | 40 | var padding = 0 41 | while (index < encoded.length) { 42 | val ch = encoded[index++] 43 | if (ch <= ' ') continue 44 | check(ch == '=') 45 | padding++ 46 | buffer = buffer shl 6 47 | buffered++ 48 | } 49 | 50 | if (buffered == 4) { 51 | result[resultSize] = (buffer shr 16).toByte() 52 | result[resultSize + 1] = (buffer shr 8 and 0xff).toByte() 53 | result[resultSize + 2] = (buffer and 0xff).toByte() 54 | resultSize += 3 55 | 56 | resultSize -= padding 57 | buffered = 0 58 | } 59 | 60 | check(buffered == 0) { 61 | "buffered: $buffered" 62 | } 63 | 64 | return when { 65 | resultSize < result.size -> result.copyOf(resultSize) 66 | else -> result 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /instance-keeper/src/androidMain/kotlin/com/arkivanov/essenty/instancekeeper/AndroidExt.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.instancekeeper 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import androidx.lifecycle.ViewModelStore 6 | import androidx.lifecycle.ViewModelStoreOwner 7 | import androidx.lifecycle.get 8 | 9 | /** 10 | * Creates a new instance of [InstanceKeeper] and attaches it to the provided AndroidX [ViewModelStore]. 11 | * 12 | * @param discardRetainedInstances a flag indicating whether any previously retained instances should be 13 | * discarded and destroyed or not, default value is `false`. 14 | */ 15 | fun InstanceKeeper( 16 | viewModelStore: ViewModelStore, 17 | discardRetainedInstances: Boolean = false, 18 | ): InstanceKeeper = 19 | ViewModelProvider( 20 | viewModelStore, 21 | object : ViewModelProvider.Factory { 22 | @Suppress("UNCHECKED_CAST") 23 | override fun create(modelClass: Class): T = InstanceKeeperViewModel() as T 24 | } 25 | ) 26 | .get() 27 | .apply { 28 | if (discardRetainedInstances) { 29 | recreate() 30 | } 31 | } 32 | .instanceKeeperDispatcher 33 | 34 | /** 35 | * Creates a new instance of [InstanceKeeper] and attaches it to the AndroidX [ViewModelStore]. 36 | * 37 | * @param discardRetainedInstances a flag indicating whether any previously retained instances should be 38 | * discarded and destroyed or not, default value is `false`. 39 | */ 40 | fun ViewModelStoreOwner.instanceKeeper(discardRetainedInstances: Boolean = false): InstanceKeeper = 41 | InstanceKeeper(viewModelStore = viewModelStore, discardRetainedInstances = discardRetainedInstances) 42 | 43 | internal class InstanceKeeperViewModel : ViewModel() { 44 | var instanceKeeperDispatcher: InstanceKeeperDispatcher = InstanceKeeperDispatcher() 45 | private set 46 | 47 | override fun onCleared() { 48 | instanceKeeperDispatcher.destroy() 49 | } 50 | 51 | fun recreate() { 52 | instanceKeeperDispatcher.destroy() 53 | instanceKeeperDispatcher = InstanceKeeperDispatcher() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /instance-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/instancekeeper/AndroidInstanceKeeperTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.instancekeeper 2 | 3 | import androidx.lifecycle.ViewModelStore 4 | import androidx.lifecycle.ViewModelStoreOwner 5 | import kotlin.test.Test 6 | import kotlin.test.assertNotSame 7 | import kotlin.test.assertSame 8 | import kotlin.test.assertTrue 9 | 10 | @Suppress("TestFunctionName") 11 | class AndroidInstanceKeeperTest { 12 | 13 | @Test 14 | fun retains_instances() { 15 | val owner = TestOwner() 16 | var instanceKeeper = owner.instanceKeeper() 17 | val instance1 = instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) 18 | 19 | instanceKeeper = owner.instanceKeeper() 20 | val instance2 = instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) 21 | 22 | assertSame(instance1, instance2) 23 | } 24 | 25 | @Test 26 | fun GIVEN_discardRetainedInstances_is_true_on_restore_THEN_instances_not_retained() { 27 | val owner = TestOwner() 28 | var instanceKeeper = owner.instanceKeeper() 29 | val instance1 = instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) 30 | 31 | instanceKeeper = owner.instanceKeeper(discardRetainedInstances = true) 32 | val instance2 = instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) 33 | 34 | assertNotSame(instance1, instance2) 35 | } 36 | 37 | @Test 38 | fun GIVEN_discardRetainedInstances_is_true_on_restore_THEN_old_instances_destroyed() { 39 | val owner = TestOwner() 40 | val instanceKeeper = owner.instanceKeeper() 41 | val instance1 = instanceKeeper.getOrCreate(key = "key", factory = ::TestInstance) 42 | 43 | owner.instanceKeeper(discardRetainedInstances = true) 44 | 45 | assertTrue(instance1.isDestroyed) 46 | } 47 | 48 | private class TestOwner : ViewModelStoreOwner { 49 | override val viewModelStore: ViewModelStore = ViewModelStore() 50 | } 51 | 52 | private class TestInstance : InstanceKeeper.Instance { 53 | var isDestroyed: Boolean = false 54 | 55 | override fun onDestroy() { 56 | isDestroyed = true 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/DefaultStateKeeperDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlinx.serialization.DeserializationStrategy 4 | import kotlinx.serialization.Serializable 5 | import kotlinx.serialization.SerializationStrategy 6 | 7 | internal class DefaultStateKeeperDispatcher( 8 | savedState: SerializableContainer?, 9 | ) : StateKeeperDispatcher { 10 | 11 | private val savedState: MutableMap? = savedState?.consume(strategy = SavedState.serializer())?.map 12 | private val suppliers = HashMap>() 13 | 14 | override fun save(): SerializableContainer { 15 | val map = savedState?.toMutableMap() ?: HashMap() 16 | 17 | suppliers.forEach { (key, supplier) -> 18 | supplier.toSerializableContainer()?.also { container -> 19 | map[key] = container 20 | } 21 | } 22 | 23 | return SerializableContainer(value = SavedState(map), strategy = SavedState.serializer()) 24 | } 25 | 26 | private fun Supplier.toSerializableContainer(): SerializableContainer? = 27 | supplier()?.let { value -> 28 | SerializableContainer(value = value, strategy = strategy) 29 | } 30 | 31 | override fun consume(key: String, strategy: DeserializationStrategy): T? = 32 | savedState 33 | ?.remove(key) 34 | ?.consume(strategy = strategy) 35 | 36 | override fun register(key: String, strategy: SerializationStrategy, supplier: () -> T?) { 37 | check(!isRegistered(key)) { "Another supplier is already registered with the key: $key" } 38 | suppliers[key] = Supplier(strategy = strategy, supplier = supplier) 39 | } 40 | 41 | override fun unregister(key: String) { 42 | check(isRegistered(key)) { "No supplier is registered with the key: $key" } 43 | suppliers -= key 44 | } 45 | 46 | override fun isRegistered(key: String): Boolean = key in suppliers 47 | 48 | private class Supplier( 49 | val strategy: SerializationStrategy, 50 | val supplier: () -> T?, 51 | ) 52 | 53 | @Serializable 54 | private class SavedState( 55 | val map: MutableMap 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleRegistryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle 2 | 3 | import com.arkivanov.essenty.lifecycle.Lifecycle.Callbacks 4 | import com.arkivanov.essenty.lifecycle.Lifecycle.State 5 | 6 | internal class LifecycleRegistryImpl(initialState: State) : LifecycleRegistry { 7 | 8 | private var callbacks = emptySet() 9 | private var _state = initialState 10 | override val state: State get() = _state 11 | 12 | override fun subscribe(callbacks: Callbacks) { 13 | check(callbacks !in this.callbacks) { "Already subscribed" } 14 | 15 | this.callbacks += callbacks 16 | 17 | val state = _state 18 | if (state >= State.CREATED) { 19 | callbacks.onCreate() 20 | } 21 | if (state >= State.STARTED) { 22 | callbacks.onStart() 23 | } 24 | if (state >= State.RESUMED) { 25 | callbacks.onResume() 26 | } 27 | } 28 | 29 | override fun unsubscribe(callbacks: Callbacks) { 30 | this.callbacks -= callbacks 31 | } 32 | 33 | override fun onCreate() { 34 | checkState(State.INITIALIZED) 35 | _state = State.CREATED 36 | callbacks.forEach(Callbacks::onCreate) 37 | } 38 | 39 | override fun onStart() { 40 | checkState(State.CREATED) 41 | _state = State.STARTED 42 | callbacks.forEach(Callbacks::onStart) 43 | } 44 | 45 | override fun onResume() { 46 | checkState(State.STARTED) 47 | _state = State.RESUMED 48 | callbacks.forEach(Callbacks::onResume) 49 | } 50 | 51 | override fun onPause() { 52 | checkState(State.RESUMED) 53 | _state = State.STARTED 54 | callbacks.reversed().forEach(Callbacks::onPause) 55 | } 56 | 57 | override fun onStop() { 58 | checkState(State.STARTED) 59 | _state = State.CREATED 60 | callbacks.reversed().forEach(Callbacks::onStop) 61 | } 62 | 63 | override fun onDestroy() { 64 | checkState(State.CREATED) 65 | _state = State.DESTROYED 66 | callbacks.reversed().forEach(Callbacks::onDestroy) 67 | callbacks = emptySet() 68 | } 69 | 70 | private fun checkState(required: State) { 71 | check(_state == required) { "Expected state $required but was $_state" } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | create-staging-repository: 8 | runs-on: ubuntu-latest 9 | name: Create staging repository 10 | outputs: 11 | repository_id: ${{ steps.create.outputs.repository_id }} 12 | steps: 13 | - id: create 14 | uses: nexus-actions/create-nexus-staging-repo@v1.3.0 15 | with: 16 | username: arkivanov 17 | password: ${{ secrets.SONATYPE_PASSWORD }} 18 | staging_profile_id: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} 19 | description: Created by GitHub Actions 20 | base_url: https://s01.oss.sonatype.org/service/local/ 21 | publish: 22 | name: Publish 23 | runs-on: macos-14 24 | needs: create-staging-repository 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | - name: Install Java 29 | uses: actions/setup-java@v3 30 | with: 31 | distribution: 'zulu' 32 | java-version: 17 33 | - name: Publish 34 | env: 35 | SONATYPE_REPOSITORY_ID: ${{ needs.create-staging-repository.outputs.repository_id }} 36 | SONATYPE_USER_NAME: ${{ secrets.SONATYPE_USER_NAME }} 37 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 38 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 39 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 40 | run: ./gradlew publish 41 | close-staging-repository: 42 | name: Close staging repository 43 | runs-on: ubuntu-latest 44 | needs: [ create-staging-repository, publish ] 45 | steps: 46 | - name: Close staging repository 47 | uses: nexus-actions/release-nexus-staging-repo@v1.1 48 | with: 49 | username: arkivanov 50 | password: ${{ secrets.SONATYPE_PASSWORD }} 51 | staging_repository_id: ${{ needs.create-staging-repository.outputs.repository_id }} 52 | base_url: https://s01.oss.sonatype.org/service/local/ 53 | close_only: 'true' 54 | check-publication: 55 | name: Check publication 56 | runs-on: macos-14 57 | needs: close-staging-repository 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v1 61 | - name: Install Java 62 | uses: actions/setup-java@v3 63 | with: 64 | distribution: 'zulu' 65 | java-version: 17 66 | - name: Check publication 67 | run: ./gradlew kotlinUpgradeYarnLock :tools:check-publication:build -Pcheck_publication 68 | -------------------------------------------------------------------------------- /back-handler/src/androidUnitTest/kotlin/com/arkivanov/essenty/backhandler/AndroidBackHandlerWithLifecycleTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.backhandler 2 | 3 | import androidx.activity.OnBackPressedDispatcher 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleOwner 6 | import androidx.lifecycle.LifecycleRegistry 7 | import kotlin.test.Test 8 | import kotlin.test.assertFalse 9 | import kotlin.test.assertTrue 10 | 11 | @Suppress("TestFunctionName") 12 | class AndroidBackHandlerWithLifecycleTest { 13 | 14 | private val dispatcher = OnBackPressedDispatcher() 15 | private val lifecycleOwner = LifecycleOwnerImpl() 16 | 17 | @Test 18 | fun GIVEN_lifecycle_created_WHEN_handler_created_THEN_hasEnabledCallbacks_returns_false() { 19 | lifecycleOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) 20 | 21 | val handler = handler() 22 | handler.register(callback()) 23 | 24 | assertFalse(dispatcher.hasEnabledCallbacks()) 25 | } 26 | 27 | @Test 28 | fun GIVEN_lifecycle_started_WHEN_handler_created_THEN_hasEnabledCallbacks_returns_true() { 29 | lifecycleOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START) 30 | 31 | val handler = handler() 32 | handler.register(callback()) 33 | 34 | assertTrue(dispatcher.hasEnabledCallbacks()) 35 | } 36 | 37 | @Test 38 | fun GIVEN_handler_created_WHEN_lifecycle_started_THEN_hasEnabledCallbacks_returns_true() { 39 | val handler = handler() 40 | handler.register(callback()) 41 | 42 | lifecycleOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START) 43 | 44 | assertTrue(dispatcher.hasEnabledCallbacks()) 45 | } 46 | 47 | @Test 48 | fun GIVEN_lifecycle_started_WHEN_lifecycle_stopped_THEN_hasEnabledCallbacks_returns_false() { 49 | lifecycleOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START) 50 | val handler = handler() 51 | handler.register(callback()) 52 | 53 | lifecycleOwner.lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP) 54 | 55 | assertFalse(dispatcher.hasEnabledCallbacks()) 56 | } 57 | 58 | private fun handler(): BackHandler = 59 | BackHandler( 60 | onBackPressedDispatcher = dispatcher, 61 | lifecycleOwner = lifecycleOwner, 62 | ) 63 | 64 | private fun callback(): BackCallback = 65 | BackCallback(isEnabled = true, onBack = {}) 66 | 67 | private class LifecycleOwnerImpl : LifecycleOwner { 68 | override val lifecycle: LifecycleRegistry = LifecycleRegistry.createUnsafe(this) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /deps.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | essenty = "2.5.0" 4 | kotlin = "2.1.0" 5 | kotlinxBinaryCompatibilityValidator = "0.16.3" 6 | kotlinxCoroutines = "1.9.0" 7 | detektGradlePlugin = "1.23.6" 8 | junit = "4.13.2" 9 | androidGradle = "8.0.2" 10 | androidxLifecycle = "2.6.2" 11 | androidxSavedstate = "1.2.1" 12 | androidxActivity = "1.8.1" 13 | jetbrainsKotlinxSerialization = "1.6.3" 14 | robolectric = "4.9.1" 15 | reaktive = "2.1.0" 16 | 17 | [libraries] 18 | 19 | kotlin-kotlinGradlePlug = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } 20 | kotlinx-binaryCompatibilityValidator = { group = "org.jetbrains.kotlinx", name = "binary-compatibility-validator", version.ref = "kotlinxBinaryCompatibilityValidator" } 21 | 22 | kotlinx-coroutinesCore = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" } 23 | kotlinx-coroutinesTest = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" } 24 | 25 | detekt-gradleDetektPlug = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detektGradlePlugin" } 26 | 27 | android-gradle = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradle" } 28 | 29 | androidx-lifecycle-lifecycleCommonJava8 = { group = "androidx.lifecycle", name = "lifecycle-common-java8", version.ref = "androidxLifecycle" } 30 | androidx-lifecycle-lifecycleRuntime = { group = "androidx.lifecycle", name = "lifecycle-runtime", version.ref = "androidxLifecycle" } 31 | androidx-lifecycle-lifecycleViewmodelKtx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } 32 | androidx-savedstate-savedstateKtx = { group = "androidx.savedstate", name = "savedstate-ktx", version.ref = "androidxSavedstate" } 33 | androidx-activity-activityKtx = { group = "androidx.activity", name = "activity-ktx", version.ref = "androidxActivity" } 34 | 35 | jetbrains-kotlin-serializationGradlePlug = { group = "org.jetbrains.kotlin", name = "kotlin-serialization", version.ref = "kotlin" } 36 | jetbrains-kotlinx-kotlinxSerializationCore = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-core", version.ref = "jetbrainsKotlinxSerialization" } 37 | jetbrains-kotlinx-kotlinxSerializationJson = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "jetbrainsKotlinxSerialization" } 38 | 39 | robolectric-robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" } 40 | 41 | reaktive-reaktive = { group = "com.badoo.reaktive", name = "reaktive", version.ref = "reaktive" } 42 | -------------------------------------------------------------------------------- /state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/PersistableBundleExt.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.os.PersistableBundle 6 | import androidx.annotation.RequiresApi 7 | import com.arkivanov.essenty.statekeeper.base64.base64ToByteArray 8 | import com.arkivanov.essenty.statekeeper.base64.toBase64 9 | import kotlinx.serialization.DeserializationStrategy 10 | import kotlinx.serialization.SerializationStrategy 11 | 12 | /** 13 | * Inserts the provided `kotlinx-serialization` [Serializable][kotlinx.serialization.Serializable] value 14 | * into this [PersistableBundle], replacing any existing value for the given [key]. 15 | * Either [key] or [value] may be `null`. 16 | * 17 | * **Note:** unlike [Bundle.putSerializable], due to the specifics of [PersistableBundle] this function 18 | * serializes the [value] eagerly, which may degrade the performance for large payloads. 19 | */ 20 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 21 | fun PersistableBundle.putSerializable(key: String?, value: T?, strategy: SerializationStrategy) { 22 | putString(key, value?.serialize(strategy)?.toBase64()) 23 | } 24 | 25 | /** 26 | * Returns a `kotlinx-serialization` [Serializable][kotlinx.serialization.Serializable] associated with 27 | * the given [key], or `null` if no mapping exists for the given [key] or a `null` value is explicitly 28 | * associated with the [key]. 29 | */ 30 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 31 | fun PersistableBundle.getSerializable(key: String?, strategy: DeserializationStrategy): T? = 32 | getString(key)?.base64ToByteArray()?.deserialize(strategy) 33 | 34 | /** 35 | * Inserts the provided [SerializableContainer] into this [Bundle], 36 | * replacing any existing value for the given [key]. Either [key] or [value] may be `null`. 37 | * 38 | * **Note:** unlike [Bundle.putSerializableContainer], due to the specifics of [PersistableBundle] 39 | * this function serializes the [value] eagerly, which may degrade the performance for large payloads. 40 | */ 41 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 42 | fun PersistableBundle.putSerializableContainer(key: String?, value: SerializableContainer?) { 43 | putSerializable(key = key, value = value, strategy = SerializableContainer.serializer()) 44 | } 45 | 46 | /** 47 | * Returns a [SerializableContainer] associated with the given [key], 48 | * or `null` if no mapping exists for the given [key] or a `null` value 49 | * is explicitly associated with the [key]. 50 | */ 51 | @RequiresApi(Build.VERSION_CODES.LOLLIPOP) 52 | fun PersistableBundle.getSerializableContainer(key: String?): SerializableContainer? = 53 | getSerializable(key = key, strategy = SerializableContainer.serializer()) 54 | -------------------------------------------------------------------------------- /lifecycle-coroutines/api/jvm/lifecycle-coroutines.api: -------------------------------------------------------------------------------- 1 | public final class com/arkivanov/essenty/lifecycle/coroutines/CoroutineScopeWithLifecycleKt { 2 | public static final fun coroutineScope (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/CoroutineScope; 3 | public static synthetic fun coroutineScope$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/CoroutineScope; 4 | public static final fun withLifecycle (Lkotlinx/coroutines/CoroutineScope;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Lkotlinx/coroutines/CoroutineScope; 5 | } 6 | 7 | public final class com/arkivanov/essenty/lifecycle/coroutines/FlowWithLifecycleKt { 8 | public static final fun flowWithLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; 9 | public static synthetic fun flowWithLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; 10 | public static final fun withLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; 11 | public static synthetic fun withLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; 12 | } 13 | 14 | public final class com/arkivanov/essenty/lifecycle/coroutines/RepeatOnLifecycleKt { 15 | public static final fun repeatOnLifecycle (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 16 | public static final fun repeatOnLifecycle (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 17 | public static synthetic fun repeatOnLifecycle$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; 18 | public static synthetic fun repeatOnLifecycle$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /lifecycle-coroutines/api/android/lifecycle-coroutines.api: -------------------------------------------------------------------------------- 1 | public final class com/arkivanov/essenty/lifecycle/coroutines/CoroutineScopeWithLifecycleKt { 2 | public static final fun coroutineScope (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/CoroutineScope; 3 | public static synthetic fun coroutineScope$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/CoroutineScope; 4 | public static final fun withLifecycle (Lkotlinx/coroutines/CoroutineScope;Lcom/arkivanov/essenty/lifecycle/Lifecycle;)Lkotlinx/coroutines/CoroutineScope; 5 | } 6 | 7 | public final class com/arkivanov/essenty/lifecycle/coroutines/FlowWithLifecycleKt { 8 | public static final fun flowWithLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; 9 | public static synthetic fun flowWithLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; 10 | public static final fun withLifecycle (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;)Lkotlinx/coroutines/flow/Flow; 11 | public static synthetic fun withLifecycle$default (Lkotlinx/coroutines/flow/Flow;Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;ILjava/lang/Object;)Lkotlinx/coroutines/flow/Flow; 12 | } 13 | 14 | public final class com/arkivanov/essenty/lifecycle/coroutines/RepeatOnLifecycleKt { 15 | public static final fun repeatOnLifecycle (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 16 | public static final fun repeatOnLifecycle (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 17 | public static synthetic fun repeatOnLifecycle$default (Lcom/arkivanov/essenty/lifecycle/Lifecycle;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; 18 | public static synthetic fun repeatOnLifecycle$default (Lcom/arkivanov/essenty/lifecycle/LifecycleOwner;Lcom/arkivanov/essenty/lifecycle/Lifecycle$State;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object; 19 | } 20 | 21 | -------------------------------------------------------------------------------- /lifecycle/src/androidMain/kotlin/com/arkivanov/essenty/lifecycle/AndroidExt.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle 2 | 3 | import androidx.lifecycle.DefaultLifecycleObserver 4 | import androidx.lifecycle.Lifecycle 5 | import androidx.lifecycle.LifecycleObserver 6 | import androidx.lifecycle.LifecycleOwner 7 | import com.arkivanov.essenty.lifecycle.Lifecycle as EssentyLifecycle 8 | 9 | /** 10 | * Converts AndroidX [Lifecycle] to Essenty [Lifecycle][EssentyLifecycle] 11 | */ 12 | fun Lifecycle.asEssentyLifecycle(): EssentyLifecycle = EssentyLifecycleInterop(this) 13 | 14 | /** 15 | * Converts AndroidX [Lifecycle] to Essenty [Lifecycle][EssentyLifecycle] 16 | */ 17 | fun LifecycleOwner.essentyLifecycle(): EssentyLifecycle = lifecycle.asEssentyLifecycle() 18 | 19 | private class EssentyLifecycleInterop( 20 | private val delegate: Lifecycle 21 | ) : EssentyLifecycle { 22 | 23 | private val observerMap = HashMap() 24 | 25 | override val state: EssentyLifecycle.State get() = delegate.currentState.toEssentyLifecycleState() 26 | 27 | override fun subscribe(callbacks: EssentyLifecycle.Callbacks) { 28 | check(callbacks !in observerMap) { "Already subscribed" } 29 | 30 | val observer = AndroidLifecycleObserver(delegate = callbacks, onDestroy = { observerMap -= callbacks }) 31 | observerMap[callbacks] = observer 32 | delegate.addObserver(observer) 33 | } 34 | 35 | override fun unsubscribe(callbacks: EssentyLifecycle.Callbacks) { 36 | observerMap.remove(callbacks)?.also { 37 | delegate.removeObserver(it) 38 | } 39 | } 40 | } 41 | 42 | private fun Lifecycle.State.toEssentyLifecycleState(): EssentyLifecycle.State = 43 | when (this) { 44 | Lifecycle.State.DESTROYED -> EssentyLifecycle.State.DESTROYED 45 | Lifecycle.State.INITIALIZED -> EssentyLifecycle.State.INITIALIZED 46 | Lifecycle.State.CREATED -> EssentyLifecycle.State.CREATED 47 | Lifecycle.State.STARTED -> EssentyLifecycle.State.STARTED 48 | Lifecycle.State.RESUMED -> EssentyLifecycle.State.RESUMED 49 | } 50 | 51 | private class AndroidLifecycleObserver( 52 | private val delegate: EssentyLifecycle.Callbacks, 53 | private val onDestroy: () -> Unit, 54 | ) : DefaultLifecycleObserver { 55 | override fun onCreate(owner: LifecycleOwner) { 56 | delegate.onCreate() 57 | } 58 | 59 | override fun onStart(owner: LifecycleOwner) { 60 | delegate.onStart() 61 | } 62 | 63 | override fun onResume(owner: LifecycleOwner) { 64 | delegate.onResume() 65 | } 66 | 67 | override fun onPause(owner: LifecycleOwner) { 68 | delegate.onPause() 69 | } 70 | 71 | override fun onStop(owner: LifecycleOwner) { 72 | delegate.onStop() 73 | } 74 | 75 | override fun onDestroy(owner: LifecycleOwner) { 76 | delegate.onDestroy() 77 | onDestroy.invoke() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/BackCallback.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.backhandler 2 | 3 | import kotlin.properties.Delegates 4 | 5 | /** 6 | * A callback for back button handling. 7 | * 8 | * @param isEnabled the initial enabled state of the callback. 9 | * @param priority determines the order of callback execution. 10 | * When calling, callbacks are sorted in ascending order first by priority and then by index, 11 | * the last enabled callback gets called. 12 | */ 13 | abstract class BackCallback( 14 | isEnabled: Boolean = true, 15 | var priority: Int = PRIORITY_DEFAULT, 16 | ) { 17 | private var enabledListeners = emptySet<(Boolean) -> Unit>() 18 | 19 | /** 20 | * Controls the enabled state of the callback. 21 | */ 22 | var isEnabled: Boolean by Delegates.observable(isEnabled) { _, _, newValue -> 23 | enabledListeners.forEach { it(newValue) } 24 | } 25 | 26 | /** 27 | * Registers the specified [listener] to be called when the enabled state of the callback changes. 28 | */ 29 | fun addEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) { 30 | this.enabledListeners += listener 31 | } 32 | 33 | /** 34 | * Unregisters the specified [listener]. 35 | */ 36 | fun removeEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) { 37 | this.enabledListeners -= listener 38 | } 39 | 40 | /** 41 | * Called when the back button is pressed, or the predictive back gesture is finished. 42 | */ 43 | abstract fun onBack() 44 | 45 | /** 46 | * Called when the predictive back gesture starts. 47 | */ 48 | open fun onBackStarted(backEvent: BackEvent) { 49 | } 50 | 51 | /** 52 | * Called on every progress of the predictive back gesture. 53 | */ 54 | open fun onBackProgressed(backEvent: BackEvent) { 55 | } 56 | 57 | /** 58 | * Called when the predictive back gesture is cancelled. 59 | */ 60 | open fun onBackCancelled() { 61 | } 62 | 63 | companion object { 64 | const val PRIORITY_DEFAULT: Int = 0 65 | const val PRIORITY_MIN: Int = Int.MIN_VALUE 66 | const val PRIORITY_MAX: Int = Int.MAX_VALUE 67 | } 68 | } 69 | 70 | fun BackCallback( 71 | isEnabled: Boolean = true, 72 | priority: Int = 0, 73 | onBackStarted: ((BackEvent) -> Unit)? = null, 74 | onBackProgressed: ((BackEvent) -> Unit)? = null, 75 | onBackCancelled: (() -> Unit)? = null, 76 | onBack: () -> Unit, 77 | ): BackCallback = 78 | object : BackCallback(isEnabled = isEnabled, priority = priority) { 79 | override fun onBackStarted(backEvent: BackEvent) { 80 | onBackStarted?.invoke(backEvent) 81 | } 82 | 83 | override fun onBackProgressed(backEvent: BackEvent) { 84 | onBackProgressed?.invoke(backEvent) 85 | } 86 | 87 | override fun onBackCancelled() { 88 | onBackCancelled?.invoke() 89 | } 90 | 91 | override fun onBack() { 92 | onBack.invoke() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/BundleExt.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import android.os.Bundle 4 | import android.os.Parcel 5 | import android.os.Parcelable 6 | import kotlinx.serialization.DeserializationStrategy 7 | import kotlinx.serialization.SerializationStrategy 8 | 9 | /** 10 | * Inserts the provided `kotlinx-serialization` [Serializable][kotlinx.serialization.Serializable] value 11 | * into this [Bundle], replacing any existing value for the given [key]. 12 | * Either [key] or [value] may be `null`. 13 | */ 14 | fun Bundle.putSerializable(key: String?, value: T?, strategy: SerializationStrategy) { 15 | putParcelable(key, ValueHolder(value = value, bytes = lazy { value?.serialize(strategy) })) 16 | } 17 | 18 | /** 19 | * Returns a `kotlinx-serialization` [Serializable][kotlinx.serialization.Serializable] associated with 20 | * the given [key], or `null` if no mapping exists for the given [key] or a `null` value is explicitly 21 | * associated with the [key]. 22 | */ 23 | fun Bundle.getSerializable(key: String?, strategy: DeserializationStrategy): T? = 24 | getParcelableCompat>(key)?.let { holder -> 25 | holder.value ?: holder.bytes.value?.deserialize(strategy) 26 | } 27 | 28 | @Suppress("DEPRECATION") 29 | private inline fun Bundle.getParcelableCompat(key: String?): T? = 30 | classLoader.let { savedClassLoader -> 31 | try { 32 | classLoader = T::class.java.classLoader 33 | getParcelable(key) as T? 34 | } finally { 35 | classLoader = savedClassLoader 36 | } 37 | } 38 | 39 | /** 40 | * Inserts the provided [SerializableContainer] into this [Bundle], 41 | * replacing any existing value for the given [key]. Either [key] or [value] may be `null`. 42 | */ 43 | fun Bundle.putSerializableContainer(key: String?, value: SerializableContainer?) { 44 | putSerializable(key = key, value = value, strategy = SerializableContainer.serializer()) 45 | } 46 | 47 | /** 48 | * Returns a [SerializableContainer] associated with the given [key], 49 | * or `null` if no mapping exists for the given [key] or a `null` value 50 | * is explicitly associated with the [key]. 51 | */ 52 | fun Bundle.getSerializableContainer(key: String?): SerializableContainer? = 53 | getSerializable(key = key, strategy = SerializableContainer.serializer()) 54 | 55 | private class ValueHolder( 56 | val value: T?, 57 | val bytes: Lazy, 58 | ) : Parcelable { 59 | override fun writeToParcel(dest: Parcel, flags: Int) { 60 | dest.writeByteArray(bytes.value) 61 | } 62 | 63 | override fun describeContents(): Int = 0 64 | 65 | companion object CREATOR : Parcelable.Creator> { 66 | override fun createFromParcel(parcel: Parcel): ValueHolder = 67 | ValueHolder(value = null, bytes = lazyOf(parcel.createByteArray())) 68 | 69 | override fun newArray(size: Int): Array?> = 70 | arrayOfNulls(size) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /back-handler/src/androidMain/kotlin/com/arkivanov/essenty/backhandler/AndroidBackHandler.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.backhandler 2 | 3 | import androidx.activity.BackEventCompat 4 | import androidx.activity.OnBackPressedCallback 5 | import androidx.activity.OnBackPressedDispatcher 6 | import androidx.activity.OnBackPressedDispatcherOwner 7 | import androidx.lifecycle.LifecycleOwner 8 | 9 | /** 10 | * Creates a new instance of [BackHandler] and attaches it to the provided AndroidX [OnBackPressedDispatcher]. 11 | */ 12 | fun BackHandler(onBackPressedDispatcher: OnBackPressedDispatcher): BackHandler = 13 | BackDispatcher().also { dispatcher -> 14 | onBackPressedDispatcher.addCallback(dispatcher.connectOnBackPressedCallback()) 15 | } 16 | 17 | /** 18 | * Creates a new instance of [BackHandler] and attaches it to the provided AndroidX [OnBackPressedDispatcher] 19 | * only when the [LifecycleOwner]'s Lifecycle is [STARTED][androidx.lifecycle.Lifecycle.State.STARTED]. 20 | */ 21 | fun BackHandler( 22 | onBackPressedDispatcher: OnBackPressedDispatcher, 23 | lifecycleOwner: LifecycleOwner, 24 | ): BackHandler = 25 | BackDispatcher().also { dispatcher -> 26 | onBackPressedDispatcher.addCallback(lifecycleOwner, dispatcher.connectOnBackPressedCallback()) 27 | } 28 | 29 | /** 30 | * Creates a new instance of [BackHandler] and attaches it to the AndroidX [OnBackPressedDispatcher]. 31 | */ 32 | fun OnBackPressedDispatcherOwner.backHandler(): BackHandler = 33 | BackHandler(onBackPressedDispatcher = onBackPressedDispatcher) 34 | 35 | /** 36 | * Creates a new instance of [OnBackPressedCallback] and connects it with this [BackDispatcher]. 37 | * All events from the returned [OnBackPressedCallback] are forwarded to this [BackDispatcher]. 38 | * The enabled state from this [BackDispatcher] is forwarded to the returned [OnBackPressedCallback]. 39 | */ 40 | fun BackDispatcher.connectOnBackPressedCallback(): OnBackPressedCallback = 41 | OnBackPressedCallbackAdapter(dispatcher = this) 42 | 43 | private class OnBackPressedCallbackAdapter( 44 | private val dispatcher: BackDispatcher, 45 | ) : OnBackPressedCallback(enabled = dispatcher.isEnabled) { 46 | 47 | init { 48 | dispatcher.addEnabledChangedListener { isEnabled = it } 49 | } 50 | 51 | override fun handleOnBackPressed() { 52 | dispatcher.back() 53 | } 54 | 55 | override fun handleOnBackStarted(backEvent: BackEventCompat) { 56 | dispatcher.startPredictiveBack(backEvent.toEssentyBackEvent()) 57 | } 58 | 59 | override fun handleOnBackProgressed(backEvent: BackEventCompat) { 60 | dispatcher.progressPredictiveBack(backEvent.toEssentyBackEvent()) 61 | } 62 | 63 | override fun handleOnBackCancelled() { 64 | dispatcher.cancelPredictiveBack() 65 | } 66 | 67 | private fun BackEventCompat.toEssentyBackEvent(): BackEvent = 68 | BackEvent( 69 | progress = progress, 70 | swipeEdge = when (swipeEdge) { 71 | BackEventCompat.EDGE_LEFT -> BackEvent.SwipeEdge.LEFT 72 | BackEventCompat.EDGE_RIGHT -> BackEvent.SwipeEdge.RIGHT 73 | else -> BackEvent.SwipeEdge.UNKNOWN 74 | }, 75 | touchX = touchX, 76 | touchY = touchY, 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /back-handler/src/commonMain/kotlin/com/arkivanov/essenty/backhandler/DefaultBackDispatcher.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.backhandler 2 | 3 | internal class DefaultBackDispatcher : BackDispatcher { 4 | 5 | private var set = emptySet() 6 | private var progressData: ProgressData? = null 7 | override val isEnabled: Boolean get() = set.any(BackCallback::isEnabled) 8 | private var enabledChangedListeners = emptySet<(Boolean) -> Unit>() 9 | private var hasEnabledCallback: Boolean = false 10 | private val onCallbackEnabledChanged: (Boolean) -> Unit = { onCallbackEnabledChanged() } 11 | 12 | private fun onCallbackEnabledChanged() { 13 | val hasEnabledCallback = isEnabled 14 | if (this.hasEnabledCallback != hasEnabledCallback) { 15 | this.hasEnabledCallback = hasEnabledCallback 16 | enabledChangedListeners.forEach { it.invoke(hasEnabledCallback) } 17 | } 18 | } 19 | 20 | override fun isRegistered(callback: BackCallback): Boolean = 21 | callback in set 22 | 23 | override fun register(callback: BackCallback) { 24 | check(callback !in set) { "Callback is already registered" } 25 | 26 | this.set += callback 27 | callback.addEnabledChangedListener(onCallbackEnabledChanged) 28 | onCallbackEnabledChanged() 29 | } 30 | 31 | override fun unregister(callback: BackCallback) { 32 | check(callback in set) { "Callback is not registered" } 33 | 34 | this.set -= callback 35 | callback.removeEnabledChangedListener(onCallbackEnabledChanged) 36 | 37 | if (callback == progressData?.callback) { 38 | progressData?.callback = null 39 | callback.onBackCancelled() 40 | } 41 | 42 | onCallbackEnabledChanged() 43 | } 44 | 45 | override fun addEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) { 46 | enabledChangedListeners += listener 47 | } 48 | 49 | override fun removeEnabledChangedListener(listener: (isEnabled: Boolean) -> Unit) { 50 | enabledChangedListeners -= listener 51 | } 52 | 53 | override fun back(): Boolean { 54 | val callback = progressData?.callback ?: set.findMostImportant() 55 | progressData = null 56 | callback?.onBack() 57 | 58 | return callback != null 59 | } 60 | 61 | override fun startPredictiveBack(backEvent: BackEvent): Boolean { 62 | val callback = set.findMostImportant() ?: return false 63 | progressData = ProgressData(startEvent = backEvent, callback = callback) 64 | callback.onBackStarted(backEvent) 65 | 66 | return true 67 | } 68 | 69 | override fun progressPredictiveBack(backEvent: BackEvent) { 70 | val progressData = progressData ?: return 71 | 72 | if (progressData.callback == null) { 73 | progressData.callback = set.findMostImportant() 74 | progressData.callback?.onBackStarted(progressData.startEvent) 75 | } 76 | 77 | progressData.callback?.onBackProgressed(backEvent) 78 | } 79 | 80 | override fun cancelPredictiveBack() { 81 | progressData?.callback?.onBackCancelled() 82 | progressData = null 83 | } 84 | 85 | private class ProgressData( 86 | val startEvent: BackEvent, 87 | var callback: BackCallback?, 88 | ) 89 | } 90 | -------------------------------------------------------------------------------- /lifecycle-coroutines/api/lifecycle-coroutines.klib.api: -------------------------------------------------------------------------------- 1 | // Klib ABI Dump 2 | // Targets: [iosArm64, iosSimulatorArm64, iosX64, js, linuxX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosSimulatorArm64, watchosX64] 3 | // Rendering settings: 4 | // - Signature version: 2 5 | // - Show manifest properties: true 6 | // - Show declarations: true 7 | 8 | // Library unique name: 9 | final fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle.coroutines/coroutineScope(kotlin.coroutines/CoroutineContext = ...): kotlinx.coroutines/CoroutineScope // com.arkivanov.essenty.lifecycle.coroutines/coroutineScope|coroutineScope@com.arkivanov.essenty.lifecycle.LifecycleOwner(kotlin.coroutines.CoroutineContext){}[0] 10 | final fun (kotlinx.coroutines/CoroutineScope).com.arkivanov.essenty.lifecycle.coroutines/withLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle): kotlinx.coroutines/CoroutineScope // com.arkivanov.essenty.lifecycle.coroutines/withLifecycle|withLifecycle@kotlinx.coroutines.CoroutineScope(com.arkivanov.essenty.lifecycle.Lifecycle){}[0] 11 | final fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).com.arkivanov.essenty.lifecycle.coroutines/flowWithLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle, com.arkivanov.essenty.lifecycle/Lifecycle.State = ..., kotlin.coroutines/CoroutineContext = ...): kotlinx.coroutines.flow/Flow<#A> // com.arkivanov.essenty.lifecycle.coroutines/flowWithLifecycle|flowWithLifecycle@kotlinx.coroutines.flow.Flow<0:0>(com.arkivanov.essenty.lifecycle.Lifecycle;com.arkivanov.essenty.lifecycle.Lifecycle.State;kotlin.coroutines.CoroutineContext){0§}[0] 12 | final fun <#A: kotlin/Any?> (kotlinx.coroutines.flow/Flow<#A>).com.arkivanov.essenty.lifecycle.coroutines/withLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle, com.arkivanov.essenty.lifecycle/Lifecycle.State = ..., kotlin.coroutines/CoroutineContext = ...): kotlinx.coroutines.flow/Flow<#A> // com.arkivanov.essenty.lifecycle.coroutines/withLifecycle|withLifecycle@kotlinx.coroutines.flow.Flow<0:0>(com.arkivanov.essenty.lifecycle.Lifecycle;com.arkivanov.essenty.lifecycle.Lifecycle.State;kotlin.coroutines.CoroutineContext){0§}[0] 13 | final suspend fun (com.arkivanov.essenty.lifecycle/Lifecycle).com.arkivanov.essenty.lifecycle.coroutines/repeatOnLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle.State = ..., kotlin.coroutines/CoroutineContext = ..., kotlin.coroutines/SuspendFunction1) // com.arkivanov.essenty.lifecycle.coroutines/repeatOnLifecycle|repeatOnLifecycle@com.arkivanov.essenty.lifecycle.Lifecycle(com.arkivanov.essenty.lifecycle.Lifecycle.State;kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1){}[0] 14 | final suspend fun (com.arkivanov.essenty.lifecycle/LifecycleOwner).com.arkivanov.essenty.lifecycle.coroutines/repeatOnLifecycle(com.arkivanov.essenty.lifecycle/Lifecycle.State = ..., kotlin.coroutines/CoroutineContext = ..., kotlin.coroutines/SuspendFunction1) // com.arkivanov.essenty.lifecycle.coroutines/repeatOnLifecycle|repeatOnLifecycle@com.arkivanov.essenty.lifecycle.LifecycleOwner(com.arkivanov.essenty.lifecycle.Lifecycle.State;kotlin.coroutines.CoroutineContext;kotlin.coroutines.SuspendFunction1){}[0] 15 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/SerializableContainer.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import com.arkivanov.essenty.statekeeper.base64.base64ToByteArray 4 | import com.arkivanov.essenty.statekeeper.base64.toBase64 5 | import kotlinx.serialization.DeserializationStrategy 6 | import kotlinx.serialization.KSerializer 7 | import kotlinx.serialization.Serializable 8 | import kotlinx.serialization.SerializationStrategy 9 | import kotlinx.serialization.descriptors.PrimitiveKind 10 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 11 | import kotlinx.serialization.descriptors.SerialDescriptor 12 | import kotlinx.serialization.encoding.Decoder 13 | import kotlinx.serialization.encoding.Encoder 14 | 15 | /** 16 | * Represents a lazy [Serializable][kotlinx.serialization.Serializable] container for a `Serializable` object. 17 | */ 18 | @Serializable(with = SerializableContainer.Serializer::class) 19 | class SerializableContainer private constructor( 20 | private var data: ByteArray?, 21 | ) { 22 | constructor() : this(data = null) 23 | 24 | private var holder: Holder<*>? = null 25 | 26 | /** 27 | * Deserializes and returns a previously stored [Serializable][kotlinx.serialization.Serializable] object. 28 | * 29 | * @param strategy a [DeserializationStrategy] for deserializing the object. 30 | */ 31 | fun consume(strategy: DeserializationStrategy): T? { 32 | val consumedValue: Any? = holder?.value ?: data?.deserialize(strategy) 33 | holder = null 34 | data = null 35 | 36 | @Suppress("UNCHECKED_CAST") 37 | return consumedValue as T? 38 | } 39 | 40 | /** 41 | * Stores a [Serializable][kotlinx.serialization.Serializable] object, replacing any previously stored object. 42 | * 43 | * @param value an object to be stored and serialized later when needed. 44 | * @param strategy a [SerializationStrategy] for serializing the value. 45 | */ 46 | fun set(value: T?, strategy: SerializationStrategy) { 47 | holder = Holder(value = value, strategy = strategy) 48 | data = null 49 | } 50 | 51 | /** 52 | * Clears any previously stored object. 53 | */ 54 | fun clear() { 55 | holder = null 56 | data = null 57 | } 58 | 59 | private class Holder( 60 | val value: T?, 61 | val strategy: SerializationStrategy, 62 | ) 63 | 64 | internal object Serializer : KSerializer { 65 | private const val NULL_MARKER = "." 66 | override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("SerializableContainer", PrimitiveKind.STRING) 67 | 68 | override fun serialize(encoder: Encoder, value: SerializableContainer) { 69 | val bytes = value.holder?.serialize() ?: value.data 70 | encoder.encodeString(bytes?.toBase64() ?: NULL_MARKER) 71 | } 72 | 73 | private fun Holder.serialize(): ByteArray? = 74 | value?.serialize(strategy) 75 | 76 | override fun deserialize(decoder: Decoder): SerializableContainer = 77 | SerializableContainer(data = decoder.decodeString().takeUnless { it == NULL_MARKER }?.base64ToByteArray()) 78 | } 79 | } 80 | 81 | /** 82 | * Creates a new [SerializableContainer] and sets the provided [value] with the provided [strategy]. 83 | */ 84 | fun SerializableContainer( 85 | value: T?, 86 | strategy: SerializationStrategy 87 | ): SerializableContainer = 88 | SerializableContainer().apply { 89 | set(value = value, strategy = strategy) 90 | } 91 | 92 | /** 93 | * A convenience method for [SerializableContainer.consume]. Throws [IllegalStateException] 94 | * if the [SerializableContainer] is empty. 95 | */ 96 | fun SerializableContainer.consumeRequired(strategy: DeserializationStrategy): T = 97 | checkNotNull(consume(strategy)) 98 | -------------------------------------------------------------------------------- /state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/StateKeeperExtTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlinx.serialization.builtins.nullable 4 | import kotlinx.serialization.builtins.serializer 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertNull 8 | 9 | @OptIn(ExperimentalStateKeeperApi::class) 10 | class StateKeeperExtTest { 11 | 12 | @Test 13 | fun saveable_holder_saves_and_restores_state() { 14 | val oldStateKeeper = StateKeeperDispatcher() 15 | val oldComponent = ComponentWithStateHolder(oldStateKeeper) 16 | 17 | oldComponent.holder.state++ 18 | 19 | val savedState = oldStateKeeper.save().serializeAndDeserialize() 20 | val newStateKeeper = StateKeeperDispatcher(savedState = savedState) 21 | val newComponent = ComponentWithStateHolder(newStateKeeper) 22 | 23 | assertEquals(1, newComponent.holder.state) 24 | } 25 | 26 | @Test 27 | fun saveable_holder_saves_and_restores_nullable_state() { 28 | val oldStateKeeper = StateKeeperDispatcher() 29 | val oldComponent = ComponentWithStateHolder(oldStateKeeper) 30 | 31 | oldComponent.nullableHolder.state = 1 32 | 33 | val savedState = oldStateKeeper.save().serializeAndDeserialize() 34 | val newStateKeeper = StateKeeperDispatcher(savedState = savedState) 35 | val newComponent = ComponentWithStateHolder(newStateKeeper) 36 | 37 | assertEquals(1, newComponent.nullableHolder.state) 38 | } 39 | 40 | @Test 41 | fun saveable_property_saves_and_restores_state() { 42 | val oldStateKeeper = StateKeeperDispatcher() 43 | val oldComponent = ComponentWithState(oldStateKeeper) 44 | 45 | oldComponent.state++ 46 | 47 | val savedState = oldStateKeeper.save().serializeAndDeserialize() 48 | val newStateKeeper = StateKeeperDispatcher(savedState = savedState) 49 | val newComponent = ComponentWithState(newStateKeeper) 50 | 51 | assertEquals(1, newComponent.state) 52 | } 53 | 54 | @Test 55 | fun saveable_property_saves_and_restores_nullable_state_1() { 56 | val oldStateKeeper = StateKeeperDispatcher() 57 | val oldComponent = ComponentWithState(oldStateKeeper) 58 | 59 | oldComponent.nullableState1 = null 60 | 61 | val savedState = oldStateKeeper.save().serializeAndDeserialize() 62 | val newStateKeeper = StateKeeperDispatcher(savedState = savedState) 63 | val newComponent = ComponentWithState(newStateKeeper) 64 | 65 | assertNull(newComponent.nullableState1) 66 | } 67 | 68 | @Test 69 | fun saveable_property_saves_and_restores_nullable_state_2() { 70 | val oldStateKeeper = StateKeeperDispatcher() 71 | val oldComponent = ComponentWithState(oldStateKeeper) 72 | 73 | oldComponent.nullableState2 = 1 74 | 75 | val savedState = oldStateKeeper.save().serializeAndDeserialize() 76 | val newStateKeeper = StateKeeperDispatcher(savedState = savedState) 77 | val newComponent = ComponentWithState(newStateKeeper) 78 | 79 | assertEquals(1, newComponent.nullableState2) 80 | } 81 | 82 | private class ComponentWithStateHolder(override val stateKeeper: StateKeeper) : StateKeeperOwner { 83 | val holder by saveable(serializer = Int.serializer(), state = Holder::state) { 84 | Holder(state = it ?: 0) 85 | } 86 | 87 | val nullableHolder by saveable(serializer = Int.serializer().nullable, state = NullableHolder::state) { 88 | NullableHolder(state = it) 89 | } 90 | } 91 | 92 | private class ComponentWithState(override val stateKeeper: StateKeeper) : StateKeeperOwner { 93 | var state: Int by saveable(serializer = Int.serializer()) { 0 } 94 | var nullableState1: Int? by saveable(serializer = Int.serializer().nullable) { 0 } 95 | var nullableState2: Int? by saveable(serializer = Int.serializer().nullable) { null } 96 | } 97 | 98 | private class Holder(var state: Int) 99 | private class NullableHolder(var state: Int?) 100 | } 101 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | 6 | [{*.kt, *.kts}] 7 | max_line_length = 140 8 | ij_kotlin_packages_to_use_import_on_demand = ^ 9 | ij_continuation_indent_size = 4 10 | ij_kotlin_align_in_columns_case_branch = false 11 | ij_kotlin_align_multiline_binary_operation = false 12 | ij_kotlin_align_multiline_extends_list = false 13 | ij_kotlin_align_multiline_method_parentheses = false 14 | ij_kotlin_align_multiline_parameters = true 15 | ij_kotlin_align_multiline_parameters_in_calls = false 16 | ij_kotlin_allow_trailing_comma = false 17 | ij_kotlin_allow_trailing_comma_on_call_site = false 18 | ij_kotlin_assignment_wrap = normal 19 | ij_kotlin_blank_lines_after_class_header = 0 20 | ij_kotlin_blank_lines_around_block_when_branches = 0 21 | ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 22 | ij_kotlin_block_comment_at_first_column = true 23 | ij_kotlin_call_parameters_new_line_after_left_paren = true 24 | ij_kotlin_call_parameters_right_paren_on_new_line = true 25 | ij_kotlin_call_parameters_wrap = on_every_item 26 | ij_kotlin_catch_on_new_line = false 27 | ij_kotlin_class_annotation_wrap = split_into_lines 28 | ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL 29 | ij_kotlin_continuation_indent_for_chained_calls = false 30 | ij_kotlin_continuation_indent_for_expression_bodies = false 31 | ij_kotlin_continuation_indent_in_argument_lists = false 32 | ij_kotlin_continuation_indent_in_elvis = false 33 | ij_kotlin_continuation_indent_in_if_conditions = false 34 | ij_kotlin_continuation_indent_in_parameter_lists = false 35 | ij_kotlin_continuation_indent_in_supertype_lists = false 36 | ij_kotlin_else_on_new_line = false 37 | ij_kotlin_enum_constants_wrap = off 38 | ij_kotlin_extends_list_wrap = normal 39 | ij_kotlin_field_annotation_wrap = split_into_lines 40 | ij_kotlin_finally_on_new_line = false 41 | ij_kotlin_if_rparen_on_new_line = true 42 | ij_kotlin_import_nested_classes = false 43 | ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ 44 | ij_kotlin_insert_whitespaces_in_simple_one_line_method = true 45 | ij_kotlin_keep_blank_lines_before_right_brace = 2 46 | ij_kotlin_keep_blank_lines_in_code = 2 47 | ij_kotlin_keep_blank_lines_in_declarations = 2 48 | ij_kotlin_keep_first_column_comment = true 49 | ij_kotlin_keep_indents_on_empty_lines = false 50 | ij_kotlin_keep_line_breaks = true 51 | ij_kotlin_lbrace_on_next_line = false 52 | ij_kotlin_line_comment_add_space = false 53 | ij_kotlin_line_comment_at_first_column = true 54 | ij_kotlin_method_annotation_wrap = split_into_lines 55 | ij_kotlin_method_call_chain_wrap = normal 56 | ij_kotlin_method_parameters_new_line_after_left_paren = true 57 | ij_kotlin_method_parameters_right_paren_on_new_line = true 58 | ij_kotlin_method_parameters_wrap = on_every_item 59 | ij_kotlin_name_count_to_use_star_import = 2147483647 60 | ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 61 | ij_kotlin_parameter_annotation_wrap = off 62 | ij_kotlin_space_after_comma = true 63 | ij_kotlin_space_after_extend_colon = true 64 | ij_kotlin_space_after_type_colon = true 65 | ij_kotlin_space_before_catch_parentheses = true 66 | ij_kotlin_space_before_comma = false 67 | ij_kotlin_space_before_extend_colon = true 68 | ij_kotlin_space_before_for_parentheses = true 69 | ij_kotlin_space_before_if_parentheses = true 70 | ij_kotlin_space_before_lambda_arrow = true 71 | ij_kotlin_space_before_type_colon = false 72 | ij_kotlin_space_before_when_parentheses = true 73 | ij_kotlin_space_before_while_parentheses = true 74 | ij_kotlin_spaces_around_additive_operators = true 75 | ij_kotlin_spaces_around_assignment_operators = true 76 | ij_kotlin_spaces_around_equality_operators = true 77 | ij_kotlin_spaces_around_function_type_arrow = true 78 | ij_kotlin_spaces_around_logical_operators = true 79 | ij_kotlin_spaces_around_multiplicative_operators = true 80 | ij_kotlin_spaces_around_range = false 81 | ij_kotlin_spaces_around_relational_operators = true 82 | ij_kotlin_spaces_around_unary_operator = false 83 | ij_kotlin_spaces_around_when_arrow = true 84 | ij_kotlin_variable_annotation_wrap = off 85 | ij_kotlin_while_on_new_line = false 86 | ij_kotlin_wrap_elvis_expressions = 1 87 | ij_kotlin_wrap_expression_body_functions = 1 88 | ij_kotlin_wrap_first_method_in_call_chain = false 89 | -------------------------------------------------------------------------------- /state-keeper/src/commonMain/kotlin/com/arkivanov/essenty/statekeeper/PolymorphicSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlinx.serialization.ExperimentalSerializationApi 4 | import kotlinx.serialization.KSerializer 5 | import kotlinx.serialization.descriptors.SerialDescriptor 6 | import kotlinx.serialization.descriptors.SerialKind 7 | import kotlinx.serialization.descriptors.buildClassSerialDescriptor 8 | import kotlinx.serialization.descriptors.element 9 | import kotlinx.serialization.encoding.CompositeDecoder 10 | import kotlinx.serialization.encoding.Decoder 11 | import kotlinx.serialization.encoding.Encoder 12 | import kotlinx.serialization.encoding.decodeStructure 13 | import kotlinx.serialization.encoding.encodeStructure 14 | import kotlinx.serialization.modules.SerializersModule 15 | import kotlin.reflect.KClass 16 | 17 | /** 18 | * Creates a polymorphic [KSerializer] for the specified class of type [T] using the specified [module]. 19 | */ 20 | @ExperimentalStateKeeperApi 21 | @ExperimentalSerializationApi 22 | inline fun polymorphicSerializer(module: SerializersModule): KSerializer = 23 | polymorphicSerializer(baseClass = T::class, module = module) 24 | 25 | /** 26 | * Creates a polymorphic [KSerializer] for the specified [baseClass] class using the specified [module]. 27 | */ 28 | @ExperimentalStateKeeperApi 29 | @ExperimentalSerializationApi 30 | fun polymorphicSerializer(baseClass: KClass, module: SerializersModule): KSerializer = 31 | PolymorphicSerializer(baseClass = baseClass, module = module) 32 | 33 | @ExperimentalSerializationApi 34 | private class PolymorphicSerializer( 35 | private val baseClass: KClass, 36 | private val module: SerializersModule, 37 | ) : KSerializer { 38 | override val descriptor: SerialDescriptor = 39 | buildClassSerialDescriptor("PolymorphicSerializer") { 40 | element("type") 41 | element("value", ContextualSerialDescriptor) 42 | } 43 | 44 | override fun serialize(encoder: Encoder, value: T) { 45 | val serializer = requireNotNull(module.getPolymorphic(baseClass, value)) 46 | encoder.encodeStructure(descriptor) { 47 | encodeStringElement(descriptor, 0, serializer.descriptor.serialName) 48 | encodeSerializableElement(descriptor, 1, serializer, value) 49 | } 50 | } 51 | 52 | override fun deserialize(decoder: Decoder): T = 53 | decoder.decodeStructure(descriptor) { 54 | var className: String? = null 55 | var value: T? = null 56 | 57 | while (true) { 58 | when (val index = decodeElementIndex(descriptor)) { 59 | 0 -> className = decodeStringElement(descriptor, index) 60 | 61 | 1 -> { 62 | val actualClassName = requireNotNull(className) 63 | val serializer = requireNotNull(module.getPolymorphic(baseClass, actualClassName)) 64 | value = decodeSerializableElement(descriptor, 1, serializer) 65 | } 66 | 67 | CompositeDecoder.DECODE_DONE -> break 68 | 69 | else -> error("Unsupported index: $index") 70 | } 71 | } 72 | 73 | requireNotNull(value) 74 | } 75 | 76 | private object ContextualSerialDescriptor : SerialDescriptor { 77 | override val elementsCount: Int = 0 78 | override val kind: SerialKind = SerialKind.CONTEXTUAL 79 | override val serialName: String = "Value" 80 | 81 | override fun getElementAnnotations(index: Int): List = elementNotFoundError(index) 82 | override fun getElementDescriptor(index: Int): SerialDescriptor = elementNotFoundError(index) 83 | override fun getElementIndex(name: String): Int = CompositeDecoder.UNKNOWN_NAME 84 | override fun getElementName(index: Int): String = elementNotFoundError(index) 85 | override fun isElementOptional(index: Int): Boolean = elementNotFoundError(index) 86 | 87 | private fun elementNotFoundError(index: Int): Nothing { 88 | throw IndexOutOfBoundsException("Element at index $index not found") 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /state-keeper/src/androidUnitTest/kotlin/com/arkivanov/essenty/statekeeper/AndroidStateKeeperTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import android.os.Bundle 4 | import android.os.Parcel 5 | import androidx.lifecycle.Lifecycle 6 | import androidx.lifecycle.LifecycleRegistry 7 | import androidx.savedstate.SavedStateRegistry 8 | import androidx.savedstate.SavedStateRegistryController 9 | import androidx.savedstate.SavedStateRegistryOwner 10 | import kotlinx.serialization.builtins.serializer 11 | import org.junit.runner.RunWith 12 | import org.robolectric.RobolectricTestRunner 13 | import kotlin.test.Test 14 | import kotlin.test.assertEquals 15 | import kotlin.test.assertNull 16 | 17 | @Suppress("TestFunctionName") 18 | @RunWith(RobolectricTestRunner::class) 19 | class AndroidStateKeeperTest { 20 | 21 | @Test 22 | fun saves_and_restores_state_without_parcelling() { 23 | var savedStateRegistryOwner = TestSavedStateRegistryOwner() 24 | savedStateRegistryOwner.controller.performRestore(null) 25 | var stateKeeper = savedStateRegistryOwner.stateKeeper() 26 | stateKeeper.register(key = "key", strategy = String.serializer()) { "data" } 27 | val bundle = Bundle() 28 | savedStateRegistryOwner.controller.performSave(bundle) 29 | 30 | savedStateRegistryOwner = TestSavedStateRegistryOwner() 31 | savedStateRegistryOwner.controller.performRestore(bundle) 32 | stateKeeper = StateKeeper(savedStateRegistry = savedStateRegistryOwner.savedStateRegistry) 33 | val restoredData = stateKeeper.consume(key = "key", strategy = String.serializer()) 34 | 35 | assertEquals("data", restoredData) 36 | } 37 | 38 | @Test 39 | fun saves_and_restores_state_with_parcelling() { 40 | var savedStateRegistryOwner = TestSavedStateRegistryOwner() 41 | savedStateRegistryOwner.controller.performRestore(null) 42 | var stateKeeper = savedStateRegistryOwner.stateKeeper() 43 | stateKeeper.register(key = "key", strategy = String.serializer()) { "data" } 44 | val bundle = Bundle() 45 | savedStateRegistryOwner.controller.performSave(bundle) 46 | 47 | savedStateRegistryOwner = TestSavedStateRegistryOwner() 48 | savedStateRegistryOwner.controller.performRestore(bundle.parcelize().deparcelize()) 49 | stateKeeper = StateKeeper(savedStateRegistry = savedStateRegistryOwner.savedStateRegistry) 50 | val restoredData = stateKeeper.consume(key = "key", strategy = String.serializer()) 51 | 52 | assertEquals("data", restoredData) 53 | } 54 | 55 | @Test 56 | fun GIVEN_isSavingAllowed_is_false_on_save_THEN_state_not_saved() { 57 | val savedStateRegistryOwner = TestSavedStateRegistryOwner() 58 | savedStateRegistryOwner.controller.performRestore(null) 59 | val stateKeeper = savedStateRegistryOwner.stateKeeper(isSavingAllowed = { false }) 60 | stateKeeper.register(key = "key", strategy = String.serializer()) { throw IllegalStateException("Must not be called") } 61 | val bundle = Bundle() 62 | 63 | savedStateRegistryOwner.controller.performSave(bundle) 64 | } 65 | 66 | @Test 67 | fun GIVEN_discardSavedState_is_true_on_restore_THEN_discards_saved_state() { 68 | var savedStateRegistryOwner = TestSavedStateRegistryOwner() 69 | savedStateRegistryOwner.controller.performRestore(null) 70 | var stateKeeper = savedStateRegistryOwner.stateKeeper() 71 | stateKeeper.register(key = "key", strategy = String.serializer()) { "data" } 72 | val bundle = Bundle() 73 | savedStateRegistryOwner.controller.performSave(bundle) 74 | 75 | savedStateRegistryOwner = TestSavedStateRegistryOwner() 76 | savedStateRegistryOwner.controller.performRestore(bundle.parcelize().deparcelize()) 77 | stateKeeper = savedStateRegistryOwner.stateKeeper(discardSavedState = true) 78 | val restoredData = stateKeeper.consume(key = "key", strategy = String.serializer()) 79 | 80 | assertNull(restoredData) 81 | } 82 | 83 | private class TestSavedStateRegistryOwner : SavedStateRegistryOwner { 84 | val controller: SavedStateRegistryController = SavedStateRegistryController.create(this) 85 | 86 | override val lifecycle: Lifecycle = LifecycleRegistry(this) 87 | override val savedStateRegistry: SavedStateRegistry get() = controller.savedStateRegistry 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /state-keeper-benchmarks/src/test/kotlin/com/arkivanov/essenty/statekeeper/benchmarks/Benchmarks.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") 2 | 3 | package com.arkivanov.essenty.statekeeper.benchmarks 4 | 5 | import android.os.Build 6 | import android.os.Bundle 7 | import android.os.Parcel 8 | import android.os.Parcelable 9 | import com.arkivanov.essenty.statekeeper.deserialize 10 | import com.arkivanov.essenty.statekeeper.serialize 11 | import kotlinx.parcelize.Parcelize 12 | import kotlinx.serialization.Serializable 13 | import org.robolectric.annotation.Config 14 | import kotlin.test.assertEquals 15 | import kotlin.time.measureTime 16 | 17 | //@RunWith(RobolectricTestRunner::class) 18 | @Config(minSdk = Build.VERSION_CODES.TIRAMISU) 19 | class Benchmarks { 20 | 21 | // Manual run only 22 | // @Test 23 | fun size() { 24 | val data = getData() 25 | println("Parcelable size: ${data.getParcelizedSize()}") 26 | println("Serializable size: ${data.getSerializedSize()}") 27 | 28 | val newDataParcelable = data.parcelize().deparcelize() 29 | val newDataSerializable = data.serialize(Data.serializer()).deserialize(Data.serializer()) 30 | 31 | assertEquals(data, newDataParcelable) 32 | assertEquals(data, newDataSerializable) 33 | } 34 | 35 | // Manual run only 36 | // @Test 37 | fun performance() { 38 | val data = getData() 39 | 40 | repeat(100) { 41 | data.parcelize().deparcelize() 42 | data.serialize(Data.serializer()).deserialize(Data.serializer()) 43 | } 44 | 45 | val t1 = 46 | measureTime { 47 | repeat(100) { 48 | data.parcelize().deparcelize() 49 | } 50 | } 51 | 52 | val t2 = 53 | measureTime { 54 | repeat(100) { 55 | data.serialize(Data.serializer()).deserialize(Data.serializer()) 56 | } 57 | } 58 | 59 | println("Parcelize time: $t1") 60 | println("Serialize time: $t2") 61 | } 62 | 63 | private fun Data.getParcelizedSize(): Int = 64 | parcelize().size 65 | 66 | private fun Data.parcelize(): ByteArray { 67 | val bundle = Bundle() 68 | bundle.putParcelable("key", this) 69 | val parcel = Parcel.obtain() 70 | parcel.writeBundle(bundle) 71 | return parcel.marshall() 72 | } 73 | 74 | private fun ByteArray.deparcelize(): Data { 75 | val parcel = Parcel.obtain() 76 | parcel.unmarshall(this, 0, size) 77 | parcel.setDataPosition(0) 78 | 79 | return requireNotNull(parcel.readBundle()).getParcelable("key", Data::class.java)!! 80 | } 81 | 82 | private fun Data.getSerializedSize(): Int = 83 | serialize(Data.serializer()).size 84 | 85 | private fun getData(): Data = 86 | getInnerData( 87 | dataList = List(30) { 88 | getInnerData( 89 | dataList = List(10) { 90 | getInnerData() 91 | }, 92 | ) 93 | }, 94 | ) 95 | 96 | private fun getInnerData(dataList: List = emptyList()): Data = 97 | Data( 98 | booleanValue = true, 99 | byteValue = 64, 100 | shortValue = 8192, 101 | integerValue = 1234567, 102 | longValue = 12345678912345, 103 | floatValue = 100F, 104 | doubleValue = 100.0, 105 | charValue = 'a', 106 | stringValue = "Some string data goes here", 107 | intList = List(100) { it }, 108 | stringList = List(100) { "Some string data goes here" }, 109 | dataList = dataList, 110 | ) 111 | 112 | @Serializable 113 | @Parcelize 114 | data class Data( 115 | val booleanValue: Boolean, 116 | val byteValue: Byte, 117 | val shortValue: Short, 118 | val integerValue: Int, 119 | val longValue: Long, 120 | val floatValue: Float, 121 | val doubleValue: Double, 122 | val charValue: Char, 123 | val stringValue: String, 124 | val intList: List, 125 | val stringList: List, 126 | val dataList: List, 127 | ) : Parcelable 128 | } 129 | -------------------------------------------------------------------------------- /state-keeper/src/androidMain/kotlin/com/arkivanov/essenty/statekeeper/AndroidExt.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import android.os.Bundle 4 | import androidx.savedstate.SavedStateRegistry 5 | import androidx.savedstate.SavedStateRegistryOwner 6 | 7 | private const val KEY_STATE = "STATE_KEEPER_STATE" 8 | 9 | /** 10 | * Creates a new instance of [StateKeeper] and attaches it to the provided AndroidX [SavedStateRegistry]. 11 | * 12 | * @param savedStateRegistry a [SavedStateRegistry] to attach the returned [StateKeeper] to. 13 | * @param discardSavedState a flag indicating whether any previously saved state should be discarded or not, 14 | * default value is `false`. 15 | * @param isSavingAllowed called before saving the state. 16 | * When `true` then the state will be saved, otherwise it won't. Default value is `true`. 17 | */ 18 | fun StateKeeper( 19 | savedStateRegistry: SavedStateRegistry, 20 | discardSavedState: Boolean = false, 21 | isSavingAllowed: () -> Boolean = { true }, 22 | ): StateKeeper = 23 | StateKeeper( 24 | savedStateRegistry = savedStateRegistry, 25 | key = KEY_STATE, 26 | discardSavedState = discardSavedState, 27 | isSavingAllowed = isSavingAllowed, 28 | ) 29 | 30 | /** 31 | * Creates a new instance of [StateKeeper] and attaches it to the provided AndroidX [SavedStateRegistry]. 32 | * 33 | * @param savedStateRegistry a [SavedStateRegistry] to attach the returned [StateKeeper] to. 34 | * @param key a key to access the provided [SavedStateRegistry], to be used by the returned [StateKeeper]. 35 | * @param discardSavedState a flag indicating whether any previously saved state should be discarded or not, 36 | * default value is `false`. 37 | * @param isSavingAllowed called before saving the state. 38 | * When `true` then the state will be saved, otherwise it won't. Default value is `true`. 39 | */ 40 | fun StateKeeper( 41 | savedStateRegistry: SavedStateRegistry, 42 | key: String, 43 | discardSavedState: Boolean = false, 44 | isSavingAllowed: () -> Boolean = { true }, 45 | ): StateKeeper { 46 | val dispatcher = 47 | StateKeeperDispatcher( 48 | savedState = savedStateRegistry 49 | .consumeRestoredStateForKey(key = key) 50 | ?.getSerializableContainer(key = KEY_STATE) 51 | ?.takeUnless { discardSavedState }, 52 | ) 53 | 54 | savedStateRegistry.registerSavedStateProvider(key = key) { 55 | Bundle().apply { 56 | if (isSavingAllowed()) { 57 | putSerializableContainer(key = KEY_STATE, value = dispatcher.save()) 58 | } 59 | } 60 | } 61 | 62 | return dispatcher 63 | } 64 | 65 | /** 66 | * Creates a new instance of [StateKeeper] and attaches it to the AndroidX [SavedStateRegistry]. 67 | * 68 | * @param discardSavedState a flag indicating whether any previously saved state should be discarded or not, 69 | * default value is `false`. 70 | * @param isSavingAllowed called before saving the state. 71 | * When `true` then the state will be saved, otherwise it won't. Default value is `true`. 72 | */ 73 | fun SavedStateRegistryOwner.stateKeeper( 74 | discardSavedState: Boolean = false, 75 | isSavingAllowed: () -> Boolean = { true }, 76 | ): StateKeeper = 77 | stateKeeper( 78 | key = KEY_STATE, 79 | discardSavedState = discardSavedState, 80 | isSavingAllowed = isSavingAllowed, 81 | ) 82 | 83 | /** 84 | * Creates a new instance of [StateKeeper] and attaches it to the AndroidX [SavedStateRegistry]. 85 | * 86 | * @param key a key to access this [SavedStateRegistry], to be used by the returned [StateKeeper]. 87 | * @param discardSavedState a flag indicating whether any previously saved state should be discarded or not, 88 | * default value is `false`. 89 | * @param isSavingAllowed called before saving the state. 90 | * When `true` then the state will be saved, otherwise it won't. Default value is `true`. 91 | */ 92 | fun SavedStateRegistryOwner.stateKeeper( 93 | key: String, 94 | discardSavedState: Boolean = false, 95 | isSavingAllowed: () -> Boolean = { true }, 96 | ): StateKeeper = 97 | StateKeeper( 98 | savedStateRegistry = savedStateRegistry, 99 | key = key, 100 | discardSavedState = discardSavedState, 101 | isSavingAllowed = isSavingAllowed 102 | ) 103 | -------------------------------------------------------------------------------- /lifecycle/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/LifecycleRegistryTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | @Suppress("TestFunctionName") 7 | class LifecycleRegistryTest { 8 | 9 | private val registry = LifecycleRegistryImpl(initialState = Lifecycle.State.INITIALIZED) 10 | 11 | @Test 12 | fun WHEN_called_THEN_calls_subscribers_in_correct_order() { 13 | val events = ArrayList() 14 | 15 | fun callbacks(name: String): Lifecycle.Callbacks = 16 | object : Lifecycle.Callbacks { 17 | override fun onCreate() { 18 | events += "onCreate $name" 19 | } 20 | 21 | override fun onStart() { 22 | events += "onStart $name" 23 | } 24 | 25 | override fun onResume() { 26 | events += "onResume $name" 27 | } 28 | 29 | override fun onPause() { 30 | events += "onPause $name" 31 | } 32 | 33 | override fun onStop() { 34 | events += "onStop $name" 35 | } 36 | 37 | override fun onDestroy() { 38 | events += "onDestroy $name" 39 | } 40 | } 41 | 42 | registry.subscribe(callbacks(name = "1")) 43 | registry.subscribe(callbacks(name = "2")) 44 | 45 | registry.onCreate() 46 | registry.onStart() 47 | registry.onResume() 48 | registry.onPause() 49 | registry.onStop() 50 | registry.onDestroy() 51 | 52 | assertEquals( 53 | listOf( 54 | "onCreate 1", 55 | "onCreate 2", 56 | "onStart 1", 57 | "onStart 2", 58 | "onResume 1", 59 | "onResume 2", 60 | "onPause 2", 61 | "onPause 1", 62 | "onStop 2", 63 | "onStop 1", 64 | "onDestroy 2", 65 | "onDestroy 1" 66 | ), 67 | events 68 | ) 69 | } 70 | 71 | @Test 72 | fun WHEN_unsubscribed_and_called_THEN_callbacks_not_called() { 73 | val events = ArrayList() 74 | 75 | val callbacks = 76 | object : Lifecycle.Callbacks { 77 | override fun onCreate() { 78 | events += "onCreate" 79 | } 80 | } 81 | 82 | registry.subscribe(callbacks) 83 | registry.unsubscribe(callbacks) 84 | registry.onCreate() 85 | 86 | assertEquals(emptyList(), events) 87 | } 88 | 89 | @Test 90 | fun WHEN_unsubscribed_from_callback_and_called_THEN_callbacks_not_called() { 91 | val events = ArrayList() 92 | 93 | val callbacks = 94 | object : Lifecycle.Callbacks { 95 | override fun onCreate() { 96 | registry.unsubscribe(this) 97 | } 98 | 99 | override fun onStart() { 100 | events += "onStart" 101 | } 102 | } 103 | 104 | registry.subscribe(callbacks) 105 | registry.onCreate() 106 | registry.onStart() 107 | 108 | assertEquals(emptyList(), events) 109 | } 110 | 111 | @Test 112 | fun WHEN_created_with_initial_state_THEN_state_returns_that_state() { 113 | val registry = LifecycleRegistryImpl(initialState = Lifecycle.State.RESUMED) 114 | 115 | assertEquals(Lifecycle.State.RESUMED, registry.state) 116 | } 117 | 118 | @Test 119 | fun GIVEN_created_with_initial_state_WHEN_subscribed_THEN_callbacks_called() { 120 | val registry = LifecycleRegistryImpl(initialState = Lifecycle.State.RESUMED) 121 | val events = ArrayList() 122 | 123 | val callbacks = 124 | object : Lifecycle.Callbacks { 125 | override fun onCreate() { 126 | events += "onCreate" 127 | } 128 | 129 | override fun onStart() { 130 | events += "onStart" 131 | } 132 | 133 | override fun onResume() { 134 | events += "onResume" 135 | } 136 | } 137 | 138 | registry.subscribe(callbacks) 139 | 140 | assertEquals(listOf("onCreate", "onStart", "onResume"), events) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/DefaultStateKeeperDispatcherTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertFalse 7 | import kotlin.test.assertNull 8 | import kotlin.test.assertTrue 9 | 10 | @Suppress("TestFunctionName") 11 | class DefaultStateKeeperDispatcherTest { 12 | 13 | @Test 14 | fun WHEN_save_recreate_consume_THEN_data_restored() { 15 | val dispatcher1 = DefaultStateKeeperDispatcher(savedState = null) 16 | 17 | val data1 = Data() 18 | val data2 = Data() 19 | 20 | dispatcher1.register(key = "key1", strategy = Data.serializer()) { data1 } 21 | dispatcher1.register(key = "key2", strategy = Data.serializer()) { data2 } 22 | dispatcher1.register(key = "key3", strategy = Data.serializer()) { null } 23 | 24 | val savedState = dispatcher1.save().serializeAndDeserialize() 25 | 26 | val dispatcher2 = DefaultStateKeeperDispatcher(savedState = savedState) 27 | 28 | val restoredData1 = dispatcher2.consume(key = "key1", strategy = Data.serializer()) 29 | val restoredData2 = dispatcher2.consume(key = "key2", strategy = Data.serializer()) 30 | val restoredData3 = dispatcher2.consume(key = "key3", strategy = Data.serializer()) 31 | 32 | assertEquals(data1, restoredData1) 33 | assertEquals(data2, restoredData2) 34 | assertNull(restoredData3) 35 | } 36 | 37 | @Test 38 | fun WHEN_save_recreate_twice_consume_THEN_data_restored() { 39 | val dispatcher1 = DefaultStateKeeperDispatcher(savedState = null) 40 | 41 | val data1 = Data(value = "value1") 42 | val data2 = Data(value = "value2") 43 | val data3 = Data(value = "value3") 44 | 45 | dispatcher1.register(key = "key1", strategy = Data.serializer()) { data1 } 46 | dispatcher1.register(key = "key2", strategy = Data.serializer()) { data2 } 47 | 48 | val savedState1 = dispatcher1.save().serializeAndDeserialize() 49 | val dispatcher2 = DefaultStateKeeperDispatcher(savedState = savedState1) 50 | dispatcher2.register(key = "key1", strategy = Data.serializer()) { data3 } 51 | val savedState2 = dispatcher2.save().serializeAndDeserialize() 52 | val dispatcher3 = DefaultStateKeeperDispatcher(savedState = savedState2) 53 | 54 | val restoredData1 = dispatcher3.consume(key = "key1", strategy = Data.serializer()) 55 | val restoredData2 = dispatcher3.consume(key = "key2", strategy = Data.serializer()) 56 | 57 | assertEquals(data3, restoredData1) 58 | assertEquals(data2, restoredData2) 59 | } 60 | 61 | @Test 62 | fun WHEN_consume_second_time_THEN_returns_null() { 63 | val dispatcher1 = DefaultStateKeeperDispatcher(savedState = null) 64 | 65 | dispatcher1.register(key = "key", strategy = Data.serializer()) { Data() } 66 | 67 | val savedState = dispatcher1.save().serializeAndDeserialize() 68 | 69 | val dispatcher2 = DefaultStateKeeperDispatcher(savedState = savedState) 70 | 71 | dispatcher2.consume(key = "key", strategy = Data.serializer()) 72 | 73 | val restoredSerializable = dispatcher2.consume(key = "key", strategy = Data.serializer()) 74 | 75 | assertNull(restoredSerializable) 76 | } 77 | 78 | @Test 79 | fun GIVEN_not_registered_WHEN_isRegistered_THEN_returns_false() { 80 | val dispatcher = DefaultStateKeeperDispatcher(savedState = null) 81 | 82 | val result = dispatcher.isRegistered(key = "key") 83 | 84 | assertFalse(result) 85 | } 86 | 87 | @Test 88 | fun GIVEN_registered_with_one_key_WHEN_isRegistered_with_another_key_THEN_returns_false() { 89 | val dispatcher = DefaultStateKeeperDispatcher(savedState = null) 90 | dispatcher.register(key = "key1", strategy = Data.serializer()) { Data() } 91 | 92 | val result = dispatcher.isRegistered(key = "key2") 93 | 94 | assertFalse(result) 95 | } 96 | 97 | @Test 98 | fun GIVEN_registered_WHEN_isRegistered_with_same_key_THEN_returns_true() { 99 | val dispatcher = DefaultStateKeeperDispatcher(savedState = null) 100 | dispatcher.register(key = "key", strategy = Data.serializer()) { Data() } 101 | 102 | val result = dispatcher.isRegistered(key = "key") 103 | 104 | assertTrue(result) 105 | } 106 | 107 | @Serializable 108 | private data class Data( 109 | val value: String, 110 | ) { 111 | constructor() : this(value = "value") // To avoid default values in the primary constructor 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /back-handler/api/jvm/back-handler.api: -------------------------------------------------------------------------------- 1 | public abstract class com/arkivanov/essenty/backhandler/BackCallback { 2 | public static final field Companion Lcom/arkivanov/essenty/backhandler/BackCallback$Companion; 3 | public static final field PRIORITY_DEFAULT I 4 | public static final field PRIORITY_MAX I 5 | public static final field PRIORITY_MIN I 6 | public fun ()V 7 | public fun (ZI)V 8 | public synthetic fun (ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V 9 | public final fun addEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V 10 | public final fun getPriority ()I 11 | public final fun isEnabled ()Z 12 | public abstract fun onBack ()V 13 | public fun onBackCancelled ()V 14 | public fun onBackProgressed (Lcom/arkivanov/essenty/backhandler/BackEvent;)V 15 | public fun onBackStarted (Lcom/arkivanov/essenty/backhandler/BackEvent;)V 16 | public final fun removeEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V 17 | public final fun setEnabled (Z)V 18 | public final fun setPriority (I)V 19 | } 20 | 21 | public final class com/arkivanov/essenty/backhandler/BackCallback$Companion { 22 | } 23 | 24 | public final class com/arkivanov/essenty/backhandler/BackCallbackKt { 25 | public static final fun BackCallback (ZILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/backhandler/BackCallback; 26 | public static synthetic fun BackCallback$default (ZILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/essenty/backhandler/BackCallback; 27 | } 28 | 29 | public abstract interface class com/arkivanov/essenty/backhandler/BackDispatcher : com/arkivanov/essenty/backhandler/BackHandler { 30 | public abstract fun addEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V 31 | public abstract fun back ()Z 32 | public abstract fun cancelPredictiveBack ()V 33 | public abstract fun isEnabled ()Z 34 | public abstract fun progressPredictiveBack (Lcom/arkivanov/essenty/backhandler/BackEvent;)V 35 | public abstract fun removeEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V 36 | public abstract fun startPredictiveBack (Lcom/arkivanov/essenty/backhandler/BackEvent;)Z 37 | } 38 | 39 | public final class com/arkivanov/essenty/backhandler/BackDispatcherKt { 40 | public static final fun BackDispatcher ()Lcom/arkivanov/essenty/backhandler/BackDispatcher; 41 | } 42 | 43 | public final class com/arkivanov/essenty/backhandler/BackEvent { 44 | public fun ()V 45 | public fun (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FF)V 46 | public synthetic fun (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FFILkotlin/jvm/internal/DefaultConstructorMarker;)V 47 | public final fun component1 ()F 48 | public final fun component2 ()Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 49 | public final fun component3 ()F 50 | public final fun component4 ()F 51 | public final fun copy (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FF)Lcom/arkivanov/essenty/backhandler/BackEvent; 52 | public static synthetic fun copy$default (Lcom/arkivanov/essenty/backhandler/BackEvent;FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FFILjava/lang/Object;)Lcom/arkivanov/essenty/backhandler/BackEvent; 53 | public fun equals (Ljava/lang/Object;)Z 54 | public final fun getProgress ()F 55 | public final fun getSwipeEdge ()Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 56 | public final fun getTouchX ()F 57 | public final fun getTouchY ()F 58 | public fun hashCode ()I 59 | public fun toString ()Ljava/lang/String; 60 | } 61 | 62 | public final class com/arkivanov/essenty/backhandler/BackEvent$SwipeEdge : java/lang/Enum { 63 | public static final field LEFT Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 64 | public static final field RIGHT Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 65 | public static final field UNKNOWN Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 66 | public static fun getEntries ()Lkotlin/enums/EnumEntries; 67 | public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 68 | public static fun values ()[Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 69 | } 70 | 71 | public abstract interface class com/arkivanov/essenty/backhandler/BackHandler { 72 | public abstract fun isRegistered (Lcom/arkivanov/essenty/backhandler/BackCallback;)Z 73 | public abstract fun register (Lcom/arkivanov/essenty/backhandler/BackCallback;)V 74 | public abstract fun unregister (Lcom/arkivanov/essenty/backhandler/BackCallback;)V 75 | } 76 | 77 | public abstract interface class com/arkivanov/essenty/backhandler/BackHandlerOwner { 78 | public abstract fun getBackHandler ()Lcom/arkivanov/essenty/backhandler/BackHandler; 79 | } 80 | 81 | -------------------------------------------------------------------------------- /instance-keeper/src/commonTest/kotlin/com/arkivanov/essenty/instancekeeper/InstanceKeeperExtTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.instancekeeper 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertNotSame 5 | import kotlin.test.assertSame 6 | 7 | @Suppress("TestFunctionName", "DEPRECATION") 8 | @OptIn(ExperimentalInstanceKeeperApi::class) 9 | class InstanceKeeperExtTest { 10 | 11 | private val dispatcher = InstanceKeeperDispatcher() 12 | 13 | @Test 14 | fun WHEN_getOrCreate_with_same_key_called_second_time_THEN_returns_same_instance() { 15 | val thing1 = dispatcher.getOrCreate(key = "key") { ThingInstance() } 16 | val thing2 = dispatcher.getOrCreate(key = "key") { ThingInstance() } 17 | 18 | assertSame(thing1, thing2) 19 | } 20 | 21 | @Test 22 | fun WHEN_getOrCreate_with_different_key_called_second_time_THEN_returns_new_instance() { 23 | val thing1 = dispatcher.getOrCreate(key = "key1") { ThingInstance() } 24 | val thing2 = dispatcher.getOrCreate(key = "key2") { ThingInstance() } 25 | 26 | assertNotSame(thing1, thing2) 27 | } 28 | 29 | @Test 30 | fun WHEN_getOrCreate_with_same_type_called_second_time_THEN_returns_same_instance() { 31 | val thing1 = dispatcher.getOrCreate { ThingInstance() } 32 | val thing2 = dispatcher.getOrCreate { ThingInstance() } 33 | 34 | assertSame(thing1, thing2) 35 | } 36 | 37 | @Test 38 | fun WHEN_getOrCreate_with_different_type_called_second_time_THEN_returns_new_instance() { 39 | val thing1 = dispatcher.getOrCreate { ThingInstance() } 40 | val thing2 = dispatcher.getOrCreate { ThingInstance() } 41 | 42 | assertNotSame>(thing1, thing2) 43 | } 44 | 45 | @Test 46 | fun WHEN_getOrCreateSimple_with_same_key_called_second_time_THEN_returns_same_instance() { 47 | val thing1 = dispatcher.getOrCreateSimple(key = "key") { Thing() } 48 | val thing2 = dispatcher.getOrCreateSimple(key = "key") { Thing() } 49 | 50 | assertSame(thing1, thing2) 51 | } 52 | 53 | @Test 54 | fun WHEN_getOrCreateSimple_with_different_key_called_second_time_THEN_returns_new_instance() { 55 | val thing1 = dispatcher.getOrCreateSimple(key = "key1") { Thing() } 56 | val thing2 = dispatcher.getOrCreateSimple(key = "key2") { Thing() } 57 | 58 | assertNotSame(thing1, thing2) 59 | } 60 | 61 | @Test 62 | fun WHEN_getOrCreateSimple_with_same_type_called_second_time_THEN_returns_same_instance() { 63 | val thing1 = dispatcher.getOrCreateSimple { Thing() } 64 | val thing2 = dispatcher.getOrCreateSimple { Thing() } 65 | 66 | assertSame(thing1, thing2) 67 | } 68 | 69 | @Test 70 | fun WHEN_getOrCreateSimple_with_different_type_called_second_time_THEN_returns_new_instance() { 71 | val thing1 = dispatcher.getOrCreateSimple { Thing() } 72 | val thing2 = dispatcher.getOrCreateSimple { Thing() } 73 | 74 | assertNotSame>(thing1, thing2) 75 | } 76 | 77 | @Test 78 | fun retainingInstance_retains_instance() { 79 | val instanceKeeper = InstanceKeeperDispatcher() 80 | val component1 = Component(instanceKeeper) 81 | 82 | val component2 = Component(instanceKeeper) 83 | 84 | assertSame(component1.instance, component2.instance) 85 | } 86 | 87 | @Test 88 | fun retainingSimpleInstance_retains_instance() { 89 | val instanceKeeper = InstanceKeeperDispatcher() 90 | val component1 = Component(instanceKeeper) 91 | 92 | val component2 = Component(instanceKeeper) 93 | 94 | assertSame(component1.simpleInstance, component2.simpleInstance) 95 | } 96 | 97 | @Test 98 | fun retainingClosable_retains_instance() { 99 | val instanceKeeper = InstanceKeeperDispatcher() 100 | val component1 = Component(instanceKeeper) 101 | 102 | val component2 = Component(instanceKeeper) 103 | 104 | assertSame(component1.closeable, component2.closeable) 105 | } 106 | 107 | @Suppress("unused") 108 | private class Thing 109 | 110 | @Suppress("unused") 111 | private class ThingInstance : InstanceKeeper.Instance 112 | 113 | @Suppress("unused") 114 | private class ThingCloseable : AutoCloseable { 115 | override fun close() { 116 | // no-op 117 | } 118 | } 119 | 120 | private class Component(override val instanceKeeper: InstanceKeeper) : InstanceKeeperOwner { 121 | val instance by retainingInstance { ThingInstance() } 122 | val simpleInstance by retainingSimpleInstance { Thing() } 123 | val closeable by retainingCloseable { ThingCloseable() } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /state-keeper/api/jvm/state-keeper.api: -------------------------------------------------------------------------------- 1 | public abstract interface annotation class com/arkivanov/essenty/statekeeper/ExperimentalStateKeeperApi : java/lang/annotation/Annotation { 2 | } 3 | 4 | public final class com/arkivanov/essenty/statekeeper/PolymorphicSerializerKt { 5 | public static final fun polymorphicSerializer (Lkotlin/reflect/KClass;Lkotlinx/serialization/modules/SerializersModule;)Lkotlinx/serialization/KSerializer; 6 | } 7 | 8 | public final class com/arkivanov/essenty/statekeeper/SerializableContainer { 9 | public static final field Companion Lcom/arkivanov/essenty/statekeeper/SerializableContainer$Companion; 10 | public fun ()V 11 | public synthetic fun ([BLkotlin/jvm/internal/DefaultConstructorMarker;)V 12 | public final fun clear ()V 13 | public final fun consume (Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; 14 | public final fun set (Ljava/lang/Object;Lkotlinx/serialization/SerializationStrategy;)V 15 | } 16 | 17 | public final class com/arkivanov/essenty/statekeeper/SerializableContainer$Companion { 18 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 19 | } 20 | 21 | public final class com/arkivanov/essenty/statekeeper/SerializableContainerKt { 22 | public static final fun SerializableContainer (Ljava/lang/Object;Lkotlinx/serialization/SerializationStrategy;)Lcom/arkivanov/essenty/statekeeper/SerializableContainer; 23 | public static final fun consumeRequired (Lcom/arkivanov/essenty/statekeeper/SerializableContainer;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; 24 | } 25 | 26 | public abstract interface class com/arkivanov/essenty/statekeeper/StateKeeper { 27 | public abstract fun consume (Ljava/lang/String;Lkotlinx/serialization/DeserializationStrategy;)Ljava/lang/Object; 28 | public abstract fun isRegistered (Ljava/lang/String;)Z 29 | public abstract fun register (Ljava/lang/String;Lkotlinx/serialization/SerializationStrategy;Lkotlin/jvm/functions/Function0;)V 30 | public abstract fun unregister (Ljava/lang/String;)V 31 | } 32 | 33 | public abstract interface class com/arkivanov/essenty/statekeeper/StateKeeperDispatcher : com/arkivanov/essenty/statekeeper/StateKeeper { 34 | public abstract fun save ()Lcom/arkivanov/essenty/statekeeper/SerializableContainer; 35 | } 36 | 37 | public final class com/arkivanov/essenty/statekeeper/StateKeeperDispatcherKt { 38 | public static final fun StateKeeperDispatcher (Lcom/arkivanov/essenty/statekeeper/SerializableContainer;)Lcom/arkivanov/essenty/statekeeper/StateKeeperDispatcher; 39 | public static synthetic fun StateKeeperDispatcher$default (Lcom/arkivanov/essenty/statekeeper/SerializableContainer;ILjava/lang/Object;)Lcom/arkivanov/essenty/statekeeper/StateKeeperDispatcher; 40 | } 41 | 42 | public final class com/arkivanov/essenty/statekeeper/StateKeeperExtKt { 43 | public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 44 | public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlin/properties/PropertyDelegateProvider; 45 | public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 46 | public static final fun saveable (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlin/properties/PropertyDelegateProvider; 47 | public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 48 | public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeper;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 49 | public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 50 | public static synthetic fun saveable$default (Lcom/arkivanov/essenty/statekeeper/StateKeeperOwner;Lkotlinx/serialization/KSerializer;Lkotlin/jvm/functions/Function1;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 51 | } 52 | 53 | public abstract interface class com/arkivanov/essenty/statekeeper/StateKeeperOwner { 54 | public abstract fun getStateKeeper ()Lcom/arkivanov/essenty/statekeeper/StateKeeper; 55 | } 56 | 57 | -------------------------------------------------------------------------------- /lifecycle/src/itvosMain/kotlin/com/arkivanov/essenty/lifecycle/ApplicationLifecycle.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle 2 | 3 | import platform.Foundation.NSNotification 4 | import platform.Foundation.NSNotificationCenter 5 | import platform.Foundation.NSNotificationName 6 | import platform.Foundation.NSOperationQueue 7 | import platform.UIKit.UIApplication 8 | import platform.UIKit.UIApplicationDidBecomeActiveNotification 9 | import platform.UIKit.UIApplicationDidEnterBackgroundNotification 10 | import platform.UIKit.UIApplicationState 11 | import platform.UIKit.UIApplicationWillEnterForegroundNotification 12 | import platform.UIKit.UIApplicationWillResignActiveNotification 13 | import platform.UIKit.UIApplicationWillTerminateNotification 14 | import platform.darwin.NSObjectProtocol 15 | 16 | /** 17 | * An implementation of [Lifecycle] that follows the [UIApplication] lifecycle notifications. 18 | * 19 | * Since this implementation subscribes to [UIApplication] global lifecycle events, 20 | * the instance and all its registered callbacks (and whatever they capture) will stay in 21 | * memory until the application is destroyed. It's ok to use it in a global scope like 22 | * `UIApplicationDelegate`, but it may cause memory leaks when used in a narrower scope like 23 | * `UIViewController` if it gets destroyed earlier. 24 | */ 25 | class ApplicationLifecycle internal constructor( 26 | private val platform: Platform, 27 | private val lifecycle: LifecycleRegistry = LifecycleRegistry(), 28 | ) : Lifecycle by lifecycle { 29 | 30 | constructor() : this(platform = DefaultPlatform) 31 | 32 | private val willEnterForegroundObserver = platform.addObserver(UIApplicationWillEnterForegroundNotification) { lifecycle.start() } 33 | private val didBecomeActiveObserver = platform.addObserver(UIApplicationDidBecomeActiveNotification) { lifecycle.resume() } 34 | private val willResignActiveObserver = platform.addObserver(UIApplicationWillResignActiveNotification) { lifecycle.pause() } 35 | private val didEnterBackgroundObserver = platform.addObserver(UIApplicationDidEnterBackgroundNotification) { lifecycle.stop() } 36 | private val willTerminateObserver = platform.addObserver(UIApplicationWillTerminateNotification) { lifecycle.destroy() } 37 | 38 | init { 39 | platform.addOperationOnMainQueue { 40 | if (lifecycle.state == Lifecycle.State.INITIALIZED) { 41 | when (platform.applicationState) { 42 | UIApplicationState.UIApplicationStateActive -> lifecycle.resume() 43 | UIApplicationState.UIApplicationStateInactive -> lifecycle.start() 44 | UIApplicationState.UIApplicationStateBackground -> lifecycle.create() 45 | else -> lifecycle.create() 46 | } 47 | } 48 | } 49 | 50 | doOnDestroy { 51 | platform.removeObserver(willEnterForegroundObserver) 52 | platform.removeObserver(didBecomeActiveObserver) 53 | platform.removeObserver(willResignActiveObserver) 54 | platform.removeObserver(didEnterBackgroundObserver) 55 | platform.removeObserver(willTerminateObserver) 56 | } 57 | } 58 | 59 | /** 60 | * Destroys this [ApplicationLifecycle] moving it to [Lifecycle.State.DESTROYED] state. 61 | * Also unsubscribes from all [UIApplication] lifecycle notifications. 62 | * 63 | * If the current state is [Lifecycle.State.INITIALIZED], then the lifecycle is first 64 | * moved to [Lifecycle.State.CREATED] state and then immediately to [Lifecycle.State.DESTROYED] state. 65 | */ 66 | fun destroy() { 67 | if (lifecycle.state == Lifecycle.State.INITIALIZED) { 68 | lifecycle.create() 69 | } 70 | 71 | lifecycle.destroy() 72 | } 73 | 74 | internal interface Platform { 75 | val applicationState: UIApplicationState 76 | 77 | fun addObserver(name: NSNotificationName, block: (NSNotification?) -> Unit): NSObjectProtocol 78 | fun removeObserver(observer: NSObjectProtocol) 79 | fun addOperationOnMainQueue(block: () -> Unit) 80 | } 81 | 82 | internal object DefaultPlatform : Platform { 83 | override val applicationState: UIApplicationState get() = UIApplication.sharedApplication.applicationState 84 | 85 | override fun addObserver(name: NSNotificationName, block: (NSNotification?) -> Unit): NSObjectProtocol = 86 | NSNotificationCenter.defaultCenter.addObserverForName( 87 | name = name, 88 | `object` = null, 89 | queue = NSOperationQueue.mainQueue, 90 | usingBlock = block, 91 | ) 92 | 93 | override fun removeObserver(observer: NSObjectProtocol) { 94 | NSNotificationCenter.defaultCenter.removeObserver(observer) 95 | } 96 | 97 | override fun addOperationOnMainQueue(block: () -> Unit) { 98 | NSOperationQueue.mainQueue.addOperationWithBlock(block) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /back-handler/api/android/back-handler.api: -------------------------------------------------------------------------------- 1 | public final class com/arkivanov/essenty/backhandler/AndroidBackHandlerKt { 2 | public static final fun BackHandler (Landroidx/activity/OnBackPressedDispatcher;)Lcom/arkivanov/essenty/backhandler/BackHandler; 3 | public static final fun BackHandler (Landroidx/activity/OnBackPressedDispatcher;Landroidx/lifecycle/LifecycleOwner;)Lcom/arkivanov/essenty/backhandler/BackHandler; 4 | public static final fun backHandler (Landroidx/activity/OnBackPressedDispatcherOwner;)Lcom/arkivanov/essenty/backhandler/BackHandler; 5 | public static final fun connectOnBackPressedCallback (Lcom/arkivanov/essenty/backhandler/BackDispatcher;)Landroidx/activity/OnBackPressedCallback; 6 | } 7 | 8 | public abstract class com/arkivanov/essenty/backhandler/BackCallback { 9 | public static final field Companion Lcom/arkivanov/essenty/backhandler/BackCallback$Companion; 10 | public static final field PRIORITY_DEFAULT I 11 | public static final field PRIORITY_MAX I 12 | public static final field PRIORITY_MIN I 13 | public fun ()V 14 | public fun (ZI)V 15 | public synthetic fun (ZIILkotlin/jvm/internal/DefaultConstructorMarker;)V 16 | public final fun addEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V 17 | public final fun getPriority ()I 18 | public final fun isEnabled ()Z 19 | public abstract fun onBack ()V 20 | public fun onBackCancelled ()V 21 | public fun onBackProgressed (Lcom/arkivanov/essenty/backhandler/BackEvent;)V 22 | public fun onBackStarted (Lcom/arkivanov/essenty/backhandler/BackEvent;)V 23 | public final fun removeEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V 24 | public final fun setEnabled (Z)V 25 | public final fun setPriority (I)V 26 | } 27 | 28 | public final class com/arkivanov/essenty/backhandler/BackCallback$Companion { 29 | } 30 | 31 | public final class com/arkivanov/essenty/backhandler/BackCallbackKt { 32 | public static final fun BackCallback (ZILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/backhandler/BackCallback; 33 | public static synthetic fun BackCallback$default (ZILkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lcom/arkivanov/essenty/backhandler/BackCallback; 34 | } 35 | 36 | public abstract interface class com/arkivanov/essenty/backhandler/BackDispatcher : com/arkivanov/essenty/backhandler/BackHandler { 37 | public abstract fun addEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V 38 | public abstract fun back ()Z 39 | public abstract fun cancelPredictiveBack ()V 40 | public abstract fun isEnabled ()Z 41 | public abstract fun progressPredictiveBack (Lcom/arkivanov/essenty/backhandler/BackEvent;)V 42 | public abstract fun removeEnabledChangedListener (Lkotlin/jvm/functions/Function1;)V 43 | public abstract fun startPredictiveBack (Lcom/arkivanov/essenty/backhandler/BackEvent;)Z 44 | } 45 | 46 | public final class com/arkivanov/essenty/backhandler/BackDispatcherKt { 47 | public static final fun BackDispatcher ()Lcom/arkivanov/essenty/backhandler/BackDispatcher; 48 | } 49 | 50 | public final class com/arkivanov/essenty/backhandler/BackEvent { 51 | public fun ()V 52 | public fun (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FF)V 53 | public synthetic fun (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FFILkotlin/jvm/internal/DefaultConstructorMarker;)V 54 | public final fun component1 ()F 55 | public final fun component2 ()Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 56 | public final fun component3 ()F 57 | public final fun component4 ()F 58 | public final fun copy (FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FF)Lcom/arkivanov/essenty/backhandler/BackEvent; 59 | public static synthetic fun copy$default (Lcom/arkivanov/essenty/backhandler/BackEvent;FLcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge;FFILjava/lang/Object;)Lcom/arkivanov/essenty/backhandler/BackEvent; 60 | public fun equals (Ljava/lang/Object;)Z 61 | public final fun getProgress ()F 62 | public final fun getSwipeEdge ()Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 63 | public final fun getTouchX ()F 64 | public final fun getTouchY ()F 65 | public fun hashCode ()I 66 | public fun toString ()Ljava/lang/String; 67 | } 68 | 69 | public final class com/arkivanov/essenty/backhandler/BackEvent$SwipeEdge : java/lang/Enum { 70 | public static final field LEFT Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 71 | public static final field RIGHT Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 72 | public static final field UNKNOWN Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 73 | public static fun getEntries ()Lkotlin/enums/EnumEntries; 74 | public static fun valueOf (Ljava/lang/String;)Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 75 | public static fun values ()[Lcom/arkivanov/essenty/backhandler/BackEvent$SwipeEdge; 76 | } 77 | 78 | public abstract interface class com/arkivanov/essenty/backhandler/BackHandler { 79 | public abstract fun isRegistered (Lcom/arkivanov/essenty/backhandler/BackCallback;)Z 80 | public abstract fun register (Lcom/arkivanov/essenty/backhandler/BackCallback;)V 81 | public abstract fun unregister (Lcom/arkivanov/essenty/backhandler/BackCallback;)V 82 | } 83 | 84 | public abstract interface class com/arkivanov/essenty/backhandler/BackHandlerOwner { 85 | public abstract fun getBackHandler ()Lcom/arkivanov/essenty/backhandler/BackHandler; 86 | } 87 | 88 | -------------------------------------------------------------------------------- /instance-keeper/api/jvm/instance-keeper.api: -------------------------------------------------------------------------------- 1 | public abstract interface annotation class com/arkivanov/essenty/instancekeeper/ExperimentalInstanceKeeperApi : java/lang/annotation/Annotation { 2 | } 3 | 4 | public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeper { 5 | public abstract fun get (Ljava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; 6 | public abstract fun put (Ljava/lang/Object;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance;)V 7 | public abstract fun remove (Ljava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; 8 | } 9 | 10 | public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { 11 | public abstract fun onDestroy ()V 12 | } 13 | 14 | public final class com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance$DefaultImpls { 15 | public static fun onDestroy (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance;)V 16 | } 17 | 18 | public final class com/arkivanov/essenty/instancekeeper/InstanceKeeper$SimpleInstance : com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { 19 | public fun (Ljava/lang/Object;)V 20 | public final fun getInstance ()Ljava/lang/Object; 21 | public fun onDestroy ()V 22 | } 23 | 24 | public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcher : com/arkivanov/essenty/instancekeeper/InstanceKeeper { 25 | public abstract fun destroy ()V 26 | } 27 | 28 | public final class com/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcherKt { 29 | public static final fun InstanceKeeperDispatcher ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcher; 30 | } 31 | 32 | public final class com/arkivanov/essenty/instancekeeper/InstanceKeeperExtKt { 33 | public static final fun getOrCreate (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; 34 | public static final fun getOrCreateCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/AutoCloseable; 35 | public static final fun getOrCreateSimple (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; 36 | public static final fun retainedCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/AutoCloseable; 37 | public static final fun retainedInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; 38 | public static final fun retainedSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; 39 | public static final fun retainingCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 40 | public static final fun retainingCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 41 | public static synthetic fun retainingCloseable$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 42 | public static synthetic fun retainingCloseable$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 43 | public static final fun retainingInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 44 | public static final fun retainingInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 45 | public static synthetic fun retainingInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 46 | public static synthetic fun retainingInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 47 | public static final fun retainingSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 48 | public static final fun retainingSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 49 | public static synthetic fun retainingSimpleInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 50 | public static synthetic fun retainingSimpleInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 51 | } 52 | 53 | public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeperOwner { 54 | public abstract fun getInstanceKeeper ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; 55 | } 56 | 57 | -------------------------------------------------------------------------------- /lifecycle-coroutines/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/coroutines/RepeatOnLifecycle.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle.coroutines 2 | 3 | import com.arkivanov.essenty.lifecycle.Lifecycle 4 | import com.arkivanov.essenty.lifecycle.LifecycleOwner 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Dispatchers 7 | import kotlinx.coroutines.Job 8 | import kotlinx.coroutines.coroutineScope 9 | import kotlinx.coroutines.launch 10 | import kotlinx.coroutines.suspendCancellableCoroutine 11 | import kotlinx.coroutines.sync.Mutex 12 | import kotlinx.coroutines.sync.withLock 13 | import kotlinx.coroutines.withContext 14 | import kotlin.coroutines.CoroutineContext 15 | import kotlin.coroutines.resume 16 | 17 | /** 18 | * Convenience method for [Lifecycle.repeatOnLifecycle]. 19 | */ 20 | suspend fun LifecycleOwner.repeatOnLifecycle( 21 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED, 22 | context: CoroutineContext = Dispatchers.Main.immediateOrFallback, 23 | block: suspend CoroutineScope.() -> Unit, 24 | ) { 25 | lifecycle.repeatOnLifecycle(minActiveState = minActiveState, context = context, block = block) 26 | } 27 | 28 | /** 29 | * Runs the given [block] in a new coroutine when this [Lifecycle] is at least at [minActiveState] and suspends 30 | * the execution until this [Lifecycle] is [Lifecycle.State.DESTROYED]. 31 | * 32 | * The [block] will cancel and re-launch as the [Lifecycle] moves in and out of the [minActiveState]. 33 | * 34 | * The [block] is called on the specified [context], which defaults to 35 | * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate] 36 | * if available on the current platform, or to [Dispatchers.Main] otherwise. 37 | * 38 | * See the [AndroidX documentation](https://developer.android.com/reference/kotlin/androidx/lifecycle/package-summary#(androidx.lifecycle.Lifecycle).repeatOnLifecycle(androidx.lifecycle.Lifecycle.State,kotlin.coroutines.SuspendFunction1)) 39 | * for more information. 40 | */ 41 | suspend fun Lifecycle.repeatOnLifecycle( 42 | minActiveState: Lifecycle.State = Lifecycle.State.STARTED, 43 | context: CoroutineContext = Dispatchers.Main.immediateOrFallback, 44 | block: suspend CoroutineScope.() -> Unit 45 | ) { 46 | require(minActiveState != Lifecycle.State.INITIALIZED) { 47 | "repeatOnEssentyLifecycle cannot start work with the INITIALIZED lifecycle state." 48 | } 49 | 50 | if (this.state == Lifecycle.State.DESTROYED) { 51 | return 52 | } 53 | 54 | coroutineScope { 55 | withContext(context) { 56 | if (this@repeatOnLifecycle.state == Lifecycle.State.DESTROYED) { 57 | return@withContext 58 | } 59 | 60 | var callback: Lifecycle.Callbacks? = null 61 | var job: Job? = null 62 | val mutex = Mutex() 63 | 64 | try { 65 | suspendCancellableCoroutine { cont -> 66 | callback = createLifecycleAwareCallback( 67 | startState = minActiveState, 68 | onStateAppear = { 69 | job = launch { 70 | mutex.withLock { 71 | block() 72 | } 73 | } 74 | }, 75 | onStateDisappear = { 76 | job?.cancel() 77 | job = null 78 | }, 79 | onDestroy = { 80 | cont.resume(Unit) 81 | }, 82 | ) 83 | 84 | this@repeatOnLifecycle.subscribe(requireNotNull(callback)) 85 | } 86 | } finally { 87 | job?.cancel() 88 | job = null 89 | callback?.let { 90 | this@repeatOnLifecycle.unsubscribe(it) 91 | } 92 | callback = null 93 | } 94 | } 95 | } 96 | } 97 | 98 | /** 99 | * Creates lifecycle aware [Lifecycle.Callbacks] interface instance. 100 | * 101 | * @param startState [Lifecycle.State] that [onStateAppear] block must be called from 102 | * @param onStateAppear block of code that will be executed when the [Lifecycle.State] was equal [startState] 103 | * @param onStateDisappear block of code that will be executed when the [Lifecycle.State] was equal to opposite [startState] 104 | * @param onDestroy block of code that will be executed when the [Lifecycle.State] was equal [Lifecycle.State.DESTROYED] 105 | * 106 | * @return [Lifecycle.Callbacks] 107 | */ 108 | private fun createLifecycleAwareCallback( 109 | startState: Lifecycle.State, 110 | onStateAppear: () -> Unit, 111 | onStateDisappear: () -> Unit, 112 | onDestroy: () -> Unit, 113 | ): Lifecycle.Callbacks = object : Lifecycle.Callbacks { 114 | 115 | override fun onCreate() { 116 | launchIfState(Lifecycle.State.CREATED) 117 | } 118 | 119 | override fun onStart() { 120 | launchIfState(Lifecycle.State.STARTED) 121 | } 122 | 123 | override fun onResume() { 124 | launchIfState(Lifecycle.State.RESUMED) 125 | } 126 | 127 | override fun onPause() { 128 | closeIfState(Lifecycle.State.RESUMED) 129 | } 130 | 131 | override fun onStop() { 132 | closeIfState(Lifecycle.State.STARTED) 133 | } 134 | 135 | override fun onDestroy() { 136 | closeIfState(Lifecycle.State.CREATED) 137 | onDestroy() 138 | } 139 | 140 | private fun launchIfState(state: Lifecycle.State) { 141 | if (startState == state) { 142 | onStateAppear() 143 | } 144 | } 145 | 146 | private fun closeIfState(state: Lifecycle.State) { 147 | if (startState == state) { 148 | onStateDisappear() 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lifecycle/src/commonMain/kotlin/com/arkivanov/essenty/lifecycle/LifecycleExt.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle 2 | 3 | /** 4 | * A convenience method for [Lifecycle.subscribe]. 5 | */ 6 | fun Lifecycle.subscribe( 7 | onCreate: (() -> Unit)? = null, 8 | onStart: (() -> Unit)? = null, 9 | onResume: (() -> Unit)? = null, 10 | onPause: (() -> Unit)? = null, 11 | onStop: (() -> Unit)? = null, 12 | onDestroy: (() -> Unit)? = null 13 | ): Lifecycle.Callbacks = 14 | object : Lifecycle.Callbacks { 15 | override fun onCreate() { 16 | onCreate?.invoke() 17 | } 18 | 19 | override fun onStart() { 20 | onStart?.invoke() 21 | } 22 | 23 | override fun onResume() { 24 | onResume?.invoke() 25 | } 26 | 27 | override fun onPause() { 28 | onPause?.invoke() 29 | } 30 | 31 | override fun onStop() { 32 | onStop?.invoke() 33 | } 34 | 35 | override fun onDestroy() { 36 | onDestroy?.invoke() 37 | } 38 | }.also(::subscribe) 39 | 40 | /** 41 | * Registers the callback [block] to be called when this [Lifecycle] is created. 42 | */ 43 | inline fun Lifecycle.doOnCreate(crossinline block: () -> Unit) { 44 | subscribe( 45 | object : Lifecycle.Callbacks { 46 | override fun onCreate() { 47 | unsubscribe(this) 48 | block() 49 | } 50 | } 51 | ) 52 | } 53 | 54 | /** 55 | * Registers the callback [block] to be called when this [Lifecycle] is started. 56 | * 57 | * @param isOneTime if `true` then the callback is automatically unregistered right before 58 | * the first call, default value is `false`. 59 | */ 60 | inline fun Lifecycle.doOnStart(isOneTime: Boolean = false, crossinline block: () -> Unit) { 61 | subscribe( 62 | object : Lifecycle.Callbacks { 63 | override fun onStart() { 64 | if (isOneTime) { 65 | unsubscribe(this) 66 | } 67 | 68 | block() 69 | } 70 | } 71 | ) 72 | } 73 | 74 | /** 75 | * Registers the callback [block] to be called when this [Lifecycle] is resumed. 76 | * 77 | * @param isOneTime if `true` then the callback is automatically unregistered right before 78 | * the first call, default value is `false`. 79 | */ 80 | inline fun Lifecycle.doOnResume(isOneTime: Boolean = false, crossinline block: () -> Unit) { 81 | subscribe( 82 | object : Lifecycle.Callbacks { 83 | override fun onResume() { 84 | if (isOneTime) { 85 | unsubscribe(this) 86 | } 87 | 88 | block() 89 | } 90 | } 91 | ) 92 | } 93 | 94 | /** 95 | * Registers the callback [block] to be called when this [Lifecycle] is paused. 96 | * 97 | * @param isOneTime if `true` then the callback is automatically unregistered right before 98 | * the first call, default value is `false`. 99 | */ 100 | inline fun Lifecycle.doOnPause(isOneTime: Boolean = false, crossinline block: () -> Unit) { 101 | subscribe( 102 | object : Lifecycle.Callbacks { 103 | override fun onPause() { 104 | if (isOneTime) { 105 | unsubscribe(this) 106 | } 107 | 108 | block() 109 | } 110 | } 111 | ) 112 | } 113 | 114 | /** 115 | * Registers the callback [block] to be called when this [Lifecycle] is stopped. 116 | * 117 | * @param isOneTime if `true` then the callback is automatically unregistered right before 118 | * the first call, default value is `false`. 119 | */ 120 | inline fun Lifecycle.doOnStop(isOneTime: Boolean = false, crossinline block: () -> Unit) { 121 | subscribe( 122 | object : Lifecycle.Callbacks { 123 | override fun onStop() { 124 | if (isOneTime) { 125 | unsubscribe(this) 126 | } 127 | 128 | block() 129 | } 130 | } 131 | ) 132 | } 133 | 134 | /** 135 | * Registers the callback [block] to be called when this [Lifecycle] is destroyed. 136 | * Calls the [block] immediately if the [Lifecycle] is already destroyed. 137 | */ 138 | inline fun Lifecycle.doOnDestroy(crossinline block: () -> Unit) { 139 | if (state == Lifecycle.State.DESTROYED) { 140 | block() 141 | } else { 142 | subscribe( 143 | object : Lifecycle.Callbacks { 144 | override fun onDestroy() { 145 | block() 146 | } 147 | } 148 | ) 149 | } 150 | } 151 | 152 | /** 153 | * Convenience method for [Lifecycle.doOnCreate]. 154 | */ 155 | inline fun LifecycleOwner.doOnCreate(crossinline block: () -> Unit) { 156 | lifecycle.doOnCreate(block) 157 | } 158 | 159 | /** 160 | * Convenience method for [Lifecycle.doOnStart]. 161 | */ 162 | inline fun LifecycleOwner.doOnStart(isOneTime: Boolean = false, crossinline block: () -> Unit) { 163 | lifecycle.doOnStart(isOneTime = isOneTime, block = block) 164 | } 165 | 166 | /** 167 | * Convenience method for [Lifecycle.doOnResume]. 168 | */ 169 | inline fun LifecycleOwner.doOnResume(isOneTime: Boolean = false, crossinline block: () -> Unit) { 170 | lifecycle.doOnResume(isOneTime = isOneTime, block = block) 171 | } 172 | 173 | /** 174 | * Convenience method for [Lifecycle.doOnPause]. 175 | */ 176 | inline fun LifecycleOwner.doOnPause(isOneTime: Boolean = false, crossinline block: () -> Unit) { 177 | lifecycle.doOnPause(isOneTime = isOneTime, block = block) 178 | } 179 | 180 | /** 181 | * Convenience method for [Lifecycle.doOnStop]. 182 | */ 183 | inline fun LifecycleOwner.doOnStop(isOneTime: Boolean = false, crossinline block: () -> Unit) { 184 | lifecycle.doOnStop(isOneTime = isOneTime, block = block) 185 | } 186 | 187 | /** 188 | * Convenience method for [Lifecycle.doOnDestroy]. 189 | */ 190 | inline fun LifecycleOwner.doOnDestroy(crossinline block: () -> Unit) { 191 | lifecycle.doOnDestroy(block) 192 | } 193 | -------------------------------------------------------------------------------- /instance-keeper/api/android/instance-keeper.api: -------------------------------------------------------------------------------- 1 | public final class com/arkivanov/essenty/instancekeeper/AndroidExtKt { 2 | public static final fun InstanceKeeper (Landroidx/lifecycle/ViewModelStore;Z)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; 3 | public static synthetic fun InstanceKeeper$default (Landroidx/lifecycle/ViewModelStore;ZILjava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; 4 | public static final fun instanceKeeper (Landroidx/lifecycle/ViewModelStoreOwner;Z)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; 5 | public static synthetic fun instanceKeeper$default (Landroidx/lifecycle/ViewModelStoreOwner;ZILjava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; 6 | } 7 | 8 | public abstract interface annotation class com/arkivanov/essenty/instancekeeper/ExperimentalInstanceKeeperApi : java/lang/annotation/Annotation { 9 | } 10 | 11 | public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeper { 12 | public abstract fun get (Ljava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; 13 | public abstract fun put (Ljava/lang/Object;Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance;)V 14 | public abstract fun remove (Ljava/lang/Object;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; 15 | } 16 | 17 | public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { 18 | public abstract fun onDestroy ()V 19 | } 20 | 21 | public final class com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance$DefaultImpls { 22 | public static fun onDestroy (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance;)V 23 | } 24 | 25 | public final class com/arkivanov/essenty/instancekeeper/InstanceKeeper$SimpleInstance : com/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance { 26 | public fun (Ljava/lang/Object;)V 27 | public final fun getInstance ()Ljava/lang/Object; 28 | public fun onDestroy ()V 29 | } 30 | 31 | public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcher : com/arkivanov/essenty/instancekeeper/InstanceKeeper { 32 | public abstract fun destroy ()V 33 | } 34 | 35 | public final class com/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcherKt { 36 | public static final fun InstanceKeeperDispatcher ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperDispatcher; 37 | } 38 | 39 | public final class com/arkivanov/essenty/instancekeeper/InstanceKeeperExtKt { 40 | public static final fun getOrCreate (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; 41 | public static final fun getOrCreateCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/AutoCloseable; 42 | public static final fun getOrCreateSimple (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; 43 | public static final fun retainedCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/AutoCloseable; 44 | public static final fun retainedInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper$Instance; 45 | public static final fun retainedSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object; 46 | public static final fun retainingCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 47 | public static final fun retainingCloseable (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 48 | public static synthetic fun retainingCloseable$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 49 | public static synthetic fun retainingCloseable$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 50 | public static final fun retainingInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 51 | public static final fun retainingInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 52 | public static synthetic fun retainingInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 53 | public static synthetic fun retainingInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 54 | public static final fun retainingSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 55 | public static final fun retainingSimpleInstance (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Lkotlin/properties/PropertyDelegateProvider; 56 | public static synthetic fun retainingSimpleInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 57 | public static synthetic fun retainingSimpleInstance$default (Lcom/arkivanov/essenty/instancekeeper/InstanceKeeperOwner;Ljava/lang/Object;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lkotlin/properties/PropertyDelegateProvider; 58 | } 59 | 60 | public abstract interface class com/arkivanov/essenty/instancekeeper/InstanceKeeperOwner { 61 | public abstract fun getInstanceKeeper ()Lcom/arkivanov/essenty/instancekeeper/InstanceKeeper; 62 | } 63 | 64 | -------------------------------------------------------------------------------- /lifecycle-coroutines/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/coroutines/LifecycleCoroutinesExtTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle.coroutines 2 | 3 | import com.arkivanov.essenty.lifecycle.Lifecycle 4 | import com.arkivanov.essenty.lifecycle.LifecycleRegistry 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.ExperimentalCoroutinesApi 7 | import kotlinx.coroutines.coroutineScope 8 | import kotlinx.coroutines.flow.flow 9 | import kotlinx.coroutines.launch 10 | import kotlinx.coroutines.test.StandardTestDispatcher 11 | import kotlinx.coroutines.test.advanceUntilIdle 12 | import kotlinx.coroutines.test.resetMain 13 | import kotlinx.coroutines.test.runTest 14 | import kotlinx.coroutines.test.setMain 15 | import kotlinx.coroutines.yield 16 | import kotlin.test.AfterTest 17 | import kotlin.test.BeforeTest 18 | import kotlin.test.Test 19 | import kotlin.test.assertEquals 20 | 21 | @OptIn(ExperimentalCoroutinesApi::class) 22 | class LifecycleCoroutinesExtTest { 23 | 24 | private val testDispatcher = StandardTestDispatcher() 25 | private val registry = LifecycleRegistry(initialState = Lifecycle.State.INITIALIZED) 26 | 27 | @BeforeTest 28 | fun beforeTesting() { 29 | Dispatchers.setMain(testDispatcher) 30 | } 31 | 32 | @AfterTest 33 | fun afterTesting() { 34 | Dispatchers.resetMain() 35 | } 36 | 37 | @Test 38 | fun test_passed_state_CREATED_must_be_trigger_block_once() = runTest { 39 | val state = Lifecycle.State.CREATED 40 | val expected = listOf(state) 41 | 42 | val actual = executeRepeatOnEssentyLifecycleTest(state) 43 | advanceUntilIdle() 44 | 45 | assertEquals(expected, actual) 46 | } 47 | 48 | @Test 49 | fun test_passed_state_STARTED_must_be_trigger_block_twice() = runTest { 50 | val state = Lifecycle.State.STARTED 51 | val expected = listOf(state, state) 52 | 53 | val actual = executeRepeatOnEssentyLifecycleTest(state) 54 | advanceUntilIdle() 55 | 56 | assertEquals(expected, actual) 57 | } 58 | 59 | @Test 60 | fun test_passed_state_RESUMED_must_be_trigger_block_twice() = runTest { 61 | val state = Lifecycle.State.RESUMED 62 | val expected = listOf(state, state) 63 | 64 | val actual = executeRepeatOnEssentyLifecycleTest(state) 65 | advanceUntilIdle() 66 | 67 | assertEquals(expected, actual) 68 | } 69 | 70 | @Test 71 | fun test_passed_state_DESTROYED_must_not_be_trigger_block() = runTest { 72 | val state = Lifecycle.State.DESTROYED 73 | val expected = emptyList() 74 | 75 | val actual = executeRepeatOnEssentyLifecycleTest(state) 76 | advanceUntilIdle() 77 | 78 | assertEquals(expected, actual) 79 | } 80 | 81 | @Test 82 | fun test_flow_passed_state_CREATED_must_be_trigger_block_once() = runTest { 83 | val state = Lifecycle.State.CREATED 84 | val expect = listOf(state, state) 85 | 86 | val actual = executeFlowWithEssentyLifecycleTest(state) 87 | advanceUntilIdle() 88 | 89 | assertEquals(expect, actual) 90 | } 91 | 92 | @Test 93 | fun test_flow_passed_state_STARTED_must_be_trigger_block_twice() = runTest { 94 | val state = Lifecycle.State.STARTED 95 | val expect = listOf(state, state, state, state) 96 | 97 | val actual = executeFlowWithEssentyLifecycleTest(state) 98 | advanceUntilIdle() 99 | 100 | assertEquals(expect, actual) 101 | } 102 | 103 | @Test 104 | fun test_flow_passed_state_RESUMED_must_be_trigger_block_twice() = runTest { 105 | val state = Lifecycle.State.RESUMED 106 | val expect = listOf(state, state, state, state) 107 | 108 | val actual = executeFlowWithEssentyLifecycleTest(state) 109 | advanceUntilIdle() 110 | 111 | assertEquals(expect, actual) 112 | } 113 | 114 | @Test 115 | fun test_flow_passed_state_DESTROYED_must_not_be_trigger_block() = runTest { 116 | val state = Lifecycle.State.DESTROYED 117 | val expect = emptyList() 118 | 119 | val actual = executeFlowWithEssentyLifecycleTest(state) 120 | advanceUntilIdle() 121 | 122 | assertEquals(expect, actual) 123 | } 124 | 125 | private suspend fun executeRepeatOnEssentyLifecycleTest( 126 | lifecycleState: Lifecycle.State 127 | ): List = coroutineScope { 128 | val events = ArrayList() 129 | 130 | launch { 131 | registry.repeatOnLifecycle( 132 | minActiveState = lifecycleState, 133 | context = testDispatcher 134 | ) { 135 | events.add(lifecycleState) 136 | } 137 | } 138 | 139 | registry.onCreate() 140 | yield() 141 | registry.onStart() 142 | yield() 143 | registry.onResume() 144 | yield() 145 | registry.onPause() 146 | yield() 147 | registry.onStop() 148 | yield() 149 | registry.onStart() 150 | yield() 151 | registry.onResume() 152 | yield() 153 | registry.onPause() 154 | yield() 155 | registry.onStop() 156 | yield() 157 | registry.onDestroy() 158 | 159 | return@coroutineScope events 160 | } 161 | 162 | private suspend fun executeFlowWithEssentyLifecycleTest( 163 | lifecycleState: Lifecycle.State 164 | ): List = coroutineScope { 165 | val actual = ArrayList() 166 | 167 | launch { 168 | flow { 169 | repeat(2) { emit(lifecycleState) } 170 | } 171 | .withLifecycle(registry, lifecycleState, testDispatcher) 172 | .collect { actual.add(it) } 173 | } 174 | 175 | 176 | registry.onCreate() 177 | yield() 178 | registry.onStart() 179 | yield() 180 | registry.onResume() 181 | yield() 182 | yield() 183 | registry.onPause() 184 | yield() 185 | registry.onStop() 186 | yield() 187 | registry.onStart() 188 | yield() 189 | registry.onResume() 190 | yield() 191 | registry.onPause() 192 | yield() 193 | registry.onStop() 194 | yield() 195 | registry.onDestroy() 196 | 197 | return@coroutineScope actual 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /lifecycle/src/commonTest/kotlin/com/arkivanov/essenty/lifecycle/LifecycleExtTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.lifecycle 2 | 3 | import com.arkivanov.essenty.lifecycle.Lifecycle.Callbacks 4 | import com.arkivanov.essenty.lifecycle.Lifecycle.State 5 | import kotlin.test.Test 6 | import kotlin.test.assertContentEquals 7 | import kotlin.test.assertEquals 8 | 9 | @Suppress("TestFunctionName") 10 | class LifecycleExtTest { 11 | private val owner = TestLifecycleOwner() 12 | private val events = ArrayList() 13 | 14 | @Test 15 | fun WHEN_doOnCreate_THEN_not_called() { 16 | State.entries.forEach { state -> 17 | owner.state = state 18 | owner.doOnCreate(callback()) 19 | } 20 | 21 | assertNoEvents() 22 | } 23 | 24 | @Test 25 | fun WHEN_doOnCreate_and_onCreate_called_THEN_called() { 26 | owner.doOnCreate(callback()) 27 | owner.call(Callbacks::onCreate) 28 | 29 | assertOneEvent() 30 | } 31 | 32 | @Test 33 | fun WHEN_doOnStart_THEN_not_called() { 34 | State.entries.forEach { state -> 35 | owner.state = state 36 | owner.doOnStart(block = callback()) 37 | } 38 | 39 | assertNoEvents() 40 | } 41 | 42 | @Test 43 | fun WHEN_doOnStart_and_onStart_called_multiple_times_THEN_called_multiple_times() { 44 | owner.doOnStart(block = callback()) 45 | owner.call(Callbacks::onStart) 46 | owner.call(Callbacks::onStart) 47 | 48 | assertEquals(2, events.size) 49 | } 50 | 51 | @Test 52 | fun WHEN_doOnStart_isOneTime_true_and_onStart_called_multiple_times_THEN_called_once() { 53 | owner.doOnStart(isOneTime = true, block = callback()) 54 | owner.call(Callbacks::onStart) 55 | owner.call(Callbacks::onStart) 56 | 57 | assertOneEvent() 58 | } 59 | 60 | @Test 61 | fun WHEN_doOnResume_THEN_not_called() { 62 | State.entries.forEach { state -> 63 | owner.state = state 64 | owner.doOnResume(block = callback()) 65 | } 66 | 67 | assertNoEvents() 68 | } 69 | 70 | @Test 71 | fun WHEN_doOnResume_and_onResume_called_multiple_times_THEN_called_multiple_times() { 72 | owner.doOnResume(block = callback()) 73 | owner.call(Callbacks::onResume) 74 | owner.call(Callbacks::onResume) 75 | 76 | assertEquals(2, events.size) 77 | } 78 | 79 | @Test 80 | fun WHEN_doOnResume_isOneTime_true_and_onResume_called_multiple_times_THEN_called_once() { 81 | owner.doOnResume(isOneTime = true, block = callback()) 82 | owner.call(Callbacks::onResume) 83 | owner.call(Callbacks::onResume) 84 | 85 | assertOneEvent() 86 | } 87 | 88 | @Test 89 | fun WHEN_doOnPause_THEN_not_called() { 90 | State.entries.forEach { state -> 91 | owner.state = state 92 | owner.doOnPause(block = callback()) 93 | } 94 | 95 | assertNoEvents() 96 | } 97 | 98 | @Test 99 | fun WHEN_doOnPause_and_onPause_called_multiple_times_THEN_called_multiple_times() { 100 | owner.doOnPause(block = callback()) 101 | owner.call(Callbacks::onPause) 102 | owner.call(Callbacks::onPause) 103 | 104 | assertEquals(2, events.size) 105 | } 106 | 107 | @Test 108 | fun WHEN_doOnPause_isOneTime_true_and_onPause_called_multiple_times_THEN_called_once() { 109 | owner.doOnPause(isOneTime = true, block = callback()) 110 | owner.call(Callbacks::onPause) 111 | owner.call(Callbacks::onPause) 112 | 113 | assertOneEvent() 114 | } 115 | 116 | @Test 117 | fun WHEN_doOnStop_THEN_not_called() { 118 | State.entries.forEach { state -> 119 | owner.state = state 120 | owner.doOnStop(block = callback()) 121 | } 122 | 123 | assertNoEvents() 124 | } 125 | 126 | @Test 127 | fun WHEN_doOnStop_and_onStop_called_multiple_times_THEN_called_multiple_times() { 128 | owner.doOnStop(block = callback()) 129 | owner.call(Callbacks::onStop) 130 | owner.call(Callbacks::onStop) 131 | 132 | assertEquals(2, events.size) 133 | } 134 | 135 | @Test 136 | fun WHEN_doOnStop_isOneTime_true_and_onStop_called_multiple_times_THEN_called_once() { 137 | owner.doOnStop(isOneTime = true, block = callback()) 138 | owner.call(Callbacks::onStop) 139 | owner.call(Callbacks::onStop) 140 | 141 | assertOneEvent() 142 | } 143 | 144 | @Test 145 | fun GIVEN_state_INITIALIZED_WHEN_doOnDestroy_THEN_not_called() { 146 | owner.doOnDestroy(callback()) 147 | 148 | assertNoEvents() 149 | } 150 | 151 | @Test 152 | fun GIVEN_state_CREATED_WHEN_doOnDestroy_THEN_not_called() { 153 | owner.state = State.CREATED 154 | 155 | owner.doOnDestroy(callback()) 156 | 157 | assertNoEvents() 158 | } 159 | 160 | @Test 161 | fun GIVEN_state_CREATED_WHEN_doOnDestroy_and_onDestroy_called_THEN_called() { 162 | owner.state = State.CREATED 163 | 164 | owner.doOnDestroy(callback()) 165 | owner.call(Callbacks::onDestroy) 166 | 167 | assertOneEvent() 168 | } 169 | 170 | @Test 171 | fun GIVEN_state_DESTROYED_WHEN_doOnDestroy_THEN_called() { 172 | owner.state = State.DESTROYED 173 | 174 | owner.doOnDestroy(callback()) 175 | 176 | assertOneEvent() 177 | } 178 | 179 | private fun assertNoEvents() { 180 | assertContentEquals(emptyList(), events) 181 | } 182 | 183 | private fun assertOneEvent() { 184 | assertEquals(1, events.size) 185 | } 186 | 187 | private fun callback(name: String = "event"): () -> Unit = 188 | { events += name } 189 | 190 | private class TestLifecycleOwner : LifecycleOwner { 191 | override val lifecycle: TestLifecycle = TestLifecycle() 192 | 193 | var state: State by lifecycle::state 194 | 195 | fun call(call: (Callbacks) -> Unit) { 196 | lifecycle.callbacks.forEach(call) 197 | } 198 | } 199 | 200 | private class TestLifecycle : Lifecycle { 201 | override var state: State = State.INITIALIZED 202 | var callbacks: MutableSet = HashSet() 203 | 204 | override fun subscribe(callbacks: Callbacks) { 205 | this.callbacks += callbacks 206 | } 207 | 208 | override fun unsubscribe(callbacks: Callbacks) { 209 | this.callbacks -= callbacks 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /state-keeper/src/commonTest/kotlin/com/arkivanov/essenty/statekeeper/SerializableContainerTest.kt: -------------------------------------------------------------------------------- 1 | package com.arkivanov.essenty.statekeeper 2 | 3 | import kotlinx.serialization.Serializable 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertNotNull 7 | import kotlin.test.assertNull 8 | 9 | @Suppress("TestFunctionName") 10 | class SerializableContainerTest { 11 | 12 | @Test 13 | fun GIVEN_value_not_set_WHEN_consume_THEN_returns_null() { 14 | val container = SerializableContainer() 15 | 16 | val data = container.consume(SerializableData.serializer()) 17 | 18 | assertNull(data) 19 | } 20 | 21 | @Test 22 | fun GIVEN_value_set_WHEN_consume_THEN_returns_value() { 23 | val data = SerializableData() 24 | val container = SerializableContainer(value = data, strategy = SerializableData.serializer()) 25 | 26 | val newData = container.consume(SerializableData.serializer()) 27 | 28 | assertEquals(data, newData) 29 | } 30 | 31 | @Test 32 | fun GIVEN_value_set_and_consumed_WHEN_consume_second_time_THEN_returns_null() { 33 | val container = SerializableContainer(value = SerializableData(), strategy = SerializableData.serializer()) 34 | container.consume(SerializableData.serializer()) 35 | 36 | val newData = container.consume(SerializableData.serializer()) 37 | 38 | assertNull(newData) 39 | } 40 | 41 | @Test 42 | fun serializes_and_deserializes_initial_data() { 43 | val data = SerializableData() 44 | val container = SerializableContainer(value = data, strategy = SerializableData.serializer()) 45 | val newContainer = container.serializeAndDeserialize() 46 | val newData = newContainer.consume(strategy = SerializableData.serializer()) 47 | 48 | assertEquals(data, newData) 49 | } 50 | 51 | @Test 52 | fun serializes_and_deserializes_set_data() { 53 | val data = SerializableData() 54 | val container = SerializableContainer() 55 | container.set(value = data, strategy = SerializableData.serializer()) 56 | val newContainer = container.serializeAndDeserialize() 57 | val newData = newContainer.consume(strategy = SerializableData.serializer()) 58 | 59 | assertEquals(data, newData) 60 | } 61 | 62 | @Test 63 | fun serializes_and_deserializes_initial_data_twice() { 64 | val data = SerializableData() 65 | val container = SerializableContainer(value = data, strategy = SerializableData.serializer()) 66 | val newContainer = container.serializeAndDeserialize().serializeAndDeserialize() 67 | val newData = newContainer.consume(strategy = SerializableData.serializer()) 68 | 69 | assertEquals(data, newData) 70 | } 71 | 72 | @Test 73 | fun serializes_and_deserializes_set_data_twice() { 74 | val data = SerializableData() 75 | val container = SerializableContainer() 76 | container.set(value = data, strategy = SerializableData.serializer()) 77 | val newContainer = container.serializeAndDeserialize().serializeAndDeserialize() 78 | val newData = newContainer.consume(strategy = SerializableData.serializer()) 79 | 80 | assertEquals(data, newData) 81 | } 82 | 83 | @Test 84 | fun serializes_and_deserializes_initial_null() { 85 | val container = SerializableContainer() 86 | val newContainer = container.serializeAndDeserialize() 87 | val newData = newContainer.consume(strategy = SerializableData.serializer()) 88 | 89 | assertNull(newData) 90 | } 91 | 92 | @Test 93 | fun serializes_and_deserializes_set_null() { 94 | val container = SerializableContainer() 95 | container.set(null, SerializableData.serializer()) 96 | val newContainer = container.serializeAndDeserialize() 97 | val newData = newContainer.consume(strategy = SerializableData.serializer()) 98 | 99 | assertNull(newData) 100 | } 101 | 102 | @Test 103 | fun serializes_and_deserializes_initial_null_twice() { 104 | val container = SerializableContainer() 105 | val newContainer = container.serializeAndDeserialize().serializeAndDeserialize() 106 | val newData = newContainer.consume(strategy = SerializableData.serializer()) 107 | 108 | assertNull(newData) 109 | } 110 | 111 | @Test 112 | fun serializes_and_deserializes_set_null_twice() { 113 | val container = SerializableContainer() 114 | container.set(null, SerializableData.serializer()) 115 | val newContainer = container.serializeAndDeserialize().serializeAndDeserialize() 116 | val newData = newContainer.consume(strategy = SerializableData.serializer()) 117 | 118 | assertNull(newData) 119 | } 120 | 121 | @Test 122 | fun serializes_and_deserializes_list() { 123 | val data = SerializableData() 124 | 125 | val containers = 126 | Containers( 127 | listOf( 128 | SerializableContainer(data, SerializableData.serializer()), 129 | SerializableContainer(), 130 | null, 131 | ) 132 | ) 133 | 134 | val newContainers = containers.serializeAndDeserialize(Containers.serializer()) 135 | 136 | val (container1, container2, container3) = newContainers.list 137 | assertNotNull(container1) 138 | assertNotNull(container2) 139 | assertNull(container3) 140 | val newData1 = container1.consume(SerializableData.serializer()) 141 | val newData2 = container2.consume(SerializableData.serializer()) 142 | assertEquals(newData1, data) 143 | assertNull(newData2) 144 | } 145 | 146 | @Test 147 | fun serializes_and_deserializes_list_twice() { 148 | val data = SerializableData() 149 | 150 | val containers = 151 | Containers( 152 | listOf( 153 | SerializableContainer(data, SerializableData.serializer()), 154 | SerializableContainer(), 155 | null, 156 | ) 157 | ) 158 | 159 | val newContainers = containers.serializeAndDeserialize(Containers.serializer()).serializeAndDeserialize(Containers.serializer()) 160 | 161 | val (container1, container2, container3) = newContainers.list 162 | assertNotNull(container1) 163 | assertNotNull(container2) 164 | assertNull(container3) 165 | val newData1 = container1.consume(SerializableData.serializer()) 166 | val newData2 = container2.consume(SerializableData.serializer()) 167 | assertEquals(newData1, data) 168 | assertNull(newData2) 169 | } 170 | 171 | @Serializable 172 | private class Containers( 173 | val list: List, 174 | ) 175 | } 176 | --------------------------------------------------------------------------------