> For the complete documentation index, see [llms.txt](https://notes.tejpratapsingh.com/_/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://notes.tejpratapsingh.com/_/android-tips/interview/compose/compose-part-5.md).

# Compose Part 5

#### **Q41. How do you perform screenshot or visual regression testing for Compose UIs?**

Compose UI is purely declarative and renders to a canvas, making it ideal for screenshot testing because you can render composables without a full Activity or device window in many cases. The primary tool is **Paparazzi** from Cash App, which renders Compose directly to an off-screen `LayoutLib` (the same engine Android Studio Layout Preview uses) without an emulator. This makes tests extremely fast and deterministic. You define a `@Composable` unit inside a `Paparazzi` test, call `paparazzi.snapshot { ... }`, and it generates a PNG. You then compare against golden images in CI. For instrumented tests on real devices, **Shot** (from Karumi) wraps your existing Espresso/Compose tests to capture screenshots and compare them. Another option is the official **Compose Preview Screenshot Testing** (introduced in Android Studio Hedgehog/Compose UI 1.6+), which generates screenshots from `@Preview` annotations automatically. The key challenge is handling dynamic data (timestamps, random images) and platform differences (fonts, shadows); you should inject fake, static data and disable animations. For CI, you must run on a consistent OS version and screen density, or use Paparazzi to avoid emulator variance entirely.

```kotlin
// Paparazzi test: Fast, no emulator, deterministic pixel comparison.
class ProfileCardScreenshotTest {
    @get:Rule
    val paparazzi = Paparazzi(
        deviceConfig = DeviceConfig.PIXEL_5,
        theme = "android:Theme.Material3.Light.NoActionBar"
    )

    @Test
    fun `profile card default state`() {
        paparazzi.snapshot {
            AppTheme {
                ProfileCard(
                    user = User(
                        name = "Alex Johnson",
                        role = "Lead Developer",
                        avatarUrl = null // Use placeholder in UI
                    ),
                    onClick = {}
                )
            }
        }
    }

    @Test
    fun `profile card dark theme`() {
        paparazzi.snapshot {
            AppTheme(darkTheme = true) {
                ProfileCard(
                    user = User(name = "Alex Johnson", role = "Lead Developer", avatarUrl = null),
                    onClick = {}
                )
            }
        }
    }
}

// Compose Preview Screenshot Testing (Android Studio / Gradle plugin):
// 1. Apply plugin: id("com.android.compose.screenshot") version "0.0.1-alpha05"
// 2. Annotate previews:
@Preview(device = "id:pixel_5")
@Composable
fun ProfileCardPreview() {
    AppTheme { ProfileCard(User("Alex", "Dev", null), {}) }
}

// Gradle generates reference screenshots and fails on diff:
// ./gradlew updateDebugScreenshotTest
// ./gradlew validateDebugScreenshotTest
```

***

#### **Q42. Explain the difference between `animate*AsState`, `updateTransition`, and `AnimatedContent`.**

`animate*AsState` (e.g., `animateFloatAsState`, `animateColorAsState`) is the simplest animation API. It animates a single value from its current value to a target value whenever the target changes. It is best for independent properties like alpha, scale, or offset that do not need to be synchronized with other properties. `updateTransition` is used when you need to coordinate multiple properties simultaneously against a single state change (e.g., fading out *and* sliding *and* scaling when a button is pressed). It takes a target state (often an enum or sealed class) and lets you define child animations (`animateFloat`, `animateColor`, etc.) that all run in lockstep with the same transition spec and interruption handling. `AnimatedContent` is a container composable that animates between different content based on a target state key. It is not for animating properties of a single composable, but for cross-fading or sliding entire UI layouts in and out when the screen state changes (e.g., switching between a loading spinner and a success screen). `AnimatedContent` uses `AnimatedContentTransitionScope` to define enter and exit transitions for the incoming and outgoing content simultaneously.

```kotlin
// animate*AsState: Single independent property.
@Composable
fun LikeButton(isLiked: Boolean) {
    val scale by animateFloatAsState(
        targetValue = if (isLiked) 1.5f else 1f,
        animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
        label = "likeScale"
    )
    Icon(
        imageVector = if (isLiked) Icons.Filled.Favorite else Icons.Outlined.FavoriteBorder,
        contentDescription = "Like",
        modifier = Modifier.scale(scale)
    )
}

// updateTransition: Coordinated multi-property animation on a single target state.
enum class BoxState { Collapsed, Expanded }

@Composable
fun ExpandableBox(boxState: BoxState) {
    val transition = updateTransition(targetState = boxState, label = "boxTransition")

    val size by transition.animateDp(label = "size") { state ->
        if (state == BoxState.Expanded) 120.dp else 48.dp
    }
    val corner by transition.animateDp(label = "corner") { state ->
        if (state == BoxState.Expanded) 24.dp else 4.dp
    }
    val color by transition.animateColor(label = "color") { state ->
        if (state == BoxState.Expanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface
    }

    Box(
        modifier = Modifier
            .size(size)
            .clip(RoundedCornerShape(corner))
            .background(color)
    )
}

// AnimatedContent: Cross-fading between completely different layouts.
@Composable
fun LoadingResult(state: UiState) {
    AnimatedContent(
        targetState = state,
        transitionSpec = {
            fadeIn(animationSpec = tween(300)) togetherWith
            fadeOut(animationSpec = tween(300)) + slideOutVertically { it / 2 }
        },
        label = "contentSwitch"
    ) { targetState ->
        when (targetState) {
            is UiState.Loading -> CircularProgressIndicator()
            is UiState.Success -> Text("Data loaded: ${targetState.data}")
            is UiState.Error -> Text("Error", color = Color.Red)
        }
    }
}
```

***

#### **Q43. How do you implement a draggable list item with reordering in Compose?**

Reordering items in a list requires tracking the dragged item, detecting when it crosses over another item's position, and updating the underlying data list accordingly. The modern approach uses `Modifier.pointerInput` with `detectDragGesturesAfterLongPress` (or `detectVerticalDragGestures`) combined with `LazyListState` to calculate item offsets and detect drop targets. However, the easiest and most robust production solution is the **Reorderable** library (or Accompanist's deprecated `swipe` utilities), or implementing it via `Modifier.anchoredDraggable` with a custom `LazyList` item offset manipulation. The core algorithm is: on long-press, record the initial index and offset; as the drag progresses, calculate which visible item's bounds the pointer is currently over; when the pointer is released, move the item in the list and animate the layout change. You must use `LazyColumn` with `items(data, key = { it.id })` so that item state and composition identity are preserved during reordering. The `animateItemPlacement` modifier (from `LazyItemScope`) automatically animates items to their new positions after the list data changes.

