arkivanov / Essenty

The most essential libraries for Kotlin Multiplatform development

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Maven Central License Twitter URL

Essenty

The most essential libraries for Kotlin Multiplatform development.

Supported targets:

  • android
  • jvm
  • js
  • wasmJs (since version 2.0.0-alpha01)
  • ios
  • watchos
  • tvos
  • macos
  • linuxX64

Lifecycle

When writing Kotlin Multiplatform (common) code we often need to handle lifecycle events of a screen. For example, to stop background operations when the screen is destroyed, or to reload some data when the screen is activated. Essenty provides the Lifecycle API to help with lifecycle handling in the common code. It is very similar to Android Activity lifecycle.

Setup

Groovy:

// Add the dependency, typically under the commonMain source set
implementation "com.arkivanov.essenty:lifecycle:<essenty_version>"

Kotlin:

// Add the dependency, typically under the commonMain source set
implementation("com.arkivanov.essenty:lifecycle:<essenty_version>")

Lifecycle state transitions

Content

The main Lifecycle interface provides ability to observe the lifecycle state changes. There are also handy extension functions for convenience.

The LifecycleRegistry interface extends both the Lifecycle and the Lifecycle.Callbacks at the same time. It can be used to manually control the lifecycle, for example in tests. You can also find some useful extension functions.

The LifecycleOwner just holds the Lifecyle. It may be implemented by an arbitrary class, to provide convenient API.

Android extensions

From Android, the Lifecycle can be obtained by using special functions, can be found here.

iOS and tvOS extensions (since v2.0.0-alpha07)

There is ApplicationLifecycle awailable for ios and tvos targets. It follows the UIApplication lifecycle notifications.

⚠️ Since this implementation subscribes to UIApplication global lifecycle events, the instance and all its registered callbacks (and whatever they capture) will stay in memory until the application is destroyed. It's ok to use it in a global scope like UIApplicationDelegate, but it may cause memory leaks when used in a narrower scope like UIViewController if it gets destroyed earlier.

Reaktive extensions

There are some useful Lifecycle extensions for Reaktive.

  • Automatic management of Disposable and DisposableScope by Lifecycle, can be found here.

Coroutines extensions

There are some useful Lifecycle extensions for Coroutines.

  • Automatic management of CoroutineScope by Lifecycle, can be found here
  • Flow.withLifecycle(Lifecycle): Flow - can be found here.
  • Lifecycle.repeatOnLifecycle(block) - can be found here.

Usage example

Observing the Lifecyle

The lifecycle can be observed using its subscribe/unsubscribe methods:

import com.arkivanov.essenty.lifecycle.Lifecycle

class SomeLogic(lifecycle: Lifecycle) {
    init {
        lifecycle.subscribe(
            object : Lifecycle.Callbacks {
                override fun onCreate() {
                    // Handle lifecycle created
                }

                // onStart, onResume, onPause, onStop are also available

                override fun onDestroy() {
                    // Handle lifecycle destroyed
                }
            }
        )
    }
}

Or using the extension functions:

import com.arkivanov.essenty.lifecycle.Lifecycle
import com.arkivanov.essenty.lifecycle.doOnCreate
import com.arkivanov.essenty.lifecycle.doOnDestroy
import com.arkivanov.essenty.lifecycle.subscribe

class SomeLogic(lifecycle: Lifecycle) {
    init {
        lifecycle.subscribe(
            onCreate = { /* Handle lifecycle created */ },
            // onStart, onResume, onPause, onStop are also available
            onDestroy = { /* Handle lifecycle destroyed */ }
        )

        lifecycle.doOnCreate {
            // Handle lifecycle created
        }

        // doOnStart, doOnResume, doOnPause, doOnStop are also available

        lifecycle.doOnDestroy {
            // Handle lifecycle destroyed
        }
    }
}

Using the LifecycleRegistry manually

A default implementation of the LifecycleRegisty interface can be instantiated using the corresponding builder function:

import com.arkivanov.essenty.lifecycle.LifecycleRegistry
import com.arkivanov.essenty.lifecycle.resume
import com.arkivanov.essenty.lifecycle.destroy

val lifecycleRegistry = LifecycleRegistry()
val someLogic = SomeLogic(lifecycleRegistry)

lifecycleRegistry.resume()

// At some point later
lifecycleRegistry.destroy()

Parcelable and Parcelize (deprecated since v1.3.0-alpha01, removed since v2.0.0-alpha01)

