03 December 2025
Posted by Nick Butcher, Jetpack Compose Product ManagerToday, the Jetpack Compose December ‘25 release is stable. This contains version 1.10 of the core Compose modules and version 1.4 of Material 3 (see the full BOM mapping), adding new features and major performance improvements.
To use today’s release, upgrade your Compose BOM version to 2025.12.00:
implementation(platform("androidx.compose:compose-bom:2025.12.00")) |
We know that the runtime performance of your app is hugely important to you and your users, so performance has been a major priority for the Compose team. This release brings a number of improvements—and you get them all by just upgrading to the latest version. Our internal scroll benchmarks show that Compose now matches the performance you would see if using Views:
Scroll performance benchmark comparing Views and Jetpack Compose across different versions of Compose
Pausable composition in lazy prefetch is now enabled by default. This is a fundamental change to how the Compose runtime schedules work, designed to significantly reduce jank during heavy UI workloads.
Previously, once a composition started, it had to run to completion. If a composition was complex, this could block the main thread for longer than a single frame, causing the UI to freeze. With pausable composition, the runtime can now "pause" its work if it's running out of time and resume the work in the next frame. This is particularly effective when used with lazy layout prefetch to prepare frames ahead of time. The Lazy layout CacheWindow APIs introduced in Compose 1.9 are a great way to prefetch more content and benefit from pausable composition to produce much smoother UI performance.
Pausable composition combined with Lazy prefetch help reduce jank
We’ve also optimized performance elsewhere, with improvements to Modifier.onPlaced, Modifier.onVisibilityChanged, and other modifier implementations. We’ll continue to invest in improving the performance of Compose.
Compose offers a number of APIs to hold and manage state across different lifecycles; for example, remember persists state across compositions, and rememberSavable/rememberSerializable to persist across activity or process recreation. retain is a new API that sits between these APIs, enabling you to persist values across configuration changes without being serialized, but not across process death. As retain does not serialize your state, you can persist objects like lambda expressions, flows, and large objects like bitmaps, which cannot be easily serialized. For example, you may use retain to manage a media player (such as ExoPlayer) to ensure that media playback doesn’t get interrupted by a configuration change.
@Composable
fun MediaPlayer() {
val applicationContext = LocalContext.current.applicationContext
val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { ... }.build() }
...
}
We want to extend our thanks to the AndroidDev community (especially the Circuit team), who have influenced and contributed to the design of this feature.
Version 1.4.0 of the material3 library adds a number of new components and enhancements:
TextField now offers an experimental TextFieldState based version, which provides a more robust method for managing text's state. In addition, new SecureTextField and OutlinedSecureTextField variants are now offered. The material Text composable now supports autoSize behaviour.
The carousel component now offers a new HorizontalCenteredHeroCarousel variant.
TimePicker now supports switching between the picker and input modes.
A vertical drag handle helps users to change an adaptive pane’s size and/or position.
Horizontal centered hero carousel
Note that Material 3 Expressive APIs continue to be developed in the alpha releases of the material3 library. To learn more, see this recent talk:
We continue to expand on our animation APIs, including updates for customizing shared element animations.
By default, sharedElement() and sharedBounds() animations attempt to animate
layout changes whenever a matching key is found in the target state. However, you may want to disable this animation dynamically based on certain conditions, such as the direction of navigation or the current UI state.
To control whether the shared element transition occurs, you can now customize the
SharedContentConfig passed to rememberSharedContentState(). The isEnabled
property determines if the shared element is active.
SharedTransitionLayout {
val transition = updateTransition(currentState)
transition.AnimatedContent { targetState ->
// Create the configuration that depends on state changing.
fun animationConfig() : SharedTransitionScope.SharedContentConfig {
return object : SharedTransitionScope.SharedContentConfig {
override val SharedTransitionScope.SharedContentState.isEnabled: Boolean
get() =
// determine whether to perform a shared element transition
}
}
}
See the documentation for more.
A new modifier, Modifier.skipToLookaheadPosition(), has been added in this release, which keeps the final position of a composable when performing shared element animations. This allows for performing transitions like “reveal” type animation, as can be seen in the Androidify sample with the progressive reveal of the camera. See the video tip here for more information:
This release adds a new shared element transition API, prepareTransitionWithInitialVelocity, which lets you pass an initial velocity (e.g. from a gesture) to a shared element transition:
Modifier.fillMaxSize()
.draggable2D(
rememberDraggable2DState { offset += it },
onDragStopped = { velocity ->
// Set up the initial velocity for the upcoming shared element
// transition.
sharedContentStateForDraggableCat
?.prepareTransitionWithInitialVelocity(velocity)
showDetails = false
},
)
A shared element transition that starts with an initial velocity from a gesture
EnterTransition and ExitTransition define how an AnimatedVisibility/AnimatedContent composable appears or disappears. A new experimental veil option allows you to specify a color to veil or scrim content; e.g., fading in/out a semi-opaque black layer over content:
Veiled animated content – note the semi-opaque veil (or scrim) over the grid content during the animation
AnimatedContent(
targetState = page,
modifier = Modifier.fillMaxSize().weight(1f),
transitionSpec = {
if (targetState > initialState) {
(slideInHorizontally { it } togetherWith
slideOutHorizontally { -it / 2 } + veilOut(targetColor = veilColor))
} else {
slideInHorizontally { -it / 2 } +
unveilIn(initialColor = veilColor) togetherWith slideOutHorizontally { it }
}
},
) { targetPage ->
...
}
Compose 1.9 introduced Modifier.onVisibilityChanged and Modifier.onFirstVisible. After reviewing your feedback, it became apparent that the contract of Modifier.onFirstVisible was not possible to honor deterministically; specifically, when an item first becomes visible. For example, a Lazy layout may dispose of items that scroll out of the viewport, and then compose them again if they scroll back into view. In this circumstance, the onFirstVisible callback would fire again, as it is a newly composed item. Similar behavior would also occur when navigating back to a previously visited screen containing onFirstVisible. As such, we have decided to deprecate this modifier in the next Compose release (1.11) and recommend migrating to onVisibilityChanged. See the documentation for more information.
We plan to change coroutine dispatch in tests to improve test flakiness and catch more issues. Currently, tests use the UnconfinedTestDispatcher, which differs from production behavior; e.g., effects may run immediately rather than being enqueued. In a future release, we plan to introduce a new API that uses StandardTestDispatcher by default to match production behaviours. You can try the new behavior now in 1.10:
@get:Rule // also createAndroidComposeRule, createEmptyComposeRule
val rule = createComposeRule(effectContext = StandardTestDispatcher())
Using the StandardTestDispatcher will queue tasks, so you must use synchronization mechanisms like composeTestRule.waitForIdle() or composeTestRule.runOnIdle(). If your test uses runTest, you must ensure that runTest and your Compose rule share the same StandardTestDispatcher instance for synchronization.
// 1. Create a SINGLE dispatcher instance
val testDispatcher = StandardTestDispatcher()
// 2. Pass it to your Compose rule
@get:Rule
val composeRule = createComposeRule(effectContext = testDispatcher)
@Test
// 3. Pass the *SAME INSTANCE* to runTest
fun myTest() = runTest(testDispatcher) {
composeRule.setContent { /* ... */ }
}
Great APIs deserve great tools, and Android Studio has a number of recent additions for Compose developers:
Transform UI: Iterate on your designs by right clicking on the @Preview, selecting Transform UI, and then describing the change in natural language.
Generate @Preview: Right-click on a composable and select Gemini > Generate [Composable name] Preview.
Customize Material Symbols with new support for icon variations in the Vector Asset wizard.
Generate code from a screenshot or ask Gemini to match your existing UI to a target image. This can be combined with remote MCP support e.g. to connect to a Figma file and generate Compose UI from designs.
Fix UI quality issues audits your UI for common problems, such as accessibility issues, and then proposes fixes.
To see these tools in action, watch this recent demonstration:
We continue to invest in Jetpack Compose to provide you with the APIs and tools you need to create beautiful, rich UIs. We value your input, so please share your feedback on these changes or what you'd like to see next in our issue tracker.