```kotlin
@Composable
fun ReorderableLazyColumn(
    items: List<String>,
    onMove: (Int, Int) -> Unit,
    modifier: Modifier = Modifier
) {
    val state = rememberLazyListState()
    var draggedIndex by remember { mutableIntStateOf(-1) }
    var dragOffset by remember { mutableFloatStateOf(0f) }

    LazyColumn(state = state, modifier = modifier) {
        itemsIndexed(items, key = { _, item -> item }) { index, item ->
            val elevation by animateDpAsState(if (draggedIndex == index) 8.dp else 0.dp)

            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(horizontal = 16.dp, vertical = 8.dp)
                    .shadow(elevation, RoundedCornerShape(8.dp))
                    .background(MaterialTheme.colorScheme.surface)
                    .animateItemPlacement(tween(300))
                    .pointerInput(Unit) {
                        detectDragGesturesAfterLongPress(
                            onDragStart = { draggedIndex = index },
                            onDragEnd = {
                                draggedIndex = -1
                                dragOffset = 0f
                            },
                            onDragCancel = {
                                draggedIndex = -1
                                dragOffset = 0f
                            },
                            onDrag = { change, dragAmount ->
                                change.consume()
                                dragOffset += dragAmount.y

                                // Calculate target index based on item heights and offset.
                                val layoutInfo = state.layoutInfo
                                val visibleItems = layoutInfo.visibleItemsInfo
                                val draggedItem = visibleItems.find { it.index == draggedIndex }
                                val targetItem = visibleItems.find { itemInfo ->
                                    val center = itemInfo.offset + itemInfo.size / 2
                                    val draggedCenter = (draggedItem?.offset ?: 0) + dragOffset.toInt() + (draggedItem?.size ?: 0) / 2
                                    draggedCenter in (center - itemInfo.size / 2)..(center + itemInfo.size / 2)
                                }

                                if (targetItem != null && targetItem.index != draggedIndex) {
                                    onMove(draggedIndex, targetItem.index)
                                    draggedIndex = targetItem.index
                                    dragOffset = 0f
                                }
                            }
                        )
                    }
                    .offset { IntOffset(0, if (draggedIndex == index) dragOffset.toInt() else 0) }
            ) {
                Text(item, modifier = Modifier.padding(16.dp))
            }
        }
    }
}

// Usage
@Composable
fun ReorderScreen() {
    var items by remember { mutableStateOf(List(20) { "Item #$it" }) }
    ReorderableLazyColumn(
        items = items,
        onMove = { from, to ->
            items = items.toMutableList().apply { add(to, removeAt(from)) }
        }
    )
}
```

