# Android TV Compose Build-Fix Cycle Reference

## Session Context (2026-05-17)
NEXSTREAM Android TV app — 119 Kotlin files built by 10 parallel subagents. The subagents all targeted `androidx.tv.material3:1.0.0-rc01` but used different API assumptions. This document records the exact fix patterns used.

## Layer 1: Resource Issues

### Duplicate color resources
```
ERROR: [color/accent] values/styles.xml [color/accent] values/colors.xml: Duplicate resources
```
**Fix:** Remove `<color>` definitions from `styles.xml` — they should only be in `colors.xml`.

### Missing / broken drawables
```
Failed to parse XML file '.../drawable/tv_banner.xml'
```
**Fix:** Remove `android:banner="@drawable/tv_banner"` from AndroidManifest.xml (or provide a proper PNG). XML shape drawables with `<size>` in dp may fail to parse.

### Missing mipmap launcher icon
```
AndroidManifest references @mipmap/ic_launcher but no launcher icon exists
```
**Fix:** Point `android:icon` to a simple drawable instead: `@drawable/ic_nexstream_launcher` (a colored `<shape>` XML). Or provide a proper mipmap PNG.

## Layer 2: Kotlin Syntax — Extra/Parent Parens

### Pattern: `)),` → `),`
Every `ClickableSurfaceDefaults.border()` call with `Border(BorderStroke(...))` needs exactly:
```kotlin
border = ClickableSurfaceDefaults.border(
    focusedBorder = Border(BorderStroke(2.dp, NexColors.Accent)),
),
```
The subagents frequently produced `)),` on one line with a `),` on the next:
```kotlin
// WRONG — extra closing paren:
border = ClickableSurfaceDefaults.border(
    focusedBorder = Border(BorderStroke(2.dp, NexColors.Accent))),
),
```
**Fix:** Remove the extra `)` before the comma on the `focusedBorder` line. The `Border(BorderStroke(...))` pattern needs only 2 closing parens: one for `BorderStroke(`, one for `Border(`. The `ClickableSurfaceDefaults.border()` gets closed by `),` on its own line.

### regex fix (use with care)
```python
# This works but can mangle files with multiple border sections.
# Prefer per-file manual fix.
import re
content = re.sub(
    r'BorderStroke\(([^)]+)\)\)\s*,\s*\n\s+\)\s*,',
    r'BorderStroke(\1)),',
    content
)
```

## Layer 3: API Name Mismatches

### `ImmutableBorder` → `Border`
```kotlin
// Subagents used:
ImmutableBorder(width = 2.dp, color = NexColors.Accent, shape = RoundedCornerShape(8.dp))
// Correct:
Border(BorderStroke(2.dp, NexColors.Accent))
```
The `ImmutableBorder` class does not exist in tv-material3 1.0.0-rc01. Replace with `Border(BorderStroke(...))`. Also update imports: `import androidx.tv.material3.ImmutableBorder` → `import androidx.tv.material3.Border`.

### `focused` → `focusedBorder`
```kotlin
// Subagents used:
ClickableSurfaceDefaults.border(focused = Border(...))
// Correct:
ClickableSurfaceDefaults.border(focusedBorder = Border(BorderStroke(...)))
```

### `shape = RoundedCornerShape` on Surface
```kotlin
// Subagents used:
Surface(shape = RoundedCornerShape(12.dp))
// Error: "Argument type mismatch: actual type is RoundedCornerShape, 
//         but ClickableSurfaceShape was expected"
// Fix: Remove shape parameter from Surface. The border handles visual shaping.
Surface()  // No shape parameter
```

### `RoundedCornerShape` in `ClickableSurfaceDefaults.border()`
The `Border` class from tv-material3 does not accept a `shape` parameter. Remove it:
```kotlin
// WRONG:
Border(BorderStroke(2.dp, NexColors.Accent), shape = RoundedCornerShape(8.dp))
// RIGHT:
Border(BorderStroke(2.dp, NexColors.Accent))
```

## Layer 4: Third-Party Library API Drift

### Coil3 ImageLoader
In `NexStreamApplication.kt`, subagents tried implementing `SingletonImageLoader.Factory` with:
- `OkHttpFetcher` — should be `OkHttpFetcherFactory`
- `DiskCache.Builder().directory(cacheDir.resolve(...))` — needs `okio.Path`, not `File`
- `crossfade(200)` — should work as `ImageLoader.Builder().crossfade(200)`

**Simplest fix:** Remove the factory implementation entirely. Use a Hilt `@Module @Provides` for the `ImageLoader` instead, or rely on Coil3's auto-configuration.

### `@Composable invocations can only happen from the context of a @Composable function`
This error appeared when `Text()` or other composables were placed directly inside `ClickableSurfaceDefaults.colors()` or `ClickableSurfaceDefaults.border()` lambda parameters. The `colors`/`border` lambdas are NOT composable scopes. Move the composable calls into the `Surface.content` lambda.

