What is new in the Jetpack Compose? Compose 1.9 is released!

What’s new in the Jetpack Compose August ’25 release

The Jetpack Compose August ’25 release is now stable, bringing version 1.9 of core Compose modules. This update introduces exciting features to make your Android apps more dynamic and performant.

How to use this release:

  • Upgrade to version 1.9 for new APIs.
  • Includes features like shadows, 2D scrolling, text styling, and better list performance.
  • Add this line to your Gradle file to get started:
implementation(platform("androidx.compose:compose-bom:2025.08.00"))

Shadows

Add modern shadow effects to your UI with two new modifiers:

  • dropShadow(): Draws a shadow behind your content.
  • innerShadow(): Draws a shadow inside the edges of a shape.
  • These differ from the existing `shadow()` modifier, which uses a lighting model.

Modifier.dropShadow()

This modifier places a shadow behind your UI element, like a card or button.

  • Customize radius, color, and spread.
  • Place `dropShadow()` before background modifiers to ensure the shadow appears behind the content.
@Composable
@Preview(showBackground = true)
fun SimpleDropShadowUsage() {
    // Track visibility state
    val pinkColor = Color(0xFFe91e63)
    val purpleColor = Color(0xFF9c27b0)
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .size(200.dp)
                .align(Alignment.Center)
                .dropShadow(
                    RoundedCornerShape(20.dp),
                    dropShadow = DropShadow(
                        15.dp,
                        color = pinkColor,
                        spread = 10.dp,
                        alpha = 0.5f
                    )
                )
                .background(
                    purpleColor,
                    shape = RoundedCornerShape(20.dp)
                )
        )
    }
}

Figure 1. Drop shadow drawn all around shape

Modifier.innerShadow()

This modifier adds a shadow inside the shape’s edges, creating a sunken effect.

  • Place `innerShadow()` after background or image modifiers, as it draws on top.
  • Customize radius, color, spread, and transparency.
@Composable
@Preview(showBackground = true)
fun SimpleInnerShadowUsage() {
    val pinkColor = Color(0xFFe91e63)
    val purpleColor = Color(0xFF9c27b0)
    Box(Modifier.fillMaxSize()) {
        Box(
            Modifier
                .size(200.dp)
                .align(Alignment.Center)
                .background(
                    purpleColor,
                    shape = RoundedCornerShape(20.dp)
                )
                .innerShadow(
                    RoundedCornerShape(20.dp),
                    innerShadow = InnerShadow(
                        15.dp,
                        color = Color.Black,
                        spread = 10.dp,
                        alpha = 0.5f
                    )
                )
        )
    }
}

Figure 2. Modifier.innerShadow() applied to a shape

For images, use a separate Box to layer the inner shadow on top:

  • Place the image in one Box.
  • Add a second Box with `innerShadow()` to overlay the shadow.
@Composable
@Preview(showBackground = true)
fun PhotoInnerShadowExample() {
    Box(Modifier.fillMaxSize()) {
        val shape = RoundedCornerShape(20.dp)
        Box(
            Modifier
                .size(200.dp)
                .align(Alignment.Center)
        ) {
            Image(
                painter = painterResource(id = R.drawable.cape_town),
                contentDescription = "Image with Inner Shadow",
                contentScale = ContentScale.Crop,
                modifier = Modifier.fillMaxSize()
                    .clip(shape)
            )
            Box(
                modifier = Modifier.fillMaxSize()
                    .innerShadow(
                        shape,
                        innerShadow = InnerShadow(15.dp,
                            spread = 15.dp)
                    )
            )
        }
    }
}

Figure 3. Inner shadow on top of an image

New Visibility Modifiers

Track when UI elements appear or disappear on the screen with new modifiers:

  • Built on the `onLayoutRectChanged` API from Compose UI 1.8.
  • Control actions based on how long or how much of an item is visible.

Use `onVisibilityChanged` to trigger actions like playing/pausing videos:

  • Triggers when an element’s visibility changes.
  • Set minimum visibility duration or fraction (e.g., fully visible for 500ms).
LazyColumn {
    items(feedData) { video ->
        VideoRow(
            video,
            Modifier.onVisibilityChanged(minDurationMs = 500, minFractionVisible = 1f) {
                visible ->
                // Track visibility state
                if (visible) video.play() else video.pause()
            },
        )
    }
}

Use `onFirstVisible` to log when an element first appears:

  • Ideal for tracking impressions (e.g., ads or content views).
  • Specify a minimum visibility duration (e.g., 500ms).
LazyColumn {
    items(100) {
        Box(
            Modifier
                // Log impressions when item has been visible for 500ms
                .onFirstVisible(minDurationMs = 500) { /* log impression */ }
                .clip(RoundedCornerShape(16.dp))
                .drawBehind { drawRect(backgroundColor) }
                .fillMaxWidth()
                .height(100.dp)
        )
    }
}