***

#### **Q44. What is the difference between `pointerInput` and `Modifier.draggable` / `Modifier.swipeable` / `Modifier.anchoredDraggable`?**

`pointerInput` is the lowest-level gesture API in Compose. It gives you access to `PointerInputScope` where you can use `awaitPointerEventScope` to read raw pointer events (down, move, up), calculate velocities, and implement completely custom gesture logic. It is the most flexible but requires the most boilerplate. `Modifier.draggable` (now largely superseded) provided a mid-level API for one-dimensional drag gestures (horizontal or vertical), exposing drag distance and state but not settling or anchoring logic. `Modifier.swipeable` was a Material component-level API that allowed dragging between predefined anchors (like a swipe-to-dismiss or bottom sheet) with physics-based settling. **It is now deprecated.** `Modifier.anchoredDraggable` (Compose Foundation 1.6+) is the official replacement for `swipeable`. It provides a robust system for dragging an element between discrete anchor points with velocity-based settling, thresholds, and snap animations. It is built on top of `pointerInput` but abstracts away the math for anchors, resistance, and decay animations. The hierarchy is: `pointerInput` (raw, any gesture) → `anchoredDraggable` (structured anchor-based drag) → high-level components like `BottomSheet` or `SwipeToDismissBox`.

```kotlin
// Low-level pointerInput: Full control, custom multi-touch or complex gestures.
@Composable
fun CustomGestureBox() {
    var offset by remember { mutableStateOf(Offset.Zero) }
    Box(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                awaitPointerEventScope {
                    while (true) {
                        val event = awaitPointerEvent()
                        // Access raw changes, historical data, pressure, etc.
                        val position = event.changes.first().position
                        offset = position
                    }
                }
            }
    ) {
        Text("Raw position: $offset")
    }
}

// anchoredDraggable: Structured drag with snap points (modern replacement for swipeable).
enum class DragAnchors { Start, Center, End }

@Composable
fun AnchoredDragBox() {
    val density = LocalDensity.current
    val state = remember {
        AnchoredDraggableState(
            initialValue = DragAnchors.Center,
            positionalThreshold = { distance: Float -> distance * 0.5f },
            velocityThreshold = { with(density) { 100.dp.toPx() } },
            snapAnimationSpec = tween(),
            decayAnimationSpec = rememberSplineBasedDecay()
        )
    }

    // Define anchors after measuring.
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { layoutSize ->
                val width = layoutSize.width.toFloat()
                state.updateAnchors(
                    DraggableAnchors {
                        DragAnchors.Start at 0f
                        DragAnchors.Center at width / 2f
                        DragAnchors.End at width
                    }
                )
            }
            .anchoredDraggable(state, Orientation.Horizontal)
            .offset { IntOffset(state.requireOffset().toInt(), 0) }
            .background(MaterialTheme.colorScheme.primary)
            .size(100.dp)
    )

    // Legacy swipeable (DEPRECATED - do not use in new code):
    // Modifier.swipeable(state, anchors, Orientation.Horizontal)
}
```

***

#### **Q45. How do you coordinate animations across multiple Composables using `Animatable` and coroutines?**

`Animatable` is a low-level animation controller that provides imperative, coroutine-based animation APIs. Unlike `animate*AsState`, which is declarative and driven by target value changes, `Animatable` exposes `animateTo`, `snapTo`, and `animateDecay` as suspend functions that you call inside a coroutine scope. This makes it perfect for coordinating multiple animations across different composables because you can launch multiple coroutines from a single event or use `coroutineScope { ... }` to run them concurrently and `await` their completion. For example, when a user taps a button, you can simultaneously animate a scale on an icon, a color on a background, and a translation on a card, with specific delays and easing for each, all synchronized by the structured concurrency of Kotlin coroutines. You typically store `Animatable` instances in `remember` at a shared ancestor or pass them down. To ensure the animations are cancellable and lifecycle-aware, use `LaunchedEffect` or `rememberCoroutineScope`. The key advantage is precise choreography: you can sequence animations with sequential `animateTo` calls, run them in parallel with `async`, and apply different `AnimationSpec`s per property.

