Open-source project
arkivanov/Essenty avatar
arkivanov/Essenty

Essenty: the lifecycle and state primitives Kotlin Multiplatform was missing

The most essential libraries for Kotlin Multiplatform development

587 stars18 forksKotlinApache-2.0

At a glance

What is it?
Essenty is a set of Apache-2.0 libraries for Kotlin Multiplatform that fills the gap Android developers had for lifecycle handling, back handling, instance keeping and state preservation, so common code can react to a screen being created and destroyed on every supported target. The lifecycle module mirrors the Android Activity lifecycle and adds coroutine, Reaktive and iOS bindings, and the whole set publishes across ten targets.
Who is it for?
Adopt Essenty if you are writing Kotlin Multiplatform common code and need lifecycle, back handling or state preservation that is not available in common code today, since the library is a direct answer to that gap and mirrors the Android API your team already knows. Do not adopt it for a single-platform Android project where the framework's own lifecycle is already reachable, and check the target list before you start, because linuxArm64 is missing from the Reaktive flavour.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 25 days ago.
What is it written in?
Mainly Kotlin, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the project fills in, and for whom

Essenty is described as the most essential libraries for Kotlin Multiplatform development, and the primary language is Kotlin with an Apache-2.0 licence. The last push was on 2026-09-28 with the project not archived, and the recent releases are 2.7.0-alpha01 from 2026-09-28, 2.6.0 from 2026-09-08 and 2.5.0 from 2025-02-24.

The gap it addresses is narrow and real. When you write Kotlin Multiplatform common code, you often need to handle the 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. Android has had a lifecycle API for that for years, and it lives in the Android framework, so it is not available in common code. Essenty provides a Lifecycle API for that, and it is very similar to the Android Activity lifecycle.

That similarity is deliberate, and it is the reason the library is portable rather than merely cross-platform. A developer moving an Android codebase into common code does not have to learn a new mental model, because the states and the transitions are the ones they already know.

The supported targets list is long and tells you the scale of the problem the library is solving: android, jvm, js, wasmJs, ios, watchos, tvos, macos, linuxX64 and linuxArm64. There is one exclusion, and the README notes that linuxArm64 is not available for lifecycle-reaktive, because Reaktive does not support it yet. That is the kind of detail a README often omits, and it is exactly the one a multiplatform team hits on day one.

The repository layout shows the modules. There is lifecycle/, lifecycle-coroutines/ and lifecycle-reaktive/ for the three flavours of the lifecycle API, plus back-handler/, instance-keeper/, state-keeper/, kotlin-js-store/, utils-internal/, tools/, docs/ and a state-keeper-benchmarks/ directory. The README section you are reading covers lifecycle and state-keeper, and state-keeper in particular is described as needed when writing common code targeting Android, where data may have to be preserved over process death.

The Lifecycle API and the three ways to use it

The main Lifecycle interface provides the ability to observe lifecycle state changes, and there are also handy extension functions for convenience. The interface itself lives in the lifecycle module, alongside the extension functions and the registry.

The LifecycleRegistry interface extends both the Lifecycle and the Lifecycle.Callbacks interfaces at the same time. That is the interesting design decision here, because it means a single object can be observed and driven. The README says it can be used to manually control the lifecycle, for example in tests, and there are extension functions for it as well.

There are three ways to observe the lifecycle, and the README demonstrates all of them.

The first is the subscribe and unsubscribe methods with a callbacks object, and the callback interface exposes onCreate, onStart, onResume, onPause, onStop and onDestroy:

kotlin
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
                }
            }
        )
    }
}

The second is the extension functions, which are the same thing with a lambda per state:

kotlin
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
        }
    }
}

The third is driving the lifecycle yourself, which is what you do in a test. A default implementation of the LifecycleRegistry interface can be instantiated using the corresponding builder function:

kotlin
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()

The LifecycleOwner interface is the other half of the abstraction. It just holds the Lifecycle, and the README notes it may be implemented by an arbitrary class to provide a convenient API. That is a small interface on purpose, because it is what lets a screen class, a view model or a fragment expose its lifecycle without inheriting from anything.

The presence of the registry with manual control is what makes the library testable, and that is the argument for the design. Android's own lifecycle is driven by the framework, which is why testing Android lifecycle behaviour is awkward. Here you construct a registry, move it to resume, run your logic, and then destroy it, and your common code sees the same sequence it would see on a device.

Platform bindings, and the memory warning that comes with them

The library does not stop at the common API, and the platform-specific sections are where the real work is.

On Android, the Lifecycle can be obtained using special functions, with the implementation in the androidMain source set of the lifecycle module. On iOS and tvOS there is an ApplicationLifecycle available for the ios and tvos targets, and it follows the UIApplication lifecycle notifications.

That last one comes with a warning, and it is the kind of warning that saves an afternoon of leak hunting. 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 or until the destroy method is called. The README says it is fine to use in a global scope like a UIApplicationDelegate, but that it may cause memory leaks when used in a narrower scope like a UIViewController if that is destroyed earlier, and it says to use the destroy method to destroy the lifecycle manually and prevent memory leaks.

The reason is worth understanding rather than just obeying. The Android lifecycle is driven by the framework, which owns the lifetime. On iOS, the observation is a subscription to an application-wide notification, so the subscriber is held by the notification source, and the only thing that breaks the reference is an explicit teardown. A view controller that goes away does not unsubscribe the application on its behalf.

The same pattern shows up in the two extension modules. There are Reaktive extensions, including automatic management of Disposable and DisposableScope by the Lifecycle. And there are coroutine extensions, with automatic management of CoroutineScope by the Lifecycle, a Flow.withLifecycle(Lifecycle) function, and a Lifecycle.repeatOnLifecycle(block) function.

