The most essential libraries for Kotlin Multiplatform development.
Supported targets:
androidjvmjswasmJsioswatchostvosmacoslinuxX64
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.
There is ApplicationLifecycle available for ios and tvos targets. It follows the UIApplication lifecycle notifications.
⚠️ Since this implementation subscribes toUIApplicationglobal lifecycle events, the instance and all its registered callbacks (and whatever they capture) will stay in memory until the application is destroyed or untilApplicationLifecycle#destroymethod is called. It's ok to use it in a global scope likeUIApplicationDelegate, but it may cause memory leaks when used in a narrower scope likeUIViewControllerif it gets destroyed earlier. Use thedestroymethod to destroy the lifecycle manually and prevent memory leaks.
There are some useful Lifecycle extensions for Reaktive.
- Automatic management of
DisposableandDisposableScopebyLifecycle, can be found here.
There are some useful Lifecycle extensions for Coroutines.
- Automatic management of
CoroutineScopebyLifecycle, can be found here Flow.withLifecycle(Lifecycle): Flow- can be found here.Lifecycle.repeatOnLifecycle(block)- 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()When writing common code targeting Android, it might be required to preserve some data over process death or Android configuration changes. For this purpose, Essenty provides the StateKeeper API, which is inspired by the AndroidX SavedStateHandle.
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. 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.
From Android side, StateKeeper can be obtained by using special functions, can be found here.
There are also some handy extension functions for serializing/deserializing KSerializable objects to/from Bundle:
fun <T : Any> Bundle.putSerializable(key: String?, value: T?, strategy: SerializationStrategy<T>)fun <T : Any> Bundle.getSerializable(key: String?, strategy: DeserializationStrategy<T>): T?fun Bundle.putSerializableContainer(key: String?, value: SerializableContainer?)fun Bundle.getSerializableContainer(key: String?): SerializableContainer?
Similar extensions are also available for PersistableBundle.
⚠️ Make sure you setupkotlinx-serializationproperly.
import com.arkivanov.essenty.statekeeper.StateKeeper
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
)
}import com.arkivanov.essenty.statekeeper.StateKeeper
import com.arkivanov.essenty.statekeeper.saveable
import kotlinx.serialization.Serializable
class SomeLogic(stateKeeper: StateKeeper) {
private var state: State by stateKeeper.saveable(serializer = State.serializer(), init = ::State)
@Serializable
private class State(val someValue: Int = 0)
}import com.arkivanov.essenty.statekeeper.StateKeeper
import com.arkivanov.essenty.statekeeper.saveable
import kotlinx.serialization.Serializable
class SomeLogic(stateKeeper: StateKeeper) {
private val viewModel by stateKeeper.saveable(serializer = State.serializer(), state = ViewModel::state) { savedState ->
ViewModel(state = savedState ?: State())
}
private class ViewModel(var state: State)
@Serializable
private class State(val someValue: Int = 0)
}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.
import com.arkivanov.essenty.statekeeper.polymorphicSerializer
import com.slack.circuit.runtime.screen.Screen
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic
object FilterSerializer : KSerializer<Filter> by polymorphicSerializer(
SerializersModule {
polymorphic(Filter::class) {
subclass(TextFilter::class, TextFilter.serializer())
subclass(RatingFilter::class, RatingFilter.serializer())
}
}
)On Android, the StateKeeper obtained via one of the extensions described above automatically saves and restores the state. On other platforms (if needed) the state can be saved and restored manually. A default implementation of StateKeeperDisptacher interface can be instantiated using the corresponding builder function. The state can be encoded as a JSON string and saved using the corresponding platform-specific API.
import com.arkivanov.essenty.statekeeper.SerializableContainer
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 when it's time to save the state
val savedState: SerializableContainer = stateKeeperDispatcher.save()
// The returned SerializableContainer can now be saved using the corresponding platform-specific APIWhen 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 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.
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 viewModel = instanceKeeper.getOrCreate { ViewModel() }
}
/*
* Survives Android configuration changes.
* ⚠️ Pay attention to not leak any dependencies.
*/
class ViewModel : InstanceKeeper.Instance {
override fun onDestroy() {
// Called when the screen is finished
}
}class SomeLogic(instanceKeeperOwner: InstanceKeeperOwner) : InstanceKeeperOwner by instanceKeeperOwner {
// Get the existing instance or create a new one
private val viewModel = retainedInstance { ViewModel() }
}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
