@file:OptIn( androidx.compose.ui.ExperimentalComposeUiApi::class, androidx.tv.material3.ExperimentalTvMaterial3Api::class, ) package com.unspooled.app.ui.screens.FEATURE_NAME import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusProperties import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.tv.material3.Text import com.unspooled.app.ui.focus.rememberFocusRegistry import com.unspooled.app.ui.focus.rememberTvFocusState import com.unspooled.app.ui.theme.NuvioTheme /** * V5-compliant screen template. * * Rules enforced: * - No moveFocus() — all navigation via focusProperties * - No var isFocused — all focus via rememberTvFocusState() * - Row containers are focusable() for vertical LazyColumn traversal * - focusGroup() on every LazyRow container */ @Composable fun FEATURE_NAMEScreen( viewModel: FEATURE_NAMEViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val registry = rememberFocusRegistry() LazyColumn( modifier = Modifier.fillMaxSize().background(NuvioTheme.colors.Background), ) { uiState.rows.forEachIndexed { rowIndex, row -> item(key = "row:$rowIndex") { Column( modifier = Modifier .fillMaxWidth() .focusGroup() // V5: row is a focus group .focusable() // V5: container is focusable for vertical nav .padding(vertical = 12.dp), ) { Text( text = row.title, color = NuvioTheme.colors.TextPrimary, modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), ) LazyRow( contentPadding = PaddingValues(horizontal = 24.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { itemsIndexed(row.items, key = { _, item -> item.id }) { index, item -> FEATURE_NAMECard( item = item, modifier = Modifier .focusRequester(registry.itemRequester("row:$rowIndex", index)), ) } } } } } } } @Composable private fun FEATURE_NAMECard( item: Item, modifier: Modifier = Modifier, ) { val (interactionSource, focused) = rememberTvFocusState() // V5: interactionSource.collectIsFocusedAsState() — never var isFocused Box( modifier = modifier .size(width = 140.dp, height = 210.dp) .focusable(interactionSource = interactionSource), ) { // Card content using focused.value } }