```kotlin
@Composable
fun ChoreographedLikeButton(onLiked: () -> Unit) {
    val scope = rememberCoroutineScope()
    val scale = remember { Animatable(1f) }
    val rotation = remember { Animatable(0f) }
    val color = remember { Animatable(Color.Gray) }

    val targetColor = MaterialTheme.colorScheme.primary

    IconButton(
        onClick = {
            scope.launch {
                // Parallel launch: scale and color animate together.
                launch {
                    scale.animateTo(1.4f, spring(dampingRatio = Spring.DampingRatioLowBouncy))
                    scale.animateTo(1f, tween(100))
                }
                launch {
                    color.animateTo(targetColor, tween(200))
                }
                // Sequential: rotation only after first scale bump.
                rotation.animateTo(15f, tween(50))
                rotation.animateTo(-15f, tween(50))
                rotation.animateTo(0f, tween(50))

                onLiked()
            }
        }
    ) {
        Icon(
            imageVector = Icons.Default.Favorite,
            contentDescription = "Like",
            modifier = Modifier
                .graphicsLayer {
                    scaleX = scale.value
                    scaleY = scale.value
                    rotationZ = rotation.value
                },
            tint = color.value
        )
    }
}

// Coordinating across siblings via a shared controller at parent level.
class AnimationController {
    val offsetX = Animatable(0f)
    val offsetY = Animatable(0f)
    val alpha = Animatable(1f)

    suspend fun scatterAndFade() = coroutineScope {
        launch { offsetX.animateTo((-100..100).random().toFloat(), tween(500)) }
        launch { offsetY.animateTo((-100..100).random().toFloat(), tween(500)) }
        alpha.animateTo(0f, tween(800))
    }
}

@Composable
fun ScatterLayout() {
    val controller = remember { AnimationController() }
    val scope = rememberCoroutineScope()

    Button(onClick = { scope.launch { controller.scatterAndFade() } }) { Text("Scatter") }

    Box(Modifier.fillMaxSize()) {
        repeat(5) { index ->
            Box(
                modifier = Modifier
                    .offset { IntOffset(controller.offsetX.value.toInt() + index * 20, controller.offsetY.value.toInt()) }
                    .alpha(controller.alpha.value)
                    .size(40.dp)
                    .background(MaterialTheme.colorScheme.secondary)
            )
        }
    }
}
```

***

#### **Q46. How would you implement a shared element transition between two screens in Compose?**

Shared element transitions (now officially supported in Compose Animation 1.8.0+) allow a visual element (like an image or text) to morph and animate from its position/size in one screen to another as navigation occurs. The implementation requires three pieces: `SharedTransitionLayout` as the root, `AnimatedVisibilityScope` or `AnimatedContentScope` to provide the animation context, and `Modifier.sharedElement()` or `Modifier.sharedBounds()` on the matching elements in both the origin and destination composables. The `sharedElement` modifier requires a matching `SharedContentState` key in both screens. Because Compose Navigation replaces the entire screen content, both the outgoing and incoming composables must be in the composition simultaneously during the transition. In Compose Navigation, the `composable` builder provides an `AnimatedContentScope` (via `this@composable`), which you pass down to the screen composables. The `sharedElement` modifier uses this scope to interpolate bounds between the two layouts. You can customize the animation with `boundsTransform` (for the path of motion) and `placeHolderSize` to handle asynchronous image loading.