### `reified` without `inline`
```kotlin
// Subagents wrote:
suspend fun <reified T> getCached(...)
// Error: "Only type parameters of inline functions can be reified"
// Fix: Either add `inline` back, or remove `reified`
suspend fun <T> getCached(...)  // Without reified
```

### `combine()` with too many args
```kotlin
// Subagents wrote:
combine(flow1, flow2, ..., flowN) { args ->
    val (a, b, c, ..., z) = args
}
// Error: "Unexpected tokens"
// If the destructuring has a line ending with ) -> args instead of ) = args,
// it's a typo. Fix: change -> to =
val (a, b, ..., z) = args  // not -> args
```

### `Border()` closing paren missing before trailing lambda

```kotlin
// BROKEN — border( never closed, colors becomes param of border() not Button()
Button(
    onClick = { ... },
    border = ButtonDefaults.border(
        focusedBorder = Border(BorderStroke(2.dp, color)),
    colors = ButtonDefaults.colors(  // ← border( still open here
        ...
    ),
)

// FIXED — extra ), closes border(), then ) closes Button, then { starts lambda
Button(
    onClick = { ... },
    border = ButtonDefaults.border(
        focusedBorder = Border(BorderStroke(2.dp, color)),
    ),  // ← ), closes border()
    colors = ButtonDefaults.colors(
        ...
    ),
) {
    Text("label")
}
```
This pattern affected 20+ files in the first NEXSTREAM build. Subagents consistently omitted the `),` after `focusedBorder`.

### `dagger.Lazy` import missing (Hilt injection)

```kotlin
// Error: "Unresolved reference 'get'" on lines calling api.get()
@Inject constructor(
    private val api: Lazy<SomeApi>,  // kotlin.Lazy auto-resolved
)
// Fix: import dagger.Lazy
import dagger.Lazy
```
Affected files: `DebridAuthManager.kt`, `DebridService.kt`, `PremiumizeClient.kt`.

### DTO field mismatch — Premiumize uses `limitMax` not `trafficMax`

```kotlin
// BROKEN — PremiumizeAccountResponse has no trafficMax
trafficMax = response.trafficMax ?: Long.MAX_VALUE
// FIXED — Premiumize calls it limit_max → limitMax in DTO
trafficMax = response.limitMax ?: Long.MAX_VALUE
```
Other providers use `traffic_max` → `l`. Premiumize is the exception. Always check DTO.

### Massive combine destructuring (50+ flows) — indexed replacement

When a `combine()` has 50+ flows, `Array<T>` destructuring only supports `component1()-component5()`. Replace with indexed access:

```kotlin
combine(flow1, flow2, ..., flow65) { args ->
    val a = args[0] as? Boolean ?: false
    val b = args[1] as? String ?: ""
    val c = (args[10] as? Number)?.toInt() ?: 0
    val d = (args[23] as? List<*>)?.filterIsInstance<String>() ?: emptyList()
    val e = (args[26] as? Set<*>)?.filterIsInstance<String>()?.toSet() ?: emptySet()
    // ... for all N arguments
}
```

CRITICAL: Match each type to the target constructor. Common pitfalls:
- `ottEnabledSet` → `Set<String>`, NOT `String`
- `rowOrder` → `List<String>`, NOT `String`
- `debridPriorityOrder` → `List<String>`, NOT `String`
- `imageCacheSize` → `String`, NOT `Int`
- `nextEpisodeCountdown` → `Int`, NOT `String`

A single off-by-one in the index silently swaps same-type values. Verify order matches combine args exactly.

### DebridProvider enum missing `displayName`

```kotlin
// Error: "Unresolved reference 'displayName'"
DebridProviderState(
    displayName = provider.displayName(),  // No such function
)
// Fix: add displayName property to enum
enum class DebridProvider {
    REAL_DEBRID, ALL_DEBRID, PREMIUMIZE, TOR_BOX;  // ← semicolon required before properties!

    val displayName: String
        get() = when (this) {
            REAL_DEBRID -> "Real-Debrid"
            ALL_DEBRID -> "AllDebrid"
            PREMIUMIZE -> "Premiumize"
            TOR_BOX -> "TorBox"
        }
}
```
Note: Kotlin enums require `;` after the last entry before property/function definitions.

## Layer 5: Tv-Material3 UI Component API Drift (Session 2026-05-18)

### Surface — no `color` parameter
```kotlin
// WRONG — tv-material3 Surface has no 'color' param:
Surface(onClick = …, color = NexColors.SurfaceHigh)  // → "None of the following candidates is applicable"
// RIGHT — use colors:
Surface(onClick = …, colors = ClickableSurfaceDefaults.colors(containerColor = NexColors.SurfaceHigh))
// Also RIGHT — just remove color= and let Surface use default colors
```

### ButtonDefaults — `buttonColors` does not exist
```kotlin
// WRONG:
Button(colors = buttonColors(containerColor = …))
// RIGHT (tv-material3):
Button(colors = ButtonDefaults.colors(containerColor = …))
// Requires: import androidx.tv.material3.ButtonDefaults

// PITFALL: bulk replace can produce ButtonDefaults.ButtonDefaults.colors() (double prefix).
// Fix: replace 'ButtonDefaults.ButtonDefaults.colors(' → 'ButtonDefaults.colors('
```

