Presentation Layer
April 4, 2026 · View on GitHub
The presentation layer handles UI rendering, user interaction, and screen-level state management. It uses Jetpack Compose for TV with MVVM architecture — ViewModels expose StateFlow<UiState> that Compose screens collect and render.
Package: com.cadnative.firevisioniptv.presentation
Package Structure
presentation/
├── mapper/ # Domain → UI model mappers
│ ├── CategoryUiMapper.kt
│ └── ChannelUiMapper.kt
├── model/ # UI state and model classes
│ ├── CategoryUiModel.kt
│ ├── ChannelUiModel.kt
│ ├── ChannelsUiState.kt
│ ├── FavoritesUiState.kt
│ ├── PlayerUiState.kt
│ ├── SearchUiState.kt
│ └── SettingsUiState.kt
├── navigation/
│ ├── FireVisionNavGraph.kt # Navigation graph setup
│ └── Screen.kt # Route definitions (sealed class)
├── ui/
│ ├── components/ # Reusable Compose components
│ │ ├── ChannelCard.kt
│ │ ├── ChannelCardSkeleton.kt
│ │ ├── EmptyState.kt
│ │ ├── ErrorState.kt
│ │ ├── LoadingIndicator.kt
│ │ ├── PlayerControls.kt
│ │ └── SideNavRail.kt
│ ├── player/
│ │ ├── ErrorRecoveryManager.kt
│ │ └── PlaybackManager.kt
│ ├── screens/ # Screen composables
│ │ ├── CategoriesScreen.kt
│ │ ├── ChannelsScreen.kt
│ │ ├── FavoritesScreen.kt
│ │ ├── HomeScreen.kt
│ │ ├── LegacySettingsScreen.kt
│ │ ├── PairingScreen.kt
│ │ ├── PlayerScreen.kt
│ │ ├── SearchScreen.kt
│ │ └── SettingsScreen.kt
│ ├── theme/
│ │ ├── Color.kt
│ │ ├── Theme.kt
│ │ └── Type.kt
│ └── utils/
│ ├── AnimationUtils.kt
│ └── FocusUtils.kt
└── viewmodel/
├── ChannelsViewModel.kt
├── FavoritesViewModel.kt
├── PairingViewModel.kt
├── PlayerViewModel.kt
├── SearchViewModel.kt
└── SettingsViewModel.kt
Navigation
flowchart TD
Splash --> Pairing
Splash --> Home
subgraph SideNavRail
Home
Channels
Categories
Search
Favorites
Settings
end
Home --> Player
Channels --> Player
Channels --> ChannelsByCategory
ChannelsByCategory --> Player
Search --> Player
Favorites --> Player
Categories --> ChannelsByCategory
Screen Routes
Defined in Screen.kt as a sealed class:
| Screen | Route | Arguments |
|---|---|---|
Pairing | "pairing" | — |
Home | "home" | — |
Channels | "channels" | — |
Categories | "categories" | — |
Search | "search" | — |
Favorites | "favorites" | — |
Settings | "settings" | — |
Player | "player/{channelId}" | channelId: String |
ChannelsByCategory | "channels/category/{categoryId}" | categoryId: String (URL-encoded) |
Navigation Graph
FireVisionNavGraph.kt configures NavHost with:
- Start destination:
Pairingon first launch (when TV code not configured), otherwiseHome - Sidebar navigation: Uses
popUpTo(Home),saveState = true,launchSingleTop = true,restoreState = trueto prevent duplicate destinations and preserve scroll state - Player navigation: Direct navigation with
channelIdargument - Pairing success: Auto-navigates to Home after 1.5s delay, clears Pairing from back stack with
inclusive = true
ViewModels
All ViewModels are @HiltViewModel with constructor injection.
ChannelsViewModel
State: ChannelsUiState
| Field | Type | Default |
|---|---|---|
channels | List<ChannelUiModel> | [] |
categories | List<String> | [] |
isLoading | Boolean | true |
isInitialLoadComplete | Boolean | false |
error | String? | null |
errorType | ErrorType | NONE |
selectedCategory | String? | null |
recentlyWatched | List<ChannelUiModel> | [] |
featuredChannels | List<ChannelUiModel> | [] |
popularCategories | List<PopularCategoryUiModel> | [] |
categoryLogos | Map<String, List<String>> | {} |
favoriteCategoryNames | Set<String> | {} |
Key behavior:
init{}— Fires immediately (pre-warmed during splash via Box overlay). Launches EPG loading, channel loading, home data, favorite categories, and refresh in parallel. Channels from Room cache appear in ~50ms.loadChannels(category?)— Collects channel flow combined with health data fromChannelHealthDao. Health flow is seeded with empty list viaonStartafterdebounceso channels render without waiting for health scan.toggleFavorite(channelId)— Optimistic UI update with rollback on errorrefresh()— TriggersRefreshChannelsUseCase. Silent on error when cached data exists.onResume()— Called by HomeScreen's lifecycle observer. Skips first call (init handles it), then triggers silent refresh to detect server-side changes.enrichWithEpgIfReady(channel)— Non-suspend. UsesEpgRepository.getNowNextIfCached()to add "Now Playing" titles without blocking. Returns channel unchanged if EPG not loaded yet.
FavoritesViewModel
State: FavoritesUiState
| Field | Type | Default |
|---|---|---|
favorites | List<ChannelUiModel> | [] |
isLoading | Boolean | false |
error | String? | null |
Key behavior:
loadFavorites()— Called ininit, combines favorites with health dataremoveFavorite(channelId)— Removes with rollback on errormoveFavoriteUp(channelId)/moveFavoriteDown(channelId)— Reorder helpers
SearchViewModel
State: SearchUiState
| Field | Type | Default |
|---|---|---|
query | String | "" |
results | List<ChannelUiModel> | [] |
recentSearches | List<String> | [] |
activeFilters | List<SearchFilter> | [] |
isLoading | Boolean | false |
error | String? | null |
Key behavior:
onQueryChange(query)— Updates query with 300ms debounce before searchingperformSearch(query)— Executes search with active filters, saves to historyaddFilter(filter)/removeFilter(filter)/clearFilters()— Filter management with re-searchclearHistory()— Clears search history
PlayerViewModel
State: PlayerUiState
| Field | Type | Default |
|---|---|---|
channel | ChannelUiModel? | null |
isPlaying | Boolean | false |
isBuffering | Boolean | false |
isLoading | Boolean | false |
position | Long | 0L |
duration | Long | 0L |
showControls | Boolean | true |
error | String? | null |
Key behavior:
loadChannel(channelId)— Loads channel data and restores saved playback positionstartPeriodicPositionSaving()— Saves position every 5 seconds during playbackonCleared()— Final position save and cleanup
SettingsViewModel
State: SettingsUiState
| Field | Type | Default |
|---|---|---|
theme | String | "dark" |
gridSize | Int | 3 |
fontSize | Float | 1.0f |
animationSpeed | Float | 1.0f |
layoutDensity | String | "comfortable" |
serverUrl | String | "" |
tvCode | String | "" |
qrCodeBitmap | Bitmap? | null |
isPaired | Boolean | false |
appVersion | String | "1.0.0" |
Key behavior:
saveServerSettings()— Validates and saves server config, generates QR code via ZXingtriggerLivelinessCheck()— Triggers manual channel health scan viaChannelHealthScannerresetToDefaults()— Resets all preferences to defaultsclearCache()— Deletes app cache directory
PairingViewModel
State: PairingUiState
| Field | Type | Default |
|---|---|---|
pin | String | "" (6-digit PIN) |
statusMessage | String | "" |
countdownText | String | "" |
isLoading | Boolean | false |
qrCodeBitmap | Bitmap? | null |
isPaired | Boolean | false |
Key behavior:
requestNewPairing()— POST to/api/v1/tv/pairing/requestwith device info, receives 6-digit PINstartPolling(pin)— Polls/api/v1/tv/pairing/status/{pin}every 3 seconds (max ~10 minutes)onPairingSuccess(channelListCode, username)— Saves TV code to SharedPreferencesuseDefaultChannelList()— Uses default code"5T6FEP"for quick setup
UI Models
ChannelUiModel
data class ChannelUiModel(
val id: String,
val name: String,
val logoUrl: String?,
val streamUrl: String?,
val category: String,
val isFavorite: Boolean,
val healthStatus: ChannelHealthStatus = UNKNOWN,
val thumbnailPath: String?
)
CategoryUiModel
data class CategoryUiModel(
val id: String,
val name: String,
val channelCount: Int
)
UI Mappers
ChannelUiMapper (@Singleton)
| Method | Description |
|---|---|
toUiModel(channel, healthStatus?, thumbnailPath?) | Single domain Channel → ChannelUiModel |
toUiModelsWithHealth(channels, healthEntities) | Batch conversion. Matches health entities by channelId, defaults to UNKNOWN. |
fromUiModel(uiModel) | Reverse: ChannelUiModel → Channel |
CategoryUiMapper (@Singleton)
| Method | Description |
|---|---|
toUiModel(category) | Category → CategoryUiModel |
fromUiModel(uiModel) | CategoryUiModel → Category |
Screens
| Screen | ViewModel | Description |
|---|---|---|
HomeScreen | ChannelsViewModel | Featured channels, recently watched, popular categories, category rows. Lifecycle-aware: refreshes on ON_RESUME via DisposableEffect + LifecycleEventObserver. |
ChannelsScreen | ChannelsViewModel | Full channel list with category filter tabs |
CategoriesScreen | ChannelsViewModel | Category grid with channel counts |
SearchScreen | SearchViewModel | Search input with debounce, filter chips, recent searches, results grid |
FavoritesScreen | FavoritesViewModel | Ordered favorites list with reorder and remove actions |
PlayerScreen | PlayerViewModel | Full-screen ExoPlayer with overlay controls, position resume |
SettingsScreen | SettingsViewModel | Preferences, server config, QR code, cache management |
PairingScreen | PairingViewModel | PIN display, QR code, countdown timer, polling status |
LegacySettingsScreen | — | Kept for backward compatibility |
Reusable Components
| Component | Description |
|---|---|
ChannelCard | TV-focused card with logo, name, health indicator, favorite icon, and optional thumbnail |
ChannelCardSkeleton | Shimmer loading placeholder for channel cards |
SideNavRail | Persistent vertical navigation rail for sidebar routes |
PlayerControls | Overlay controls for the player (play/pause, seek, channel info) |
EmptyState | Centered message for empty lists |
ErrorState | Error message with retry action |
LoadingIndicator | Centered loading spinner |
Player Infrastructure
PlaybackManager
ui/player/PlaybackManager.kt
Manages ExoPlayer lifecycle, media source creation, and playback state.
ErrorRecoveryManager
ui/player/ErrorRecoveryManager.kt
Handles playback errors with retry logic and user-facing error messages.
Theme
Color.kt— Color definitions for dark TV themeTheme.kt—FireVisionThemecomposable wrapping Material3 dark color schemeType.kt— Typography definitions optimized for TV viewing distance