```kotlin
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun SharedElementNavHost() {
    val navController = rememberNavController()

    SharedTransitionLayout {
        NavHost(navController = navController, startDestination = "list") {
            composable(
                "list",
                exitTransition = { fadeOut(tween(300)) }
            ) {
                ListScreen(
                    animatedVisibilityScope = this@composable,
                    onItemClick = { id, bounds ->
                        navController.navigate("detail/$id")
                    }
                )
            }
            composable(
                "detail/{itemId}",
                enterTransition = {
                    fadeIn(tween(300)) + slideInVertically { it / 2 }
                },
                exitTransition = { fadeOut(tween(300)) }
            ) { backStack ->
                val itemId = backStack.arguments?.getString("itemId")!!
                DetailScreen(
                    itemId = itemId,
                    animatedVisibilityScope = this@composable
                )
            }
        }
    }
}

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun ListScreen(
    animatedVisibilityScope: AnimatedVisibilityScope,
    onItemClick: (String) -> Unit
) {
    LazyColumn {
        items(10) { index ->
            val key = "image_$index"
            Image(
                painter = painterResource(R.drawable.sample),
                contentDescription = null,
                modifier = Modifier
                    .size(80.dp)
                    .clip(RoundedCornerShape(8.dp))
                    .sharedElement(
                        state = rememberSharedContentState(key = key),
                        animatedVisibilityScope = animatedVisibilityScope,
                        boundsTransform = { _, _ -> tween(500) }
                    )
                    .clickable { onItemClick(index.toString()) }
            )
        }
    }
}

@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
fun DetailScreen(
    itemId: String,
    animatedVisibilityScope: AnimatedVisibilityScope
) {
    val key = "image_$itemId"
    Column(Modifier.fillMaxSize().padding(16.dp)) {
        Image(
            painter = painterResource(R.drawable.sample),
            contentDescription = null,
            modifier = Modifier
                .fillMaxWidth()
                .aspectRatio(1f)
                .clip(RoundedCornerShape(16.dp))
                .sharedElement(
                    state = rememberSharedContentState(key = key),
                    animatedVisibilityScope = animatedVisibilityScope,
                    boundsTransform = { _, _ -> tween(500) }
                )
        )
        Text("Detail for item $itemId", style = MaterialTheme.typography.headlineMedium)
    }
}
```

***

#### **Q47. How do you build a multi-theme design system in Compose that supports dynamic color, light/dark mode, and brand themes?**

A robust multi-theme system in Compose is built around `MaterialTheme` (or a fully custom theme) and the ability to swap `ColorScheme`, `Typography`, and `Shapes` at runtime. For **light/dark mode**, you define two `ColorScheme` instances and select between them using `isSystemInDarkTheme()`. For **dynamic color** (Material You), use `dynamicLightColorScheme(context)` and `dynamicDarkColorScheme(context)` from `androidx.compose.material3` on Android 12+ (API 31+), with a fallback to static brand palettes on older versions. For **brand themes** (e.g., switching between "Professional Blue" and "Playful Orange" within the same app), you create multiple pairs of color schemes and store the user's selection in a `StateFlow` or `DataStore`. The root composable reads this selection and wraps the entire app in the corresponding `MaterialTheme`. To ensure consistency, centralize all theme tokens in a single design system module. Avoid hardcoding colors in individual screens; always reference `MaterialTheme.colorScheme`. For edge cases where Material3 tokens are insufficient, create a custom `CompositionLocal` for extended tokens (e.g., `LocalExtendedColors`) but keep it scoped to your design system.

```kotlin
// Define brand palettes.
val BlueLight = lightColorScheme(primary = Color(0xFF1565C0), /* ... */)
val BlueDark = darkColorScheme(primary = Color(0xFF90CAF9), /* ... */)
val OrangeLight = lightColorScheme(primary = Color(0xFFE65100), /* ... */)
val OrangeDark = darkColorScheme(primary = Color(0xFFFFB74D), /* ... */)

enum class BrandTheme { Blue, Orange }

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    brand: BrandTheme = BrandTheme.Blue,
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val context = LocalContext.current
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        brand == BrandTheme.Orange -> if (darkTheme) OrangeDark else OrangeLight
        else -> if (darkTheme) BlueDark else BlueLight
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = AppTypography,
        shapes = AppShapes,
        content = content
    )
}

// Usage with ViewModel-driven brand switching.
class ThemeViewModel(private val dataStore: DataStore<<Preferences>) : ViewModel() {
    val brand: StateFlow<<BrandTheme> = dataStore.data
        .map { BrandTheme.valueOf(it[BRAND_KEY] ?: "Blue") }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), BrandTheme.Blue)
}

@Composable
fun MyApp(vm: ThemeViewModel = viewModel()) {
    val brand by vm.brand.collectAsStateWithLifecycle()
    val darkTheme = isSystemInDarkTheme()

    AppTheme(darkTheme = darkTheme, brand = brand) {
        NavHost(...) { ... }
    }
}
```

