> 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-6.md).

# Compose Part 6

#### **Q51. What Compose compiler metrics do you monitor (e.g., `reportsDestination`), and how do you act on them?**

The Compose Compiler can generate two types of reports: **metrics** (aggregated CSV data about restartable/skippable functions and composable classes) and **reports** (detailed HTML/JSON per-module breakdowns). You enable them in your Gradle build file via `composeCompiler { reportsDestination = ...; metricsDestination = ... }`. The key metrics to monitor are the ratio of **restartable** versus **skippable** composables. A composable that is restartable but *not* skippable means it will recompose every time its parent recomposes, even if its parameters haven't changed. This is usually caused by unstable parameter types (e.g., `ArrayList`, `Function` interfaces without `@Composable`, or custom classes the compiler cannot prove immutable). You act on these reports by identifying the specific classes flagged as unstable and stabilizing them: converting `var` to `val`, using immutable collections (`List`, `Map`), or annotating them with `@Immutable` or `@Stable`. You also look for composables with high recomposition counts in production (via Layout Inspector or tracing) and cross-reference them with the compiler reports to confirm they are skippable. If a frequently-recomposing leaf composable is marked non-skippable due to an unstable parameter, that is your first optimization target.

```kotlin
// build.gradle.kts (module level)
android {
    composeCompiler {
        // Enable compiler reports and metrics.
        reportsDestination = layout.buildDirectory.dir("compose-reports")
        metricsDestination = layout.buildDirectory.dir("compose-metrics")
        // Optional: enable strong skipping for newer projects.
        enableStrongSkippingMode = true
    }
}

// Example output analysis:
// - app_release-classes.txt: Shows which classes are inferred as stable/unstable.
// - app_release-composables.csv: Lists every composable with 'restartable' and 'skippable' booleans.

// Acting on an unstable class found in the report:
// BEFORE (unstable - compiler report flags this as preventing skipping):
data class UserProfile(var name: String, var tags: ArrayList<String>)

// AFTER (stable - enables skipping for all composables that take this parameter):
@Immutable
data class UserProfile(val name: String, val tags: List<String>)

// A composable that now benefits from the fix:
@Composable
fun ProfileCard(profile: UserProfile) { // Now skippable!
    Column {
        Text(profile.name)
        Text(profile.tags.joinToString())
    }
}
```

***

#### **Q52. How does the Compose Compiler plugin differ from standard Kotlin compilation, and what version alignment issues have you encountered?**

The Compose Compiler is a Kotlin compiler plugin (IR transformer) that runs during the IR (Intermediate Representation) phase of compilation. It transforms every function annotated with `@Composable` by injecting a hidden `$composer: Composer` parameter, wrapping the function body in `composer.startRestartGroup()` / `endRestartGroup()` calls, and generating code to track state reads and parameter stability. Standard Kotlin compilation simply compiles functions as-is; the Compose plugin fundamentally rewrites the function signature and body so the runtime can manage recomposition scopes. This means the Compose Compiler version must strictly align with the Kotlin version (e.g., Compose Compiler 1.5.14 requires Kotlin 1.9.24). If versions mismatch, you get build failures or cryptic IR errors. Additionally, the Compose UI, Foundation, Material3, and Animation libraries must align with each other and with the compiler. In multi-module projects, a common issue is one module upgrading Material3 while another lags on Foundation, causing binary incompatibility crashes at runtime (e.g., `NoSuchMethodError` for `Modifier` node constructors). The solution is to use the Compose BOM (Bill of Materials) or a centralized version catalog (`libs.versions.toml`) and enforce it via CI dependency scanning.

```kotlin
// Version catalog (gradle/libs.versions.toml) to enforce alignment.
[versions]
kotlin = "1.9.24"
compose-compiler = "1.5.14"
compose-bom = "2024.06.00"

[libraries]
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }

// build.gradle.kts (app or feature module)
dependencies {
    implementation(platform(libs.compose.bom)) // Pins ALL Compose artifacts to BOM version.
    implementation(libs.compose.ui)
    implementation(libs.compose.material3)
    // Do NOT specify versions here; the BOM handles alignment.
}

android {
    composeOptions {
        kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
    }
}

// CI enforcement script (pseudo-code) to detect version drift:
// ./gradlew dependencies --configuration releaseCompileClasspath | grep "androidx.compose" | sort | uniq -c
// If any compose artifact appears with a different version than the BOM, fail the build.
```

