Mark your models as stable with the Compose runtime annotation library
In this article, we’ll look at how to address this issue using the new compose-runtime-annotation library.

In Jetpack Compose, understanding stability is pretty important because it directly affects recomposition and overall UI performance. There are several strategies for making composable functions stable, and one of the most common is marking your classes with the stability annotations @Immutable
and @Stable
.
However, there’s a catch: even if your model class is entirely composed of immutable properties, it will still be marked as unstable if it comes from a different package. This becomes especially problematic when building libraries, SDKs, or apps with a multi-module architecture, since all classes from external packages are treated as unstable, something you can’t control from the call site.
In this article, we’ll look at how to address this issue using the new compose-runtime-annotation
library, with a real-world example of how the RevenueCat’s Android SDK solved it in practice.
The stability problem
In Jetpack Compose, recomposition is triggered when state changes or new parameters are passed into a composable. The key factor the runtime uses to decide whether two objects are equivalent is stability. Thanks to smart stability checks, even if state or parameters change, Compose can skip recomposition when it determines the objects are stable.
However, even if you intend for a class to be stable, it will ultimately be considered unstable if it contains any unstable types. For example, consider the following model class, which might typically come from a network response:
1data class User(
2 val id: String,
3 val name: String,
4 val profiles: List<String>,
5)
Since interface types are treated as unstable, the User class above ends up unstable because the profiles property is a List. In cases like this, you can explicitly mark the class with @Immutable
or @Stable
to inform the Compose compiler: “I know this class is immutable, so please treat it as stable.”
1@Immutable
2data class User(
3 val id: String,
4 val name: String,
5 val profiles: List<String>,
6)
On the other hand, if your class contains only stable types, like in the example below, the Compose compiler will consider it stable by default:
1data class User(
2 val id: String,
3 val name: String,
4 val address: String,
5)
6
The problem, however, is that even if a class contains only stable types, it will still be treated as unstable if it originates from another package. For example, if the User class above comes from an open-source library, an SDK, or even another module in a multi-module architecture, the compiler will still consider it unstable.
You could technically resolve this issue by adding the compose-runtime library and annotating those classes with stability annotations. However, it’s generally not a good idea to introduce a dependency on Compose in modules that are completely unrelated, such as a module containing only plain POJO models.
Even if you add compose-runtime to other modules/libraries/SDKs, those modules then gain access to Compose runtime features like SideEffect
, LaunchedEffect
, snapshotFlow
, StateFlow<T>.collectAsState()
, and many more. These APIs will start appearing in your IDE’s autocomplete, which increases the risk of accidental or improper usage in places they don’t belong.
Compose runtime annotation library
The idea, then, is: what if we could use “only” the stability annotations in our Compose-unrelated modules, such as pure JVM or core modules?
To address this, the Jetpack Compose team recently released the compose-runtime-annotation
library, which contains only the stability annotations. It is designed for Kotlin Multiplatform (JVM, Android, iOS, etc.) and is available under the following package:
1compileOnly("androidx.compose.runtime:runtime-annotation:1.9.0")
And now, you’ll be able to import only the stability-related annotations, such as @Immutable
, @Stable
, and @StableMarker
, and mark your classes as stable with those annotations.
The reason you can use compileOnly
instead of the implementation
method in the Gradle file is that the only requirement is to let the Compose compiler know your classes are stable. These annotations are only utilized at compile time rather than at runtime.
One great real-world example is RevenueCat’s Android SDK. It recently adopted the compose-runtime-annotation library to mark classes in its core module as stable, improving overall UI performance. You can check out the PR to see how the library was introduced and applied.
Compose stable marker library
Before the compose-runtime-annotation
library was released, there was the original Compose Stable Marker library, introduced about two years earlier, which behaves very similarly.
1compileOnly("com.github.skydoves:compose-stable-marker:1.0.2")
Now that the official library is available, it’s generally best to use it, but there’s one caveat to keep in mind. The compose-runtime-annotation
library requires a compileSdkVersion
of at least 34. While the library itself supports older devices (the min SDK is down to API 21), building and verifying it correctly requires modern build tools (compile SDK 34 or higher).
Most projects today are already targeting compileSdkVersion
34 or higher, especially since Google Play is pushing developers to migrate to target SDK 35, but if you’re building libraries or SDKs that are still stuck below compile SDK 34, Compose Stable Marker remains a valid alternative.
Boosting the stability
If you’re building an application consisting of several modules, there are two more ways to boost your Compose UI stabilities.
1. Stability configuration file: Starting with Compose Compiler 1.5.5, you can provide a configuration file at compile time to mark specific classes as stable. This makes it possible to treat classes you don’t control, such as standard library types like LocalDateTime
, as stable.
2. Strong skipping mode: When enabled, strong skipping makes all restartable composable functions skippable, even if they have unstable parameters. Non-restartable composable functions, however, remain unskippable. This is enabled by default in Kotlin 2.0.20.
By using this, you may not always need stability annotations. However, if you’re building libraries or SDKs, you can’t assume that your users will handle every stability issue correctly. It’s still better to mark your classes as stable and provide explicit stability guarantees to ensure better performance.
Conclusion
In this article, you explored why stability matters, the challenges of stability across different packages, and how to address these issues using the compose-runtime-annotation
and Compose Stable Marker libraries. With Strong Skipping mode and stability configuration files, it has become easier to make classes stable, but if you’re delivering libraries or SDKs, explicitly ensuring stability remains essential for achieving better performance.
You might also like
- Blog post
remember vs rememberSaveable: deep dive into state management and recomposition in Jetpack Compose
Understanding the differences between remember and rememberSaveable by exploring their internal mechanisms, and how they relate to state and recomposition.
- Blog post
Server-driven UI SDK on Android: how RevenueCat enables remote paywalls without app updates
Learning server-driven UI by exploring RevenueCat's Android SDK.
- Blog post
Turn Your App into Revenue: Building Paywalls in Android With Jetpack Compose
In this article, you'll learn how to seamlessly implement in-app subscriptions and paywall features in Android using Jetpack Compose and the RevenueCat SDK.