# tv-material3 Compose API Migration Pitfalls

Common compile errors when migrating or building Android TV apps with `androidx.tv:tv-material:1.0.0-rc01` and `tv-foundation:1.0.0-alpha12`.

## 1. Surface: `color` → `colors`

`androidx.tv.material3.Surface` does NOT have a `color` parameter. It was removed in favor of `colors`.

**Clickable Surface (has `onClick`):**
```kotlin
// BROKEN
Surface(
    onClick = { ... },
    color = NexColors.SurfaceHigh,
) { ... }

// FIXED
Surface(
    onClick = { ... },
    scale = ClickableSurfaceDefaults.scale(focusedScale = 1.0f),
    border = ClickableSurfaceDefaults.border(
        focusedBorder = Border(BorderStroke(2.dp, NexColors.Accent)),
    ),
    colors = ClickableSurfaceDefaults.colors(
        containerColor = NexColors.SurfaceHigh,
        focusedContainerColor = NexColors.Surface,
    ),
) { ... }
```

**Decorative Surface (no `onClick`):**
```kotlin
// BROKEN  
Surface(color = NexColors.SurfaceTop) { ... }

// FIXED — wrap in Box with background instead
Box(
    modifier = Modifier.background(NexColors.SurfaceTop)
) { ... }
```

**PITFALL:** Do NOT use `ClickableSurfaceDefaults.colors()` on decorative Surfaces. The type is `ClickableSurfaceColors` which doesn't match `SurfaceColors`. Decorative Surfaces need `SurfaceDefaults.colors()` or a Box+background workaround.

## 2. ButtonDefaults: `buttonColors` → `colors`

```kotlin
// BROKEN
Button(onClick = {}) {
    Text("Click")
}.buttonColors(  // ← doesn't exist
    containerColor = NexColors.Accent,
    contentColor = Color.White,
)

// FIXED
Button(
    onClick = {},
    colors = ButtonDefaults.colors(  // ← as parameter to Button
        containerColor = NexColors.Accent,
        contentColor = Color.White,
    ),
) { Text("Click") }
```

Also: `ButtonDefaults.ButtonDefaults.colors()` is a double-qualification error. Just `ButtonDefaults.colors()`.

## 3. TextField/TextFieldDefaults: Missing from tv-material3

`androidx.tv.material3` does NOT include `TextField` or `TextFieldDefaults`. Use standard Compose Material3:

```kotlin
// WRONG — import doesn't resolve
import androidx.tv.material3.TextField
import androidx.tv.material3.TextFieldDefaults

// CORRECT
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
```

## 4. ClickableSurfaceDefaults.border(): `focused` → `focusedBorder`

```kotlin
// BROKEN
border = ClickableSurfaceDefaults.border(
    focused = Border(BorderStroke(2.dp, color)),  // 'focused' doesn't exist
)

// FIXED
border = ClickableSurfaceDefaults.border(
    focusedBorder = Border(BorderStroke(2.dp, color)),
)
```

The parameter is `focusedBorder`, not `focused`. Also applies to `ButtonDefaults.border()`.

## 5. Shape Type Mismatches

tv-material3 composables expect TV-specific shape types, not foundation shapes:

| Composable | Expects | Don't Use |
|---|---|---|
| `Card` (clickable) | `CardShape` | `RoundedCornerShape` |
| `Surface` (clickable) | `ClickableSurfaceShape` | `RoundedCornerShape` |

```kotlin
// BROKEN — RoundedCornerShape doesn't match CardShape
Card(onClick = {}, shape = RoundedCornerShape(8.dp)) { ... }

// FIXED — use CardDefaults for shape
Card(
    onClick = {},
    scale = CardDefaults.scale(focusedScale = 1.06f),
    border = CardDefaults.border(
        focusedBorder = Border(border = BorderStroke(2.dp, color), shape = cardShape),
    ),
    colors = CardDefaults.colors(...),
) { ... }
// Note: remove explicit `shape =` parameter from clickable Card
```

## 6. Modifier.isFocused Removed

The `isFocused` Modifier property was removed in recent Compose. Use a `mutableStateOf` + `onFocusChanged`:

```kotlin
// BROKEN
fun Modifier.nexAutoFocusIndicator(): Modifier = composed {
    val focused = this.isFocused  // ← doesn't exist on Modifier
    ...
}

// FIXED — accept isFocused as parameter
fun Modifier.nexAutoFocusIndicator(
    isFocused: Boolean,  // ← explicit parameter
): Modifier = composed {
    val animatedScale by animateFloatAsState(
        targetValue = if (isFocused) scale else 1.00f,
        ...
    )
    ...
}
```

The `import androidx.compose.ui.focus.isFocused` was also removed — remove that import line.

## 7. Missing Common Imports

Onboarding/setup files frequently miss these:

```kotlin
import androidx.compose.runtime.collectAsState   // for .collectAsState()
import androidx.compose.runtime.remember          // for remember { }
import androidx.compose.foundation.layout.Arrangement  // for Arrangement.spacedBy
import androidx.compose.foundation.layout.Column       // for Column { }
import androidx.compose.foundation.layout.height       // for .height()
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.Border
import androidx.compose.foundation.BorderStroke
```

## 8. `async` in Suspending Functions Needs `coroutineScope`

When using `async {}` inside a `suspend` function without a CoroutineScope receiver:

```kotlin
// BROKEN — async requires CoroutineScope
private suspend fun buildHomeContent(): HomeContentState {
    val deferred = async { fetchData() }  // COMPILE ERROR
    ...
}

// FIXED — wrap in coroutineScope
private suspend fun buildHomeContent(): HomeContentState = coroutineScope {
    val deferred = async { fetchData() }  // OK
    ...
    HomeContentState(...)  // last expression = return value (no 'return' keyword)
}
// Requires: import kotlinx.coroutines.coroutineScope
```