The coroutine one deserves a note, because the names collide with Android's own lifecycle-aware APIs. The point of a shared repeatOnLifecycle is that common code can restart a collection when the screen is active and cancel it when the screen is not, which is a pattern that is easy to get subtly wrong by hand and impossible to do at all in common code without something like this.

Setup, and the packaging detail that matters for multiplatform

Adding the dependency is the same in any build system, and the README gives both forms. In Groovy:

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

And in Kotlin:

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

The comment in both blocks is the part worth reading: the dependency goes under the commonMain source set. That is a Kotlin Multiplatform convention rather than an Essenty one, but it tells you how the library expects to be consumed, and it is the detail that trips people up when they add it to a shared module and then find that the actual source set is commonMain, commonTest and similar.

The coordinates are com.arkivanov.essenty with the artefact name, lifecycle in this case, and the version placeholder. The Maven Central badge points at a search for the group com.arkivanov.essenty, which is a useful trick: searching the group rather than the artefact shows every module in the set, and the module list in the repository tree is the same set, so the two views agree.

That agreement matters for a library that describes itself as the most essential ones. There is a lifecycle module, a back-handler module for platform back gestures, an instance-keeper module for retaining objects across configuration changes, a state-keeper module for preserving data over process death, a kotlin-js-store module, an internal utilities module and a benchmarks module for the state keeper. The README section on StateKeeper begins exactly where the lifecycle one stops, describing the case where common code targeting Android may need to preserve some data over process death.

The build setup is a conventional Gradle Kotlin DSL project with a deps.versions.toml for version management, a detekt.yml for static analysis, settings.gradle.kts and build.gradle.kts at the root, a gradle.properties, the gradle wrapper, and an .editorconfig. There is also a docs/ directory for media such as the lifecycle state transition diagram shown in the README.

How to evaluate it

The evaluation is mostly about whether you have the problem.

If you are writing shared code for a Kotlin Multiplatform project and you need to stop background work when a screen goes away, this library removes a real obstacle, because the Android answer is not reachable from common code. If you are writing a pure Android or pure JVM application, you have the framework's own lifecycle and this is one more dependency.

The two questions to check before adopting are the target list and the module you need. The target list is explicit, including the linuxArm64 gap for the Reaktive flavour, so you can rule it in or out immediately. The module list matters because the README covers lifecycle and state-keeper, and the other modules have their own documentation.

For the lifecycle module specifically, there is a design point to weigh. The API mirrors the Android Activity lifecycle rather than inventing a cross-platform one, and the LifecycleRegistry exists to make that lifecycle drivable in tests. If your team is coming from Android, that is a short learning curve. If you are coming from Compose Multiplatform or a framework with its own lifecycle model, you need to decide which lifecycle drives the shared code, because a project with two competing lifecycles is a common source of confusion.

The coroutine and Reaktive extensions are the parts that will save the most time, and they are also the parts where the API names are most likely to collide with ones you already use. Flow.withLifecycle and repeatOnLifecycle in particular need a decision about which one you call in common code, and mixing them with the platform ones in the same project is the mistake to avoid.

Finally, note the release status. There is a 2.6.0 stable release and a 2.7.0-alpha01 alongside it, so the project is mid-cycle on the 2.7 line. Pin the version you use, and read the migration notes for whatever minor version you adopt, because a library with this much surface area across ten targets will move names more often than a single-platform library would.

Editorial conclusion

Adopt Essenty if you are writing Kotlin Multiplatform common code and need lifecycle, back handling or state preservation that is not available in common code today, since the library is a direct answer to that gap and mirrors the Android API your team already knows. Do not adopt it for a single-platform Android project where the framework's own lifecycle is already reachable, and check the target list before you start, because linuxArm64 is missing from the Reaktive flavour. Before you commit, decide which lifecycle drives your shared code so you are not calling two sets of repeatOnLifecycle functions in one project, read the iOS ApplicationLifecycle warning about manually calling destroy to avoid leaks, and pin the version you use, since the project ships an alpha alongside each stable release.

Frequently asked questions

What is Essenty?

It is a set of Apache-2.0 libraries for Kotlin Multiplatform development, providing a Lifecycle API for common code along with back handling, instance keeping and state preservation modules. The primary language is Kotlin, the last push was on 2026-09-28, and the recent releases include 2.6.0 and 2.7.0-alpha01.

Which platforms does Essenty support?

The listed targets are android, jvm, js, wasmJs, ios, watchos, tvos, macos, linuxX64 and linuxArm64, with the exception that linuxArm64 is not available for lifecycle-reaktive because Reaktive does not support it yet.

How do I add the Essenty lifecycle dependency?

Add it under the commonMain source set, using the group com.arkivanov.essenty and the artefact name, for example implementation("com.arkivanov.essenty:lifecycle:<essenty_version>") in a Kotlin build script or the Groovy equivalent.

What does the Essenty LifecycleRegistry do?

The LifecycleRegistry interface extends both the Lifecycle and the Lifecycle.Callbacks interfaces at the same time, so a single object can be observed and driven. It can be used to manually control the lifecycle, for example in tests, and a default implementation is instantiated with a builder function and then moved through states such as resume and destroy.

What are the coroutine and Reaktive extensions in Essenty?

The coroutine extensions provide automatic management of a CoroutineScope by the Lifecycle, a Flow.withLifecycle(Lifecycle) function and a Lifecycle.repeatOnLifecycle(block) function. The Reaktive extensions provide automatic management of Disposable and DisposableScope by the Lifecycle.

Official sources

  1. arkivanov/Essenty on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/arkivanov-essenty.svg)](https://hysenlabs.com/projects/arkivanov-essenty)

Was this useful?