Tracking Lifecycle changes in Compose Multiplatform: IOS and Android

Kashif Mehmood
ProAndroidDev
Published in
5 min readFeb 5, 2024

--

If you are an Android developer, moving towards Compose Multiplatform you already know how the lifecycle works in Android activities. It used to be the first lesson while learning Android Development. The plain old onStart, onCreate, and onResume methods. However, with time this got complex with fragment lifecycle and then Compose’s internal lifecycle with different side effects.

With the introduction of Compose Multiplatform, this concept became more complex as now it is not only the Android lifecycle you need to worry about but also other platforms as well that you might be targeting that might differ from your familiar platform.

In this article, we will be learning how you can track Android and iOS platform lifecycles when working with Compose Multiplatform. This can come in handy in situations like clearing cache, initializing SDKs, removing broadcast listeners, sending analytics, etc.

Let’s start with creating an enum class that holds our lifecycle events,

/**
*
* An enum class that contains lifecycle events
*/
enum class LifecycleRegistry {
onCreate, onStart, onResume, onPause, onStop, onDestroy, onAny
}

I have named these same as the lifecycle events of Android activities just to have a better understanding. Let’s create a function that will be invoked when a lifecycle change occurs, I am keeping it simple, you can modify this according to your needs.

fun lifecycleCallBack(event: LifecycleRegistry) {
println(event.name)
}

In Android, we can create a custom lifecycle observer that can observe lifecycle changes and perform actions accordingly.

For this, you need to define a Custom Lifecycle Event Observer and add the observer to the lifecycle of the activity

val lifeCycleObserver = LifecycleEventObserver { _ , event ->
when (event) {
Lifecycle.Event.ON_RESUME -> lifecycleCallBack(LifecycleRegistry.onResume)
Lifecycle.Event.ON_PAUSE -> lifecycleCallBack(LifecycleRegistry.onPause)
Lifecycle.Event.ON_CREATE -> lifecycleCallBack(LifecycleRegistry.onCreate)
Lifecycle.Event.ON_STOP -> lifecycleCallBack(LifecycleRegistry.onStop)
Lifecycle.Event.ON_DESTROY -> lifecycleCallBack(LifecycleRegistry.onDestroy)
Lifecycle.Event.ON_START -> lifecycleCallBack(LifecycleRegistry.onStart)
Lifecycle.Event.ON_ANY -> lifecycleCallBack(LifecycleRegistry.onAny)
}
}
lifecycle.addObserver(lifeCycleObserver)

Here we create a LifecycleEventObserver, inside the observer we check which event has occurred and invoke our callback with that particular event of our LifecycleRegistry enum. At the end, we add this to the lifecycle of the activity. Now, let’s check if we can see the lifecycle change logs in the Logcat.

We can see the events in the Logcat so it is working fine on the Android side.

Now let’s move to the iOS part.

The iOS view lifecycle differs from the Android lifecycle in naming but it is not so different after all

The view lifecycle of iOS is similar to the activity lifecycle in Android. But how do we track the lifecycle? Luckily, It’s a very easy thing to do.

fun MainViewController() = ComposeUIViewController { App() }

This is how a normal compose base UI view controller looks, but there is magic hidden underneath the ComposeUIViewController. The ComposeUIViewController can take two lambdas as parameters configure and content. In the above example we are only passing the content parameter, so let’s add a configure parameter.

fun MainViewController() = ComposeUIViewController(configure = {
}) {
App()
}

inside the configure block, you can as the name suggests configure the UI View Controller. Below is the complete configuration code

class ComposeUIViewControllerConfiguration {
/**
* Control Compose behaviour on focus changed inside Compose.
*/
var onFocusBehavior: OnFocusBehavior = OnFocusBehavior.FocusableAboveKeyboard
/**
* Reassign this property with an object implementing [ComposeUIViewControllerDelegate] to receive
* UIViewController lifetime events.
*/
var delegate = object : ComposeUIViewControllerDelegate {}
}
/**
* Interface for UIViewController specific lifetime callbacks to allow injecting logic without overriding internal ComposeWindow.
* All of those callbacks are invoked at the very end of overrided function implementation.
*/
interface ComposeUIViewControllerDelegate {
fun viewDidLoad() = Unit
fun viewWillAppear(animated: Boolean) = Unit
fun viewDidAppear(animated: Boolean) = Unit
fun viewWillDisappear(animated: Boolean) = Unit
fun viewDidDisappear(animated: Boolean) = Unit
}
sealed interface OnFocusBehavior {
/**
* The Compose view will stay on the current position.
*/
object DoNothing : OnFocusBehavior
/**
* The Compose view will be panned in "y" coordinates.
* A focusable element should be displayed above the keyboard.
*/
object FocusableAboveKeyboard : OnFocusBehavior
// TODO Better to control OnFocusBehavior with existing WindowInsets.
// Definition: object: FocusableBetweenInsets(insets: WindowInsets) : OnFocusBehavior
// Usage: onFocusBehavior = FocusableBetweenInsets(WindowInsets.ime.union(WindowInsets.systemBars))
}

We will only be using the delegate property here,

fun MainViewController() = ComposeUIViewController(configure = {
/**
* A delegate to track composeViewControllers lifecycle callbacks
*/
delegate = object : ComposeUIViewControllerDelegate {
override fun viewDidAppear(animated: Boolean) {
super.viewDidAppear(animated)
lifecycleCallBack(LifecycleRegistry.onResume)
}
override fun viewDidLoad() {
super.viewDidLoad()
lifecycleCallBack(LifecycleRegistry.onCreate)
}
override fun viewWillDisappear(animated: Boolean) {
super.viewWillDisappear(animated)
lifecycleCallBack(LifecycleRegistry.onPause)
}
override fun viewWillAppear(animated: Boolean) {
super.viewWillAppear(animated)
lifecycleCallBack(LifecycleRegistry.onStart)
}
override fun viewDidDisappear(animated: Boolean) {
super.viewDidDisappear(animated)
lifecycleCallBack(LifecycleRegistry.onDestroy)
}
}
}) { App() }

In the code above, we have implemented the ComposeUIViewControllerDelegate interface which invokes lifecycle methods on state changes, and inside that, we use our lifecycle callback and map the iOS functions to the events in our lifecycleRegistry enum.

Now let’s check if these are working as they should by checking them in the Xcode logs.

So far so good, This is how lifecycles can be tracked in Compose Multiplatform. This is a bare minimum but you can modify this according to your need for example using a SharedFlow which updates the state and can be collected inside a composable. It depends on how you need to do it but the good thing is you can do it.

The code can be found here:

Let’s connect:

https://www.linkedin.com/in/kashif-mehmood-km/

https://twitter.com/kashif_mehmood_

--

--

Software Engineer | Kotlin Multiplatfrom | Jetpack/Multiplatform Compose |