***

#### **Q48. What is the role of `CompositionLocalProvider` in theming, and how do you avoid overusing it?**

`CompositionLocalProvider` injects values into the Composition tree that descendant composables can read implicitly via `LocalX.current`. In theming, it is the mechanism behind `MaterialTheme`, `LocalContentColor`, `LocalTextStyle`, and `LocalDensity`. When you call `MaterialTheme(...)`, it internally wraps your content in `CompositionLocalProvider` calls for `LocalColorScheme`, `LocalTypography`, and `LocalShapes`. This is appropriate because theme tokens are true cross-cutting concerns: nearly every composable in the subtree needs access to colors and fonts, and threading them through every function parameter would be absurdly verbose. However, overusing `CompositionLocalProvider` for business logic, navigation controllers, or feature-specific state creates "spaghetti" dependencies. Composables that read a `CompositionLocal` are context-sensitive—they behave differently depending on where they are placed, making them harder to preview, test, and reason about. The rule is: use `CompositionLocal` for infrastructure and ambient context (theme, locale, window insets, scroll state) but never for screen-specific data, ViewModels, or event handlers. If a composable needs a callback or a piece of data to render, pass it as a parameter.

```kotlin
// APPROPRIATE: Extending the theme with custom tokens via CompositionLocal.
val LocalExtendedColors = staticCompositionLocalOf { ExtendedColors() }

data class ExtendedColors(val success: Color = Color.Green, val warning: Color = Color.Yellow)

@Composable
fun ExtendedTheme(content: @Composable () -> Unit) {
    val extended = ExtendedColors(
        success = if (isSystemInDarkTheme()) Color(0xFF81C784) else Color(0xFF4CAF50)
    )
    CompositionLocalProvider(LocalExtendedColors provides extended) {
        MaterialTheme(content = content)
    }
}

@Composable
fun StatusBadge(successful: Boolean) {
    val extended = LocalExtendedColors.current
    Badge(containerColor = if (successful) extended.success else extended.warning) {
        Text(if (successful) "OK" else "WARN")
    }
}

// INAPPROPRIATE: Using CompositionLocal for business logic.
// val LocalUserViewModel = compositionLocalOf<UserViewModel> { error("Missing") }
// BAD: Hidden dependency, untestable, unpredictable behavior based on tree position.
```

***

#### **Q49. How do you handle typography scaling and accessibility font sizes gracefully in Compose layouts?**

Compose uses `sp` (scalable pixels) for text, which automatically respects the user's system font scale setting. However, extreme font scales (up to 200% in accessibility settings) can break layouts that are designed with fixed heights or tight padding. The first rule is to **never use fixed heights** for text containers unless absolutely necessary. Use `Modifier.wrapContentHeight()` and allow text to occupy the space it needs. For lists, ensure rows use `IntrinsicSize.Min` or simply let the content determine the height. If a design requires a maximum of two lines, use `maxLines = 2` and `overflow = TextOverflow.Ellipsis` rather than clipping. For critical layouts that must not break (e.g., onboarding screens with illustrations), you can read the current font scale via `LocalDensity.current.fontScale` and apply adaptive layouts: switch from a horizontal to a vertical arrangement, or reduce non-essential elements if the scale exceeds a threshold. Always test with the largest font size enabled in system settings. Material3 components like `ListItem`, `Card`, and `Button` handle scaling reasonably well by default if you avoid overriding their intrinsic heights.

```kotlin
@Composable
fun AccessibleCard(title: String, description: String) {
    val fontScale = LocalDensity.current.fontScale

    Card(
        modifier = Modifier
            .fillMaxWidth()
            .wrapContentHeight(), // CRITICAL: Let the card grow with text.
        shape = RoundedCornerShape(12.dp)
    ) {
        Column(Modifier.padding(16.dp)) {
            Text(
                text = title,
                style = MaterialTheme.typography.titleLarge,
                maxLines = 2,
                overflow = TextOverflow.Ellipsis
            )
            Spacer(Modifier.height(8.dp))
            Text(
                text = description,
                style = MaterialTheme.typography.bodyMedium,
                maxLines = if (fontScale > 1.5f) 4 else 3, // Adapt max lines for extreme scaling.
                overflow = TextOverflow.Ellipsis
            )

            // Adaptive layout: if font scale is huge, stack buttons vertically.
            if (fontScale > 1.3f) {
                Column(Modifier.padding(top = 16.dp)) {
                    Button(onClick = {}, modifier = Modifier.fillMaxWidth()) { Text("Primary") }
                    OutlinedButton(onClick = {}, modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) { Text("Secondary") }
                }
            } else {
                Row(Modifier.padding(top = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
                    Button(onClick = {}) { Text("Primary") }
                    OutlinedButton(onClick = {}) { Text("Secondary") }
                }
            }
        }
    }
}
```