***

#### **Q53. What is the impact of enabling/disabling `strongSkipping` mode in the Compose compiler?**

Strong skipping (enabled via `enableStrongSkippingMode = true` in the Compose Compiler options, available in Compose Compiler 1.5.10+) relaxes the skipping requirements for composables with unstable parameters. Normally, a composable can only skip recomposition if all its parameters are **stable** and unchanged according to `==`. With strong skipping enabled, a composable can skip even if it has unstable parameters, provided the parameter instances are **referentially equal** (`===`). This dramatically reduces unnecessary recompositions in real-world codebases where legacy models or third-party classes are not marked `@Immutable`. The risk is subtle: if an unstable object is mutated *in-place* (e.g., a `var` property on a class changes) but the reference remains the same, strong skipping will incorrectly skip recomposition, leading to stale UI. Therefore, it is safe only when your codebase follows the convention of never mutating objects in-place (always creating new instances for state changes). Enabling it can improve frame times and reduce recomposition counts significantly, but you must audit your state holders to ensure they are truly immutable in practice. If you see stale UI after enabling it, you likely have a class with mutable fields that is not being recreated on change.

```kotlin
// build.gradle.kts - Enable strong skipping.
android {
    composeCompiler {
        enableStrongSkippingMode = true
    }
}

// Impact demonstration:
class LegacyModel(var name: String) // Unstable: has mutable 'var'.

@Composable
fun DisplayModel(model: LegacyModel) {
    Text(model.name) // Without strong skipping: recomposes every parent recomposition.
}

// With strong skipping enabled:
// If you call DisplayModel(LegacyModel("A")) and then pass the SAME instance,
// it SKIPS even though LegacyModel is unstable.
// DANGER: If you mutate model.name = "B" and pass the same instance, UI is STALE.

// SAFE pattern with strong skipping: always create new instances.
class SafeModel(val name: String) // Immutable; strong skipping is redundant but safe.

@Composable
fun SafeScreen() {
    var model by remember { mutableStateOf(SafeModel("A")) }
    SafeDisplay(model)
    Button(onClick = { model = SafeModel("B") }) { Text("Update") } // New instance = safe.
}

@Composable
fun SafeDisplay(model: SafeModel) {
    Text(model.name) // Skips correctly with or without strong skipping.
}
```

***

#### **Q54. How do you manage Compose version alignment across a multi-module project with different teams?**

In a multi-module monorepo with multiple feature teams, version alignment is critical because Compose artifacts (UI, Foundation, Material3, Animation, Compiler) are tightly coupled. The standard practice is to centralize all Compose versions in a **shared version catalog** (`gradle/libs.versions.toml`) at the root project level. All modules reference Compose libraries through catalog aliases without hardcoding versions. The **Compose BOM** (Bill of Materials) is placed in the root `build.gradle.kts` script plugin or convention plugin so every module inherits it transitively. Teams are forbidden from declaring direct Compose dependencies with explicit versions; this is enforced by CI linting (e.g., a custom Gradle task or a Detekt/ArchUnit rule). For the Compose Compiler, the version is set in a shared `androidCompose` convention plugin applied to every Android module. When a global Compose upgrade is needed, a single PR updates the catalog, and CI runs full regression tests across all feature modules. If teams need to stage a migration (e.g., one team moves to Material3 while another stays on Material2 temporarily), you isolate the dependency to a dedicated module or use Gradle dependency resolution strategies to force a single version globally, preventing runtime crashes from mixed versions.

