Agent skill
android-tv-development
Android TV-specific patterns for D-pad navigation, focus management, and TV UI
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/android-tv-development
SKILL.md
Android TV Development Skill
When to Use
Apply when building TV screens, implementing D-pad navigation, or handling focus management.
Focus Management Fundamentals
FocusRequester Pattern
// Create requesters for key navigation targets
val heroPlayFocusRequester = remember { FocusRequester() }
val topNavFocusRequester = remember { FocusRequester() }
val firstRowFocusRequester = remember { FocusRequester() }
// Request focus after composition settles
LaunchedEffect(Unit) {
delay(100)
heroPlayFocusRequester.requestFocus()
}
Focus Properties for D-pad Navigation
// Define explicit navigation paths
Modifier
.focusRequester(buttonFocusRequester)
.focusProperties {
up = topNavFocusRequester // D-pad UP goes here
down = firstRowFocusRequester // D-pad DOWN goes here
left = FocusRequester.Cancel // Prevent left navigation
}
Key Event Handling
Modifier.onPreviewKeyEvent { keyEvent ->
if (keyEvent.type == KeyEventType.KeyDown && keyEvent.key == Key.DirectionUp) {
if (atTopOfList) {
coroutineScope.launch {
listState.scrollToItem(0)
heroFocusRequester.requestFocus()
}
true // Consumed
} else false
} else false
}
Home Screen Pattern
Layer Structure
Box(modifier = Modifier.fillMaxSize()) {
// Layer 1: Dynamic background (blurred backdrop)
DynamicBackground(imageUrl = previewItem?.backdropUrl)
// Layer 2: Content (Hero + Rows)
Column {
HeroSection(
items = featuredItems,
onPlayClick = onPlayClick,
playButtonFocusRequester = heroPlayFocusRequester
)
LazyColumn {
items(rows) { row ->
ItemRow(
items = row.items,
upFocusRequester = heroPlayFocusRequester
)
}
}
}
// Layer 3: Top Navigation
TopNavigationBar(
downFocusRequester = heroPlayFocusRequester,
modifier = Modifier.align(Alignment.TopCenter)
)
}
Hero Section (30% screen height)
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.30f)
) {
// Backdrop image
AsyncImage(model = backdropUrl, ...)
// Gradient overlay
Box(modifier = Modifier
.fillMaxSize()
.background(Brush.verticalGradient(
colors = listOf(Color.Transparent, TvColors.Background)
))
)
// Content (title, buttons)
Column {
Text(item.name, style = MaterialTheme.typography.displayMedium)
Row {
Button(
onClick = onPlayClick,
modifier = Modifier.focusRequester(playFocusRequester)
) { Text("Play") }
}
}
}
Item Row Pattern
@Composable
private fun ItemRow(
title: String,
items: List<JellyfinItem>,
upFocusRequester: FocusRequester? = null,
modifier: Modifier = Modifier
) {
val firstCardFocus = remember { FocusRequester() }
Column(
modifier = modifier.focusGroup() // Row as single focus unit
) {
// Header with accent bar
Row {
Box(modifier = Modifier
.width(4.dp)
.height(24.dp)
.background(accentColor))
Text(title, style = MaterialTheme.typography.titleLarge)
}
// Cards
LazyRow(
modifier = Modifier.focusRestorer(firstCardFocus),
horizontalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(horizontal = 10.dp)
) {
itemsIndexed(items, key = { _, item -> item.id }) { index, item ->
MediaCard(
item = item,
modifier = Modifier
.then(if (index == 0) Modifier.focusRequester(firstCardFocus) else Modifier)
.then(upFocusRequester?.let {
Modifier.focusProperties { up = it }
} ?: Modifier)
)
}
}
}
}
Navigation Rail Pattern
@Composable
fun TopNavigationBar(
selectedItem: NavItem,
onItemSelected: (NavItem) -> Unit,
downFocusRequester: FocusRequester,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.background(Brush.verticalGradient(
colors = listOf(TvColors.Background, Color.Transparent)
))
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(24.dp)
) {
NavItem.entries.forEach { item ->
NavigationButton(
item = item,
isSelected = item == selectedItem,
onClick = { onItemSelected(item) },
modifier = Modifier.focusProperties { down = downFocusRequester }
)
}
}
}
Long-Press Dialog Pattern
// State
var dialogParams by remember { mutableStateOf<DialogParams?>(null) }
// On long click
onLongClick = { item ->
dialogParams = DialogParams(
title = item.name,
items = buildHomeDialogItems(item, jellyfinClient)
)
}
// Dialog display
dialogParams?.let { params ->
DialogPopup(
params = params,
onDismiss = { dialogParams = null },
onItemSelected = { action -> handleAction(action) }
)
}
TV-Specific Tips
- Focus Indicators: Use
focusedContainerColorandfocusedBorderfor clear visual feedback - Scale on Focus: Keep
focusedScale = 1fto avoid layout shifts - Scroll Position: Save/restore scroll positions during navigation
- Loading States: Show TvLoadingIndicator during data fetches
- Error Handling: Display errors in dialogs, not inline
- Background Polling: Use
POLL_INTERVAL_MS = 30_000Lfor home screen refresh
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?