***

#### **Q50. How do you implement a custom ripple effect or remove the default ripple in Material3 Compose?**

In Material3, the default ripple (now called "ripple" rather than "indication" in older versions) is provided by `LocalIndication`. To change the ripple color, boundedness, or radius globally, you create a custom `IndicationNodeFactory` or use `rememberRipple` (legacy API) / `IndicationNodeFactory` (modern API) and provide it via `CompositionLocalProvider(LocalIndication provides ...)`. To **remove** the ripple entirely (common for custom buttons or game UIs), provide a no-op `Indication` instance. In Material3, `LocalRippleTheme` from Material2 is gone; the modern approach uses `Indication` directly. For a **custom ripple** (e.g., a square ripple, a color-matched ripple, or a delayed ripple), you implement `IndicationNodeFactory` to create a custom `Modifier.Node` that draws the ripple effect in the `DrawModifierNode` phase, tracking press state via `pointerInput`. Alternatively, for simple color changes, wrap the component in a `Surface` with a specific `contentColor` and rely on `LocalContentColor` to propagate to the default ripple. For per-component control, apply `Modifier.indication(interactionSource, indication)` directly.

```kotlin
// Remove ripple globally.
val NoIndication = object : Indication {
    override fun CreateModifier(interactionSource: InteractionSource): Modifier = Modifier
}

// Modern custom ripple using IndicationNodeFactory (Compose 1.6+ / Material3).
class CustomRippleNodeFactory(
    private val color: Color,
    private val radius: Dp = Dp.Unspecified
) : IndicationNodeFactory {
    override fun create(interactionSource: InteractionSource): Modifier.Node {
        return CustomRippleNode(interactionSource, color, radius)
    }
}

private class CustomRippleNode(
    private val interactionSource: InteractionSource,
    private val rippleColor: Color,
    private val rippleRadius: Dp
) : Modifier.Node(), DrawModifierNode, PointerInputModifierNode {

    private var isPressed by mutableStateOf(false)
    private var pressPosition by mutableStateOf(Offset.Zero)

    override fun onAttach() {
        coroutineScope.launch {
            interactionSource.interactions.collect { interaction ->
                when (interaction) {
                    is PressInteraction.Press -> {
                        isPressed = true
                        pressPosition = interaction.pressPosition
                    }
                    is PressInteraction.Release, is PressInteraction.Cancel -> isPressed = false
                }
            }
        }
    }

    override fun ContentDrawScope.draw() {
        drawContent()
        if (isPressed) {
            val radius = if (rippleRadius == Dp.Unspecified) size.maxDimension / 2 else rippleRadius.toPx()
            drawCircle(
                color = rippleColor.copy(alpha = 0.12f),
                radius = radius,
                center = pressPosition
            )
        }
    }

    override fun onPointerEvent(pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize) {
        // Handled by interactionSource from clickable/Indication.
    }

    override fun onCancelPointerInput() {}
}

// Usage: Global override.
@Composable
fun NoRippleTheme(content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalIndication provides NoIndication) {
        MaterialTheme(content = content)
    }
}

// Usage: Custom colored ripple on a specific button.
@Composable
fun CustomRippleButton(onClick: () -> Unit) {
    val interactionSource = remember { MutableInteractionSource() }
    val customIndication = remember { CustomRippleNodeFactory(color = Color.Red, radius = 48.dp) }

    Box(
        modifier = Modifier
            .size(120.dp, 48.dp)
            .background(MaterialTheme.colorScheme.primaryContainer)
            .clickable(
                interactionSource = interactionSource,
                indication = customIndication,
                onClick = onClick
            ),
        contentAlignment = Alignment.Center
    ) {
        Text("Tap Me")
    }
}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://notes.tejpratapsingh.com/_/android-tips/interview/compose/compose-part-5.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