When using `= coroutineScope { }`, the **last expression** is the return value — do NOT use `return`.

## 9. Global sed/Python Replace Danger

**PITFALL:** Global search-and-replace across multiple files for API migrations is DANGEROUS. `sed -i 's/color =/colors = ClickableSurfaceDefaults.colors(containerColor =/' *.kt` will:

- Match inside `background(color = ...)` — background takes `color`, not `colors`
- Match inside `border(color = ...)` — border takes `color`, not `colors`
- Match decorative Surfaces that need different handling
- Break inline `if`/`else` conditions split across lines

**Prefer:** Read each file, identify the specific lines that need changes, apply targeted patches. One file at a time, build after each batch of fixes.

## 11. ImmutableBorder Removed Between Alpha and RC

`androidx.tv.material3.ImmutableBorder` was present in TV Material3 alpha releases but **removed** in `1.0.0-rc01`.

## 12. Canvas / DrawScope is NOT @Composable (Theme Color Hoisting)

**PITFALL:** `Canvas(onDraw = { ... })` and `drawBehind { ... }` lambdas run in `DrawScope`, which is **NOT a `@Composable` scope**. Theme color accessors like `NuvioTheme.colors.SurfaceHigh` or `MaterialTheme.colorScheme.primary` are composable read-only properties — they fail with `@Composable invocations can only happen from the context of a @Composable function` when called inside Canvas/DrawScope.

**BROKEN:**
```kotlin
Canvas(modifier = Modifier.fillMaxSize()) {
    drawCircle(color = NuvioTheme.colors.SurfaceHigh)  // COMPILE ERROR
    drawArc(color = NuvioTheme.colors.Accent)           // COMPILE ERROR
}
```

**FIXED — hoist colors to local variables at @Composable scope:**
```kotlin
val surfaceHighColor = NuvioTheme.colors.SurfaceHigh
val accentColor = NuvioTheme.colors.Accent

Canvas(modifier = Modifier.fillMaxSize()) {
    drawCircle(color = surfaceHighColor)  // OK
    drawArc(color = accentColor)          // OK
}
```

This applies to ALL DrawScope lambdas: `Canvas()`, `Modifier.drawBehind {}`, `Modifier.drawWithContent {}`, `Modifier.drawWithCache {}`.

## 13. Pair Destructuring Ambiguity with rememberTvFocusState()

**PITFALL:** `com.unspooled.app.ui.focus.rememberTvFocusState()` returns `Pair<MutableInteractionSource, State<Boolean>>`. Using Kotlin destructuring declaration `val (interactionSource, focused) = rememberTvFocusState()` can cause a `Function 'component2()' is ambiguous` compile error on some Kotlin compiler versions due to conflicting extension functions on `Pair`.

**BROKEN:**
```kotlin
val (interactionSource, focused) = rememberTvFocusState()
// Ambiguous component2(): Array<T>, List<T>, Pair<A,B>, Map.Entry<K,V>...
```

**FIXED — use explicit `.first` / `.second`:**
```kotlin
val focusState = rememberTvFocusState()
val interactionSource = focusState.first
val focused = focusState.second
// Then use focused.value (it's a State<Boolean>, not Boolean)
```

Also: always import `com.unspooled.app.ui.focus.rememberTvFocusState` — it is not auto-imported and silently missing imports cause `Unresolved reference` errors at callsites modified by subagents. `CardDefaults.border()` and `ClickableSurfaceDefaults.border()` now expect `Border?` directly instead of `ImmutableBorder`.

**BROKEN (alpha pattern, 23+ files affected):**
```kotlin
import androidx.tv.material3.ImmutableBorder

Card(
    border = CardDefaults.border(
        focused = ImmutableBorder(  // Unresolved reference 'ImmutableBorder'
            width = 2.dp,
            color = NexColors.Accent,
            shape = cardShape,
        ),
    ),
)
```

**FIXED (rc01 — use `Border` directly):**
```kotlin
import androidx.tv.material3.Border
import androidx.compose.foundation.BorderStroke

Card(
    border = CardDefaults.border(
        focusedBorder = Border(  // ← also note: 'focused' → 'focusedBorder'
            border = BorderStroke(2.dp, NexColors.Accent),
            shape = cardShape,
        ),
    ),
)
```

**Local replacement (if 23+ files need it):**
Create a local data class to avoid rewriting every call site:
```kotlin
// In ui/components/ImmutableBorder.kt
data class ImmutableBorder(
    val width: Dp,
    val color: Color,
    val shape: Shape,
)
```
But this only works if the `border()` function signature ALSO accepts the local type.
In practice, `CardDefaults.border()` expects `Border?` in rc01, so a wrapper
function around `CardDefaults.border()` is needed:
```kotlin
fun CardDefaults.border(
    focused: ImmutableBorder,
    unfocused: ImmutableBorder? = null,
): Border {
    return this.border(
        focusedBorder = Border(BorderStroke(focused.width, focused.color), focused.shape),
        unfocusedBorder = unfocused?.let {
            Border(BorderStroke(it.width, it.color), it.shape)
        },
    )
}
```

**Detection:** grep for `ImmutableBorder` — any hits mean the file was written for alpha and needs rc01 migration.
This affected 23 out of 119 files in UNSPOOLED (2026-05-28).

```kotlin
// BROKEN
LoadingMessage(message = "Searching...")

// FIXED
LoadingMessage(text = "Searching...")
```

The composable parameter is `text`, not `message`. Check function signatures when parameter names don't match.
