The most essential libraries for Kotlin Multiplatform development.
Supported targets:
android
jvm
js
(IR
andLEGACY
)ios
watchos
tvos
macos
linuxX64
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.
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>")
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.
From Android, the Lifecycle
can be obtained by using special functions, can be found here.
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
}
}
}
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()
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.
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 theparcelize-darwin
plugin, please report them here.
Parcelable
interface extends java.io.Serializable
on JVM. This makes it possible to serialize and deserialize Parcelable
classes as ByteArray
using ObjectOutputStream and ObjectInputStream.
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.
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.
⚠️ Supported only on Android and Darwin (Apple) targets. For Darwin (Apple) targets the support was added in version1.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).
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)
}
}
// 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>
// 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
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.
⚠️ TheStateKeeper
API relies on theParcelable
interface provided by theparcelable
module described above. It can fail in non-instrumented Android tests (unit tests). Consider using your own test implementations or mocks.
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>")
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 extens 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.
From Android side, StateKeeper
can be obtained by using special functions, can be found here.
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
}
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()
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.
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>")
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 extens 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.
From Android side, InstanceKeeper
can be obtained by using special functions, can be found here.
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
}
}
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()
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.
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>")
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.
From Android side, BackHandler
can be obtained by using special functions, can be found here.
Both BackHandler
and BackDispatcher
bring the new Android Predictive Back Gesture to Kotlin Multiplatform.
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
.
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.
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
}
}
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
}
Twitter: @arkann1985