### Duplicate Border imports
Files often contain two `import androidx.tv.material3.Border` lines (subagent template duplication).
This causes "Conflicting import: imported name 'Border' is ambiguous".
```python
# Fix: keep first occurrence, remove duplicates
seen = False
for line in lines:
    if line.strip() == 'import androidx.tv.material3.Border':
        if not seen: keep; seen = True
    else: keep
```
Affects: HeroBanner, MediaCard, StreamSourceCard, DetailsScreen, JustWatchSection, 
SeasonsSection, StreamSection, KeyboardGrid, SearchScreen, PlayerControls.

### TextField in tv-material3 context
```kotlin
// tv-material3 does NOT have TextField/TextFieldDefaults.
// WRONG:
import androidx.tv.material3.TextField  // compile error
// RIGHT — use compose.material3 directly:
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
```
Affects: DebridSetupCard, ProfileCreateScreen.

### Empty .background() after removing Surface color
When removing `color = NexColors.Xxx` from Surface calls, orphaned `.background()` blocks become empty:
```kotlin
// BROKEN (empty parens after color removal):
Column(modifier = modifier.background())
// FIXED:
Column(modifier = modifier.background(NexColors.Surface))
```

## Layer 6: Data Layer / Coroutine Patterns

### Flow wrapping for Room suspend DAOs
```kotlin
// WRONG — calling suspend DAO directly in .map{}:
fun getWatchlist(profileId: Int): Flow<List<MetaPreview>> {
    return watchlistDao.getByProfile(profileId).map { items -> … }
}
// ERROR: "Suspend function should be called only from a coroutine"
// RIGHT — wrap in flow {} builder:
fun getWatchlist(profileId: Int): Flow<List<MetaPreview>> = flow {
    val items = watchlistDao.getByProfile(profileId)
    emit(items.map { item -> … })
}
// Requires: import kotlinx.coroutines.flow.flow
```
Affects: TraktService.kt (getWatchlist, getContinueWatching), HomeViewModel (async/await).

### HomeViewModel coroutineScope wrapping
```kotlin
// WRONG — async{} inside suspend fun without coroutineScope:
private suspend fun buildHomeContent(): HomeContentState {
    val deferred = async { fetchCatalogSafe(…) }  // compile error
}
// RIGHT — wrap in coroutineScope:
private suspend fun buildHomeContent(): HomeContentState = coroutineScope {
    val deferred = async { fetchCatalogSafe(…) }
}
// Requires: import kotlinx.coroutines.coroutineScope
```

### Data layer API fixes (one-liners)
```kotlin
// MediaType deprecation:
// WRONG: MediaType.get("application/json")
// RIGHT: "application/json".toMediaType()
// NEEDS: import okhttp3.MediaType.Companion.toMediaType

// PremiumizeClient field: limitMax NOT trafficMax
// DebridAuthManager param: deviceCode NOT code
// CacheManager: remove crossinline from non-inline function
```

### Double .build() on MediaItem.Builder
```kotlin
// WRONG (stray .build() after template):
val mediaItem = MediaItem.Builder().setUri(url).build().build()
// RIGHT:
val mediaItem = MediaItem.Builder().setUri(url).build()
player.setMediaItem(mediaItem)
```

## Layer 7: Player-Specific API Removal

### PlayerScreen onKeyEvent (not available in this Compose version)
```kotlin
// WRONG: import androidx.compose.ui.focus.onKeyEvent
//        Modifier.onKeyEvent { keyEvent -> … }
// FIX: Track brace depth to remove entire .onKeyEvent{…} block + import.
```
```python
# Python: find start line containing '.onKeyEvent {', count braces to find matching '}',
# remove lines between them, and remove the import line.
start = find_line('.onKeyEvent { keyEvent ->')
depth = 1
for j in range(start+1, len(lines)):
    depth += lines[j].count('{') - lines[j].count('}')
    if depth == 0:
        end = j
        break
lines = lines[:start] + lines[end+1:]  # remove block
```

### Player NexStreamPlaybackService setSessionActivity
```kotlin
// setSessionActivity expects non-null PendingIntent, but getLaunchIntentForPackage
// returns Intent?. The safe fix: just don't set session activity.
mediaSession = MediaSession.Builder(this, player).build()
```

## Fix Workflow (Fastest Path — 2026-05-18 update)

1. **Start with a clean git checkout** — never fix files that a subagent already mangled.
   `git checkout -- . && git clean -fd` (approve the clean after reviewing untracked files)
2. **Fix in priority order by error category**: data-layer one-liners → import dedup → tv-material3 API → Flow wrapping → UI shape removal.
3. **After each batch, rebuild** — many errors cascade from a few root causes.
4. **Don't use regex bulk fixes** across 20+ files — every file has slightly different indentation and context. Fix each file individually in the IDE or with targeted Python scripts that read/write specific patterns.
5. **Open in Android Studio** for final polishing — it highlights remaining red squiggles instantly.
