├── 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 |
--------------------------------------------------------------------------------