```kotlin
// Root gradle/libs.versions.toml
[versions]
compose-bom = "2024.06.00"
compose-compiler = "1.5.14"

[libraries]
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-foundation = { group = "androidx.compose.foundation", name = "foundation" }
compose-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }

// build-logic/convention-plugins/src/main/kotlin/AndroidComposeConventionPlugin.kt
class AndroidComposeConventionPlugin : Plugin<<Project> {
    override fun apply(target: Project) = target.run {
        pluginManager.apply("org.jetbrains.kotlin.plugin.compose")
        extensions.configure<<BaseExtension> {
            (this as? LibraryExtension ?: this as AppExtension)?.let { ext ->
                ext.buildFeatures.compose = true
                ext.composeOptions {
                    kotlinCompilerExtensionVersion = libs.findVersion("compose-compiler").get().requiredVersion
                }
            }
        }
        dependencies {
            add("implementation", platform(libs.findLibrary("compose-bom").get()))
            add("implementation", libs.findLibrary("compose-ui").get())
            add("implementation", libs.findLibrary("compose-material3").get())
            add("implementation", libs.findLibrary("compose-foundation").get())
        }
    }
}

// Feature module build.gradle.kts - NO versions, NO direct compose declarations.
plugins {
    id("convention.android-compose") // Applies everything centrally.
}
dependencies {
    // Team only adds feature-specific deps; Compose is inherited.
    implementation(project(":core:designsystem"))
}
```

***

#### **Q55. How do you handle `SurfaceView` or `TextureView` content (e.g., camera, video) within a Compose layout?**

`SurfaceView` and `TextureView` are special Android Views that render content on a separate hardware layer (Surface) for high-performance graphics like camera previews, video playback, or OpenGL. In Compose, you embed them via `AndroidView`. The primary challenge is **z-ordering**: `SurfaceView` has its own window that sits behind or above the app window, so it can draw over Compose UI unless you manage the `setZOrderMediaOverlay()` or `setZOrderOnTop()` settings. `TextureView` is more flexible as it renders into the standard View hierarchy and supports transformations, but it is less performant than `SurfaceView` for high-frame-rate content. The second challenge is **lifecycle**: camera and video surfaces must be initialized, started, and stopped with precise lifecycle events. Because `AndroidView` is inside a Composable, you use `DisposableEffect` or `LifecycleEventObserver` to forward `onResume`, `onPause`, and `onDestroy` to the underlying surface. You must also handle **aspect ratio and sizing**: the surface has a fixed buffer size that may not match the Compose layout size, so you typically use `onSizeChanged` or `Modifier.onGloballyPositioned` to reconfigure the surface dimensions. For camera previews, prefer `PreviewView` (from CameraX), which internally manages `SurfaceView`/`TextureView` selection and lifecycle, wrapped in `AndroidView`.

```kotlin
@Composable
fun CameraPreview(
    modifier: Modifier = Modifier,
    scaleType: PreviewView.ScaleType = PreviewView.ScaleType.FILL_CENTER
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraController = remember { LifecycleCameraController(context) }

    AndroidView(
        factory = {
            PreviewView(context).apply {
                this.scaleType = scaleType
                implementationMode = PreviewView.ImplementationMode.PERFORMANCE // Uses SurfaceView if possible.
            }
        },
        modifier = modifier,
        update = { previewView ->
            previewView.controller = cameraController
        },
        onRelease = {
            cameraController.unbind()
        }
    )

    // Bind camera to lifecycle and handle permissions/config.
    DisposableEffect(lifecycleOwner) {
        cameraController.bindToLifecycle(lifecycleOwner)
        onDispose {
            cameraController.unbind()
        }
    }
}

// Custom SurfaceView for OpenGL or video with explicit z-order.
@Composable
fun VideoSurface(videoUri: Uri, modifier: Modifier = Modifier) {
    val context = LocalContext.current
    val player = remember { MediaPlayer() }

    AndroidView(
        factory = {
            SurfaceView(context).apply {
                // Ensure SurfaceView is behind the app UI if you want Compose controls overlaid.
                setZOrderMediaOverlay(true)
                holder.addCallback(object : SurfaceHolder.Callback {
                    override fun surfaceCreated(holder: SurfaceHolder) {
                        player.setDisplay(holder)
                        player.setDataSource(context, videoUri)
                        player.prepareAsync()
                    }
                    override fun surfaceChanged(h: SurfaceHolder, f: Int, w: Int, hgt: Int) {}
                    override fun surfaceDestroyed(h: SurfaceHolder) {}
                })
            }
        },
        modifier = modifier,
        onRelease = {
            player.stop()
            player.release()
        }
    )
}
```