Rich Styling in OutputTransformation

Style text input in `BasicTextField` without changing its data:

  • Use `TextFieldBuffer.addStyle()` to apply colors or font weights.
  • Perfect for formatting phone numbers, credit cards, or other inputs.
  • Works only inside an `OutputTransformation`.
// Format a phone number and color the punctuation
val phoneTransformation = OutputTransformation {
    // 1234567890 -> (123) 456-7890
    if (length == 10) {
        insert(0, "(")
        insert(4, ") ")
        insert(9, "-")
        // Color the added punctuation
        val gray = Color(0xFF666666)
        addStyle(SpanStyle(color = gray), 0, 1)
        addStyle(SpanStyle(color = gray), 4, 5)
        addStyle(SpanStyle(color = gray), 9, 10)
    }
}
BasicTextField(
    state = myTextFieldState,
    outputTransformation = phoneTransformation
)

LazyLayout

Build custom lazy components with stable APIs:

  • `LazyLayoutMeasurePolicy`: Defines how items are measured and placed.
  • `LazyLayoutItemProvider`: Manages the list of items.
  • `LazyLayoutPrefetchState`: Handles prefetching for performance.

Prefetch Improvements

Make lists and grids scroll faster with new prefetch options:

  • `LazyLayoutCacheWindow` lets you prefetch items ahead and retain items behind.
  • Customize prefetching by viewport fraction or dp size.
  • Improves performance by composing items before they’re visible.

Configure prefetching with `LazyListState`:

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun LazyColumnCacheWindowDemo() {
    // Prefetch items 150.dp ahead and retain items 100.dp behind the visible viewport
    val dpCacheWindow = LazyLayoutCacheWindow(ahead = 150.dp, behind = 100.dp)
    // Alternatively, prefetch/retain items as a fraction of the list size
    // val fractionCacheWindow = LazyLayoutCacheWindow(aheadFraction = 1f, behindFraction = 0.5f)
    val state = rememberLazyListState(cacheWindow = dpCacheWindow)
    LazyColumn(state = state) {
        items(1000) { Text(text = "$it", fontSize = 80.sp) }
    }
}

Note: Prefetching may run `LaunchedEffects` or `DisposableEffects` earlier. Avoid using these for visibility tracking. Use `onFirstVisible` or `onVisibilityChanged` instead.

Scroll

2D Scroll APIs

Create layouts that scroll in both directions, like spreadsheets or image viewers:

  • `Scrollable2D` enables 2D scrolling and flinging.
  • Supports nested scrolling for complex layouts.
  • Unlike `Scrollable`, which is single-direction, `Scrollable2D` moves freely in 2D.
val offset = remember { mutableStateOf(Offset.Zero) }
Box(
    Modifier.size(150.dp)
        .scrollable2D(
            state =
                rememberScrollable2DState { delta ->
                    offset.value = offset.value + delta // update the state
                    delta // indicate that we consumed all the pixels available
                }
        )
        .background(Color.LightGray),
    contentAlignment = Alignment.Center,
) {
    Text(
        "X=${offset.value.x.roundToInt()} Y=${offset.value.y.roundToInt()}",
        style = TextStyle(fontSize = 32.sp),
    )
}

Scroll Interop Improvements

Improved integration with Android Views for scrolling:

  • Use `ViewTreeObserver` to listen to Compose scroll events.
  • Fixed incorrect fling velocities between Compose and Views.
  • Correct order for nested scroll callbacks in Views.
  • Proper nested scrolling with `NestedScrollView` inside `AndroidView`.

Improve Crash Analysis

Debug crashes more easily with detailed stack traces:

  • New opt-in API shows composable names and locations in crashes.
  • Helps identify and fix crash sources quickly.
  • Best for debug builds (not release, due to performance impact).

Enable it in your app’s entry point:

class App : Application() {
    override fun onCreate() {
        // Enable only for debug flavor to avoid perf regressions in release
        Composer.setDiagnosticStackTraceEnabled(BuildConfig.DEBUG)
    }
}

New Annotations and Lint Checks

New tools to write safer Compose code:

  • New Library: `runtime-annotation` for `@Stable`, `@Immutable`, and `@StableMarker` without needing Compose runtime.
  • @RememberInComposition: Marks functions/constructors that need `remember` in composition, with lint check errors.
  • @FrequentlyChangingValue: Flags functions/getters causing frequent recompositions, with lint check warnings.

Additional Updates

  • AGP/Lint Requirement: Use Android Gradle Plugin (AGP) 8.8.2 or higher for better lint support.
  • Context Menu APIs:
    • `Modifier.appendTextContextMenuComponents()`: Add custom items to context menus.
    • `Modifier.filterTextContextMenuComponents()`: Remove items from context menus.

Post a Comment

Previous Post Next Post