⚠️ Unfortunately, the new K2 compiler will not support Parcelable/Parcelize with Kotlin Multiplatform (see #102). This module is mostly deprecated since v1.3.0-alpha01 and will be removed in v2.0. As a replacement, Essenty supports kotlinx-serialization since v1.3.0-alpha01.

Essenty brings both Android Parcelable interface and the @Parcelize annotation from kotlin-parcelize compiler plugin to Kotlin Multiplatform, so they both can be used in common code. This is typically used for state/data preservation over Android configuration changes, when writing common code targeting Android.

Parcelable for Darwin (Apple) targets (experimental)

Additionally, Essenty provides an experimental support of Parcelable and @Parcelize for all Darwin (Apple) targets via parcelize-darwin compiler plugin. This only affects your project's runtime if you explicitly enable the parcelize-darwin compiler plugin in your project. Otherwise, it's just no-op.

⚠️ If you experience any issues with the parcelize-darwin plugin, please report them here.

Parcelable for JVM targets (experimental)

Parcelable interface extends java.io.Serializable on JVM. This makes it possible to serialize and deserialize Parcelable classes as ByteArray using ObjectOutputStream and ObjectInputStream.

Setup

Groovy:

plugins {
    id "kotlin-parcelize" // Apply the plugin for Android
    id "com.arkivanov.parcelize.darwin" // Optional, only if you need support for Darwin targets
}

// Add the dependency, typically under the commonMain source set
implementation "com.arkivanov.essenty:parcelable:<essenty_version>"

Kotlin:

plugins {
    id("kotlin-parcelize") // Apply the plugin for Android
    id("com.arkivanov.parcelize.darwin") // Optional, only if you need support for Darwin targets
}

// Add the dependency, typically under the commonMain source set
implementation("com.arkivanov.essenty:parcelable:<essenty_version>")

The parcelize-darwin is published on Maven Central, you may need to add mavenCentral() repository to your project. You can find more information about parcelize-darwin plugin setup here.

Usage example

Once the dependency is added and the plugin is applied, we can use it as follows:

// In commonMain

import com.arkivanov.essenty.parcelable.Parcelable
import com.arkivanov.essenty.parcelable.Parcelize

@Parcelize
data class User(
    val id: Long,
    val name: String
) : Parcelable

When compiled for Android, the Parcelable implementation will be generated automatically. When compiled for other targets, it will be just a regular class without any extra generated code.

Custom Parcelers

⚠️ Supported only on Android and Darwin (Apple) targets. For Darwin (Apple) targets the support was added in version 1.2.0-alpha-06.

If you don't own the type that you need to @Parcelize, you can write a custom Parceler for it (similar to kotlin-parcelize).

Simple option in commonMain

import com.arkivanov.essenty.parcelable.CommonParceler
import com.arkivanov.essenty.parcelable.ParcelReader
import com.arkivanov.essenty.parcelable.ParcelWriter
import com.arkivanov.essenty.parcelable.readLong
import com.arkivanov.essenty.parcelable.writeLong
import kotlinx.datetime.Instant

internal object InstantParceler : CommonParceler<Instant> {
    override fun create(reader: ParcelReader): Instant =
        Instant.fromEpochSeconds(reader.readLong())

    override fun Instant.write(writer: ParcelWriter) {
        writer.writeLong(epochSeconds)
    }
}

Full-featured parcelers with platform-specific serialization

// In commonMain

import com.arkivanov.essenty.parcelable.Parceler
import kotlinx.datetime.Instant

internal expect object InstantParceler : Parceler<Instant>
// In androidMain

import com.arkivanov.essenty.parcelable.Parceler
import kotlinx.datetime.Instant

internal actual object InstantParceler : Parceler<Instant> {
    override fun create(parcel: Parcel): Instant = 
        Instant.fromEpochSeconds(parcel.readLong())

    override fun Instant.write(parcel: Parcel, flags: Int) {
        parcel.writeLong(epochSeconds) 
    }
}
// In iosMain or darwinMain

import com.arkivanov.essenty.parcelable.Parceler
import kotlinx.datetime.Instant
import platform.Foundation.NSCoder
import platform.Foundation.decodeInt64ForKey
import platform.Foundation.encodeInt64

internal actual object InstantParceler : Parceler<Instant> {
    override fun create(coder: NSCoder): Instant =
        Instant.fromEpochSeconds(coder.decodeInt64ForKey(key = "epochSeconds"))

    override fun Instant.write(coder: NSCoder) {
        coder.encodeInt64(value = epochSeconds, forKey = "epochSeconds")
    }
}
// In all other sources (or in a custom nonAndroidMain source set)

internal actual object InstantParceler : Parceler<Instant>

Applying the parceler

// In commonMain

import com.arkivanov.essenty.parcelable.Parcelable
import com.arkivanov.essenty.parcelable.Parcelize
import com.arkivanov.essenty.parcelable.TypeParceler
import com.arkivanov.essenty.parcelable.WriteWith
import kotlinx.datetime.Instant

// Class-local parceler
@Parcelize
@TypeParceler<Instant, InstantParceler>()
data class User(
    val id: Long,
    val name: String,
    val dateOfBirth: Instant,
) : Parcelable

// Type-local parceler
@Parcelize
data class User(
    val id: Long,
    val name: String,
    val dateOfBirth: @WriteWith<InstantParceler> Instant,
) : Parcelable

StateKeeper

When writing common code targeting Android, it might be required to preserve some data over Android configuration changes or process death. For this purpose, Essenty provides the StateKeeper API, which is inspired by the AndroidX SavedStateHandle.

⚠️ The StateKeeper API is used to rely on the Parcelable interface provided by the parcelable module described above. As described above, Parcelable/Parcelize support is deprecated since v1.3.0-alpha01 and removed since v2.0.0-alpha01. As a replacement, the StateKeeper API now supports kotlinx-serialization.

Setup

Groovy:

// Add the dependency, typically under the commonMain source set
implementation "com.arkivanov.essenty:state-keeper:<essenty_version>"

Kotlin:

// Add the dependency, typically under the commonMain source set
implementation("com.arkivanov.essenty:state-keeper:<essenty_version>")

Content

The main StateKeeper interface provides ability to register/unregister state suppliers, and also to consume any previously saved state. You can also find some handy extension functions.

The StateKeeperDispatcher interface extends StateKeeper and allows state saving, by calling all registered state providers.

The StateKeeperOwner interface is just a holder of StateKeeper. It may be implemented by an arbitrary class, to provide convenient API.

Android extensions

From Android side, StateKeeper can be obtained by using special functions, can be found here.

Usage example

Using StateKeeper (the recommended way since v1.3.0-alpha01)

⚠️ Make sure you setup kotlinx-serialization properly.

import com.arkivanov.essenty.parcelable.Parcelable
import kotlinx.serialization.Serializable

class SomeLogic(stateKeeper: StateKeeper) {
    // Use the saved State if any, otherwise create a new State
    private var state: State = stateKeeper.consume(key = "SAVED_STATE", strategy = State.serializer()) ?: State()

    init {
        // Register the State supplier
        stateKeeper.register(key = "SAVED_STATE", strategy = State.serializer()) { state }
    }

    @Serializable
    private class State(
        val someValue: Int = 0
    ) : Parcelable
}
Polymorphic serialization (experimental)

Sometimes it might be necessary to serialize an interface or an abstract class that you don't own but have implemented. For this purpose Essenty provides polymorphicSerializer function that can be used to create custom polymorphic serializers for unowned base types.

For example a third-party library may have the following interface.

interface Filter {
    // Omitted code
}

Then we can have multiple implementations of Filter.

@Serializable
class TextFilter(val text: String) : Filter { /* Omitted code */ }

@Serializable
class RatingFilter(val stars: Int) : Filter { /* Omitted code */ }

Now we can create a polymorphic serializer for Filter as follows. It can be used to save and restore Filter directly via StateKeeper, or to have Filter as part of another Serializable class.

object FilterSerializer : KSerializer<Filter> by polymorphicSerializer(
    SerializersModule {
        polymorphic(Filter::class) {
            subclass(TextFilter::class, TextFilter.serializer())
            subclass(RatingFilter::class, RatingFilter.serializer())
        }
    }
)

Using StateKeeper (the deprecated Parcelable way before v1.3.0-alpha01)

import com.arkivanov.essenty.parcelable.Parcelable
import com.arkivanov.essenty.parcelable.Parcelize
import com.arkivanov.essenty.statekeeper.StateKeeper
import com.arkivanov.essenty.statekeeper.consume

class SomeLogic(stateKeeper: StateKeeper) {
    // Use the saved State if any, otherwise create a new State
    private var state: State = stateKeeper.consume("SAVED_STATE") ?: State()

    init {
        // Register the State supplier
        stateKeeper.register("SAVED_STATE") { state }
    }

    @Parcelize
    private class State(
        val someValue: Int = 0
    ) : Parcelable
}

Using the StateKeeperDispatcher manually

⚠️ There is no any kotlinx-serialization replacement for ParcelableContainer currently, please continue using it until v2.0

A default implementation of the StateKeeperDisptacher interface can be instantiated using the corresponding builder function:

import com.arkivanov.essenty.parcelable.ParcelableContainer
import com.arkivanov.essenty.statekeeper.StateKeeper
import com.arkivanov.essenty.statekeeper.StateKeeperDispatcher

val stateKeeperDispatcher = StateKeeperDispatcher(/*Previously saved state, or null*/)
val someLogic = SomeLogic(stateKeeperDispatcher)

// At some point later
val savedState: ParcelableContainer = stateKeeperDispatcher.save()

InstanceKeeper

When writing common code targetting Android, it might be required to retain objects over Android configuration changes. This use case is covered by the InstanceKeeper API, which is similar to the AndroidX ViewModel.

Setup

Groovy:

// Add the dependency, typically under the commonMain source set
implementation "com.arkivanov.essenty:instance-keeper:<essenty_version>"

Kotlin:

// Add the dependency, typically under the commonMain source set
implementation("com.arkivanov.essenty:instance-keeper:<essenty_version>")

Content

The main InstanceKeeper interface is responsible for storing object instances, represented by the [InstanceKeeper.Instance] interface. Instances of the InstanceKeeper.Instance interface survive Android Configuration changes, the InstanceKeeper.Instance.onDestroy() method is called when InstanceKeeper goes out of scope (e.g. the screen is finished). You can also find some handy extension functions.

The InstanceKeeperDispatcher interface extends InstanceKeeper and adds ability to destroy all registered instances.

The InstanceKeeperOwner interface is just a holder of InstanceKeeper. It may be implemented by an arbitrary class, to provide convenient API.

Android extensions

From Android side, InstanceKeeper can be obtained by using special functions, can be found here.

Usage example

Using the InstanceKeeper

import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.arkivanov.essenty.instancekeeper.getOrCreate

class SomeLogic(instanceKeeper: InstanceKeeper) {
    // Get the existing instance or create a new one
    private val thing: RetainedThing = instanceKeeper.getOrCreate { RetainedThing() }
}

/*
 * Survives Android configuration changes.
 * ⚠️ Pay attention to not leak any dependencies.
 */
class RetainedThing : InstanceKeeper.Instance {
    override fun onDestroy() {
        // Called when the screen is finished
    }
}

Using the InstanceKeeperDispatcher manually

A default implementation of the InstanceKeeperDispatcher interface can be instantiated using the corresponding builder function:

import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import com.arkivanov.essenty.instancekeeper.InstanceKeeperDispatcher

// Create a new instance of InstanceKeeperDispatcher, or reuse an existing one
val instanceKeeperDispatcher = InstanceKeeperDispatcher()
val someLogic = SomeLogic(instanceKeeperDispatcher)

// At some point later
instanceKeeperDispatcher.destroy()

BackHandler

The BackHandler API provides ability to handle back button clicks (e.g. the Android device's back button), in common code. This API is similar to AndroidX OnBackPressedDispatcher.

Setup

Groovy:

// Add the dependency, typically under the commonMain source set
implementation "com.arkivanov.essenty:back-handler:<essenty_version>"

Kotlin:

// Add the dependency, typically under the commonMain source set
implementation("com.arkivanov.essenty:back-handler:<essenty_version>")

Content

The BackHandler interface provides ability to register and unregister back button callbacks. When the device's back button is pressed, all registered callbacks are called in reverse order, the first enabled callback is called and the iteration finishes.

Starting from v1.2.x, when the device's back button is pressed, all registered callbacks are sorted in ascending order first by priority and then by index, the last enabled callback is called.

BackCallback allows handling back events, including predictive back gestures.

The BackDispatcher interface extends BackHandler and is responsible for triggering the registered callbacks. The BackDispatcher.back() method triggers all registered callbacks in reverse order, and returns true if an enabled callback was called, and false if no enabled callback was found.

Android extensions

From Android side, BackHandler can be obtained by using special functions, can be found here.

Predictive Back Gesture (starting from v1.2.x)

Both BackHandler and BackDispatcher bring the new Android Predictive Back Gesture to Kotlin Multiplatform.

Predictive Back Gesture on Android

On Android, the predictive back gesture only works starting with Android T. On Android T, it works only between Activities, if enabled in the system settings. Starting with Android U, the predictive back gesture also works between application's screens inside an Activity. In the latter case, back gesture events can be handled using BackCallback.

Predictive Back Gesture on other platforms

On all other platforms, predictive back gestures can be dispatched manually via BackDispatcher. This can be done e.g. by adding an overlay on top of the UI and handling touch events manually.

Usage example

Using the BackHandler

import com.arkivanov.essenty.backhandler.BackHandler

class SomeLogic(backHandler: BackHandler) {
    private val callback = BackCallback {
        // Called when the back button is pressed
    }

    init {
        backHandler.register(callback)

        // Disable the callback when needed
        callback.isEnabled = false
    }
}

Using the BackDispatcher manually

A default implementation of the BackDispatcher interface can be instantiated using the corresponding builder function:

import com.arkivanov.essenty.backhandler.BackDispatcher

val backDispatcher = BackDispatcher()
val someLogic = SomeLogic(backDispatcher)

if (!backDispatcher.back()) {
    // The back pressed event was not handled
}

Author

Twitter: @arkann1985

If you like this project you can always Buy Me A Coffee ;-)

About

The most essential libraries for Kotlin Multiplatform development

License:Apache License 2.0


Languages

Language:Kotlin 100.0%