***

#### **Q56. What are the implications of using Compose in a `Service` or `Widget` context (e.g., Glance)?**

Jetpack Compose is a UI toolkit designed for in-app Activities and Fragments; it cannot be used directly inside an Android `Service` because Services do not have a window or view hierarchy. For **App Widgets** (Home screen widgets), the system renders `RemoteViews`, not Compose. You cannot embed `@Composable` functions directly into a `RemoteViews` factory. Instead, Google provides **Glance** (androidx.glance), a framework built on top of Compose-style declarative APIs that compiles down to `RemoteViews` under the hood. Glance provides its own set of composables (`GlanceModifier`, `Text`, `Image`, `Button`, `LazyColumn`) that are *not* the same as Compose UI; they are restricted by the capabilities of `RemoteViews` (no custom drawing, no touch gestures beyond basic clicks, no animations). The implication is that you must learn a separate but similar API (Glance) and accept its limitations: widgets are static, updates are driven by `WorkManager` or `AlarmManager` with rate limits, and interactivity is limited to `actionStartActivity`, `actionRunCallback`, or `actionSendBroadcast`. For foreground Services that need a UI (e.g., media playback notification), you use the standard `Notification` builder with `MediaStyle`, not Compose. For floating windows from a Service (using `WindowManager.addView`), you *can* use a `ComposeView`, but you must manage the `View` lifecycle, window token, and touch events manually.

```kotlin
// Glance AppWidget - NOT standard Compose UI.
class MyAppWidget : GlanceAppWidget() {
    override suspend fun provideGlance(context: Context, id: GlanceId) {
        provideContent {
            GlanceTheme {
                Column(
                    modifier = GlanceModifier.fillMaxSize().padding(16.dp),
                    verticalAlignment = Alignment.Vertical.Top
                ) {
                    Text(
                        text = "Next Meeting",
                        style = TextStyle(color = GlanceTheme.colors.primary, fontSize = 18.sp)
                    )
                    Spacer(GlanceModifier.height(8.dp))
                    // Limited interactivity: only predefined actions.
                    Button(
                        text = "Open App",
                        onClick = actionStartActivity<<MainActivity>()
                    )
                }
            }
        }
    }
}

// Standard Compose inside a floating window from a Service (advanced/edge case).
class FloatingComposeService : Service() {
    override fun onCreate() {
        super.onCreate()
        val windowManager = getSystemService(WINDOW_SERVICE) as WindowManager
        val params = WindowManager.LayoutParams(
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.WRAP_CONTENT,
            WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY,
            WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
            PixelFormat.TRANSLUCENT
        )

        val composeView = ComposeView(this).apply {
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnDetachedFromWindow)
            setContent {
                MaterialTheme {
                    FloatingActionButton(onClick = { /* ... */ }) {
                        Icon(Icons.Default.Add, null)
                    }
                }
            }
        }
        windowManager.addView(composeView, params)
    }

    override fun onDestroy() {
        super.onDestroy()
        // Must manually remove the view to avoid leaks.
    }
}
```

***

#### **Q57. How do you manage memory leaks in Compose, particularly with `remember` holding references to lifecycle-bound objects?**

The most common Compose memory leak occurs when `remember` or `rememberUpdatedState` captures a reference to a lifecycle-bound object such as an `Activity`, `Fragment`, `ViewModel`, `NavController`, or a `CoroutineScope` tied to a specific lifecycle. Because `remember` retains its value for the lifetime of the Composition (or until the key changes), the captured object stays reachable from the Compose slot table even after the underlying Android component is destroyed. For example, `val activity = LocalContext.current as Activity` followed by `remember { SomeListener { activity.finish() } }` creates a leak if the Composition outlives the Activity configuration change. The fix is to **never capture lifecycle-bound objects directly**. Instead, capture a lambda that resolves the action at invocation time, or use `rememberUpdatedState` to wrap a callback so the reference is updated on recomposition without restarting the effect. For `DisposableEffect`, always provide the correct keys and perform cleanup in `onDispose`. For `LaunchedEffect`, ensure it is scoped to the correct `CoroutineScope` (which is automatically cancelled when the composable leaves the Composition). If you need a `Context`, prefer `LocalContext.current.applicationContext` for long-lived operations. For `ViewModel`s, rely on `LocalViewModelStoreOwner` and `viewModel()` which manage lifecycle correctly.

