Essenty
Essenty copied to clipboard
The most essential libraries for Kotlin Multiplatform development
Essenty
The most essential libraries for Kotlin Multiplatform development.
Supported targets:
androidjvmjs(IRandLEGACY)iosArm64,iosX64watchosArm32,watchosArm64,watchosX64tvosArm64,tvosX64macosX64linuxX64
Lifecyle
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.
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
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.
Setup
Groovy:
plugins {
id "kotlin-parcelize" // Apply the plugin for Android
}
// 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
}
// Add the dependency, typically under the commonMain source set
implementation("com.arkivanov.essenty:parcelable:<essenty_version>")
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
If you don't own the type that you need to @Parcelize, you can write a custom Parceler for it (similar to kotlin-parcelize).
// 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 all other sources (or in a custom nonAndroidMain source set)
internal actual object InstantParceler : Parceler<Instant>
Which can be used as follows:
// In commonMain
import com.arkivanov.essenty.parcelable.Parcelable
import com.arkivanov.essenty.parcelable.Parcelize
import com.arkivanov.essenty.parcelable.WriteWith
import kotlinx.datetime.Instant
@Parcelize
data class User(
val id: Long,
val name: String,
val dateOfBirth: @WriteWith<InstantParceler> Instant,
) : Parcelable
Parcelize for Darwin/Apple targets
Currently there is no extra code generated when compiled for Darwin/Apple targets. However I made a proof of concept: kotlin-parcelize-darwin compiler plugin. It is not used yet by Essenty, and the applicabilty is being considered. Please raise a Discussion if you are interested.
StateKeeper
When writing common code targetting 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
StateKeeperAPI relies on theParcelableinterface provided by theparcelablemodule described above. It can fail in non-instrumented Android tests (unit tests). Consider using your own test implementations or mocks.
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 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.
Android extensions
From Android side, StateKeeper can be obtained by using special functions, can be found here.
Usage example
Using the StateKeeper
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 StateKeeperDisptacher manually
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 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.
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.
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.
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
}
BackPressedDispatcher
⚠️
BackPressedHandlerandBackPressedDispatcherAPI (theback-pressedmodule) is entirely deprecated. Please useBackHandlerandBackDispatcherdescribed above.
The BackPressedDispatcher API provides ability to handle back button events (e.g. an 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-pressed:<essenty_version>"
Kotlin:
// Add the dependency, typically under the commonMain source set
implementation("com.arkivanov.essenty:back-pressed:<essenty_version>")
Content
The BackPressedHandler interface provides ability to register/unregister back button handlers. When the device's back button is clicked, all registered handlers are called in reverse order. If a handler returns true then the event is considered as handled and the handling process stops, the remaining handlers are not called. If none of the handlers returned true then the event is considered as unhandled.
The BackPressedDispatcher interface extends BackPressedHandler and is responsible for triggering the registered handlers. The BackPressedDispatcher.onBackPressed() triggers all registered handlers in reverse order, returns true if the event is handled, and false if the event is unhandled.
Android extensions
From Android side, BackPressedDispatcher can be obtained by using special functions, can be found here.
⚠️ Due to the nature of AndroidX
OnBackPressedDispatcherAPI, it is not possible to map it 1-1 toBackPressedHandler. Please keep in mind some possible side effects described in the corresponding KDocs.
Usage example
Using the BackPressedHandler
class SomeLogic(backPressedHandler: BackPressedHandler) {
init {
backPressedHandler.register {
// Called when the back button is pressed
true // Return true to consume the event, or false to allow other registered callbacks
}
}
}
Using the BackPressedDispatcher manually
A default implementation of the BackPressedDispatcher interface can be instantiated using the corresponding builder function:
import com.arkivanov.essenty.backpressed.BackPressedDispatcher
import com.arkivanov.essenty.backpressed.BackPressedHandler
val backPressedDispatcher = BackPressedDispatcher()
val someLogic = SomeLogic(backPressedDispatcher)
if (!backPressedDispatcher.onBackPressed()) {
// The back pressed event was not handled
}
Author
Twitter: @arkann1985