```kotlin
// BAD: Leaks the Activity across configuration changes.
@Composable
fun LeakyBackButton() {
    val activity = LocalContext.current as Activity
    val onBack = remember { { activity.finish() } } // Activity leaked in slot table!
    Button(onClick = onBack) { Text("Exit") }
}

// GOOD: Use rememberUpdatedState to keep callback fresh without restarting.
@Composable
fun SafeBackButton() {
    val context = LocalContext.current
    val currentOnBack by rememberUpdatedState { (context as? Activity)?.finish() }

    // DisposableEffect with proper cleanup.
    DisposableEffect(Unit) {
        val backCallback = object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() { currentOnBack() }
        }
        // Register callback...
        onDispose {
            backCallback.remove() // CRITICAL: cleanup.
        }
    }
    Button(onClick = currentOnBack) { Text("Exit") }
}

// BAD: remember holding a ViewModel-scoped coroutine job that references a repository with a Context.
@Composable
fun LeakyDataLoader(vm: MyViewModel) {
    val data = remember { vm.loadData() } // If vm is retained but this composable recomposes with different keys,
    // the old reference may persist.
}

// GOOD: Let ViewModel manage its own scope; Compose only observes.
@Composable
fun SafeDataLoader(vm: MyViewModel) {
    val data by vm.dataFlow.collectAsStateWithLifecycle()
    // No direct reference to ViewModel internals in remember.
}

// BAD: Holding NavController in remember.
@Composable
fun LeakyNavigation() {
    val controller = rememberNavController()
    val callback = remember { { controller.navigate("route") } } // NavController leaked if recomposed with same key.
}

// GOOD: Pass navigation lambda via parameter or use rememberUpdatedState.
@Composable
fun SafeNavigation(onNavigate: () -> Unit) {
    val currentOnNavigate by rememberUpdatedState(onNavigate)
    Button(onClick = currentOnNavigate) { Text("Go") }
}
```

***

#### **Q58. How do you implement a responsive layout that adapts to foldable devices or large screens using Compose?**

Responsive design in Compose relies on **window size classes** (`WindowWidthSizeClass`, `WindowHeightSizeClass`) rather than raw screen pixels. You calculate these using `calculateWindowSizeClass(activity)` from the Material3 adaptive library. Based on the width class (`Compact`, `Medium`, `Expanded`), you switch between single-pane and dual-pane layouts. For foldable devices, you also need to detect **folding features** (hinges, folds) using the Jetpack WindowManager library (`WindowInfoTracker`). This tells you if the device is in `FLAT` posture (use full screen), `HALF_OPENED` (use tabletop or book posture), or if a `FoldingFeature` separates the screen into two display regions. Compose provides adaptive components like `NavigationSuiteScaffold` (which switches between bottom bar, rail, and drawer based on width) and `ListDetailPaneScaffold` (which implements a canonical list-detail flow with automatic two-pane layout on large screens). The key principle is to avoid hardcoding dimensions; instead, make layout decisions based on the current window size and fold state. You should also handle **drag-and-drop** and **keyboard/mouse** inputs gracefully on large screens.

```kotlin
@Composable
fun AdaptiveApp(activity: Activity) {
    val windowSizeClass = calculateWindowSizeClass(activity)
    val foldingFeature = rememberFoldingFeature(activity)

    val isExpanded = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded
    val isBookPosture = foldingFeature?.state == FoldingFeature.State.HALF_OPENED &&
                        foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL

    when {
        isBookPosture -> BookPostureLayout(foldingFeature = foldingFeature)
        isExpanded -> TwoPaneLayout()
        else -> SinglePaneLayout()
    }
}

@Composable
fun rememberFoldingFeature(activity: Activity): FoldingFeature? {
    val context = LocalContext.current
    var feature by remember { mutableStateOf<<FoldingFeature?>(null) }

    DisposableEffect(context) {
        val tracker = WindowInfoTracker.getOrCreate(context)
        val job = tracker.windowLayoutInfo(activity)
            .mapNotNull { it.displayFeatures.filterIsInstance<<FoldingFeature>().firstOrNull() }
            .distinctUntilChanged()
            .onEach { feature = it }
            .launchIn(CoroutineScope(Dispatchers.Main))
        onDispose { job.cancel() }
    }
    return feature
}

// ListDetailPaneScaffold for canonical list-detail flow (Material3 adaptive).
@Composable
fun TwoPaneListDetail(items: List<Item>, selectedId: String?, onSelected: (String) -> Unit) {
    ListDetailPaneScaffold(
        directive = Navigator.scaffoldDirective,
        listPane = {
            AnimatedPane {
                ItemListPane(items = items, selectedId = selectedId, onSelected = onSelected)
            }
        },
        detailPane = {
            AnimatedPane {
                // Only show if selected.
                selectedId?.let { ItemDetailPane(itemId = it) }
            }
        }
    )
}
```

***

#### **Q59. What is the `Recomposer` and when would you need to interact with it directly?**

The `Recomposer` is the core orchestrator of the Compose runtime. It manages the scheduling and execution of compositions, applying snapshot state changes to the UI tree. Every `Composition` (created by `setContent` or `rememberComposition`) is associated with a `Recomposer`. Normally, the framework creates and manages a `Recomposer` automatically for you, tied to a `CoroutineScope` (typically the UI thread's `Dispatchers.Main`). You almost never need to interact with it directly in standard app development. However, there are advanced scenarios where direct interaction is necessary: (1) **Custom composition contexts**: if you are embedding Compose in a non-standard host (e.g., a custom renderer, off-screen rendering for screenshots, or a game engine), you may create a `Recomposer` manually and drive it with a custom coroutine dispatcher. (2) **Testing**: in very low-level Compose runtime tests, you may need to control when recomposition happens via `Recomposer.runRecomposeAndApplyChanges()`. (3) **Pausing/resuming composition**: if you need to freeze a portion of the UI tree (e.g., for a screenshot or background processing), you can manipulate the `Recomposer`'s clock or scope. (4) **Multi-window or multi-surface**: when rendering Compose to multiple `ComposeView`s with different frame rates or lifecycles, you might manage separate `Recomposer` instances. For 99.9% of developers, `Recomposer` is an internal implementation detail.

```kotlin
// Advanced: Manual Recomposer for an off-screen composition (e.g., server-side rendering or testing).
fun createOffscreenComposition(): Composition {
    val coroutineContext = Dispatchers.Default + SupervisorJob()
    val recomposer = Recomposer(coroutineContext)

    // A custom Applier that writes to a data structure instead of Android Views.
    val root = NodeApplier.RootNode()
    val composition = ControlledComposition(NodeApplier(root), recomposer)

    // Launch the recomposer loop.
    CoroutineScope(coroutineContext).launch {
        recomposer.runRecomposeAndApplyChanges()
    }

    composition.setContent {
        Text("Rendered off-screen")
        Box(Modifier.size(100.dp).background(Color.Red))
    }

    // Force an immediate recomposition (not frame-bound).
    runBlocking(coroutineContext) {
        recomposer.awaitIdle()
    }

    return composition
}

// Standard usage (never touch Recomposer directly):
@Composable
fun NormalApp() {
    // The framework creates Recomposer behind the scenes.
    MaterialTheme {
        NavHost(...) { ... }
    }
}

// Testing: Using composeTestRule.mainClock to control time instead of Recomposer directly.
class AnimationTest {
    @get:Rule val rule = createComposeRule()

    @Test
    fun animatedContent() {
        rule.setContent { AnimatedVisibility(visible = true) { Text("Hello") } }
        rule.mainClock.advanceTimeBy(300) // Manually drives the Recomposer's clock.
        rule.onNodeWithText("Hello").assertExists()
    }
}
```

***

#### **Q60. How do you handle text input fields with complex formatting (e.g., credit cards, phone numbers) in Compose?**

Compose provides `VisualTransformation` as the idiomatic way to format text inside `BasicTextField` or `OutlinedTextField` without mutating the underlying state. A `VisualTransformation` has two methods: `filter(text: AnnotatedString): TransformedText` which maps the raw text to the displayed text, and an `OffsetMapping` which translates cursor positions between the raw and displayed text spaces. For a credit card, the raw state stores only digits (`"4532015112830366"`), while the visual text shows groups of four separated by spaces (`"4532 0151 1283 0366"`). The `OffsetMapping` must handle both directions: `originalToTransformed` moves the cursor from raw index to visual index (adding spaces), and `transformedToOriginal` moves from visual index back to raw index (subtracting spaces). For phone numbers, the mapping is more complex because formatting varies by region and digit count (e.g., `(555) 123-4567`). You typically implement a state machine or use a library like `libphonenumber` to determine the formatting mask, then build the `OffsetMapping` dynamically. The key constraint is that the mapping must be monotonic and handle out-of-bounds indices gracefully, as the cursor may land on a formatting character (space, dash, parenthesis) where no raw character exists; in such cases, you typically snap to the nearest valid raw index.

```kotlin
// Credit Card Visual Transformation
class CreditCardVisualTransformation : VisualTransformation {
    override fun filter(text: AnnotatedString): TransformedText {
        val trimmed = text.text.take(16)
        val formatted = trimmed.chunked(4).joinToString(" ")

        val mapping = object : OffsetMapping {
            override fun originalToTransformed(offset: Int): Int {
                if (offset <= 0) return 0
                // Every 4 digits adds 1 space, except after the last group.
                val spaces = (offset - 1) / 4
                return (offset + spaces).coerceAtMost(formatted.length)
            }

            override fun transformedToOriginal(offset: Int): Int {
                if (offset <= 0) return 0
                // Remove spaces: spaces are at indices 4, 9, 14.
                val spacesBefore = when {
                    offset <= 4 -> 0
                    offset <= 9 -> 1
                    offset <= 14 -> 2
                    else -> 3
                }
                return (offset - spacesBefore).coerceAtMost(trimmed.length)
            }
        }

        return TransformedText(AnnotatedString(formatted), mapping)
    }
}

// Phone Number Visual Transformation (US format).
class PhoneVisualTransformation : VisualTransformation {
    override fun filter(text: AnnotatedString): TransformedText {
        val digits = text.text.filter { it.isDigit() }.take(10)
        val formatted = when {
            digits.length < 4 -> digits
            digits.length < 7 -> "(${digits.take(3)}) ${digits.substring(3)}"
            else -> "(${digits.take(3)}) ${digits.substring(3, 6)}-${digits.substring(6)}"
        }

        val mapping = object : OffsetMapping {
            override fun originalToTransformed(offset: Int): Int {
                if (offset <= 0) return 0
                val extra = when {
                    offset <= 3 -> 1 // '('
                    offset <= 6 -> 3 // '(', ')', ' '
                    else -> 4 // '(', ')', ' ', '-'
                }
                return (offset + extra).coerceAtMost(formatted.length)
            }

            override fun transformedToOriginal(offset: Int): Int {
                if (offset <= 1) return 0
                return when {
                    offset <= 5 -> offset - 1 // inside area code
                    offset <= 9 -> offset - 3 // after ') '
                    else -> offset - 4 // full format
                }.coerceAtMost(digits.length)
            }
        }

        return TransformedText(AnnotatedString(formatted), mapping)
    }
}

// Usage in a TextField.
@Composable
fun PaymentScreen() {
    var cardNumber by remember { mutableStateOf("") }
    var phone by remember { mutableStateOf("") }

    Column(Modifier.padding(16.dp)) {
        OutlinedTextField(
            value = cardNumber,
            onValueChange = { if (it.length <= 16) cardNumber = it.filter { c -> c.isDigit() } },
            label = { Text("Card Number") },
            visualTransformation = CreditCardVisualTransformation(),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
        )

        OutlinedTextField(
            value = phone,
            onValueChange = { if (it.length <= 10) phone = it.filter { c -> c.isDigit() } },
            label = { Text("Phone Number") },
            visualTransformation = PhoneVisualTransformation(),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone)
        )
    }
}
```


---

# 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-6.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.
