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
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:

ScreenRouteArguments
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)

FireVisionNavGraph.kt configures NavHost with:

  • Start destination: Pairing on first launch (when TV code not configured), otherwise Home
  • Sidebar navigation: Uses popUpTo(Home), saveState = true, launchSingleTop = true, restoreState = true to prevent duplicate destinations and preserve scroll state
  • Player navigation: Direct navigation with channelId argument
  • 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

FieldTypeDefault
channelsList<ChannelUiModel>[]
categoriesList<String>[]
isLoadingBooleantrue
isInitialLoadCompleteBooleanfalse
errorString?null
errorTypeErrorTypeNONE
selectedCategoryString?null
recentlyWatchedList<ChannelUiModel>[]
featuredChannelsList<ChannelUiModel>[]
popularCategoriesList<PopularCategoryUiModel>[]
categoryLogosMap<String, List<String>>{}
favoriteCategoryNamesSet<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 from ChannelHealthDao. Health flow is seeded with empty list via onStart after debounce so channels render without waiting for health scan.
  • toggleFavorite(channelId) — Optimistic UI update with rollback on error
  • refresh() — Triggers RefreshChannelsUseCase. 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. Uses EpgRepository.getNowNextIfCached() to add "Now Playing" titles without blocking. Returns channel unchanged if EPG not loaded yet.

FavoritesViewModel

State: FavoritesUiState

FieldTypeDefault
favoritesList<ChannelUiModel>[]
isLoadingBooleanfalse
errorString?null

Key behavior:

  • loadFavorites() — Called in init, combines favorites with health data
  • removeFavorite(channelId) — Removes with rollback on error
  • moveFavoriteUp(channelId) / moveFavoriteDown(channelId) — Reorder helpers

SearchViewModel

State: SearchUiState

FieldTypeDefault
queryString""
resultsList<ChannelUiModel>[]
recentSearchesList<String>[]
activeFiltersList<SearchFilter>[]
isLoadingBooleanfalse
errorString?null

Key behavior:

  • onQueryChange(query) — Updates query with 300ms debounce before searching
  • performSearch(query) — Executes search with active filters, saves to history
  • addFilter(filter) / removeFilter(filter) / clearFilters() — Filter management with re-search
  • clearHistory() — Clears search history

PlayerViewModel

State: PlayerUiState

FieldTypeDefault
channelChannelUiModel?null
isPlayingBooleanfalse
isBufferingBooleanfalse
isLoadingBooleanfalse
positionLong0L
durationLong0L
showControlsBooleantrue
errorString?null

Key behavior:

  • loadChannel(channelId) — Loads channel data and restores saved playback position
  • startPeriodicPositionSaving() — Saves position every 5 seconds during playback
  • onCleared() — Final position save and cleanup

SettingsViewModel

State: SettingsUiState

FieldTypeDefault
themeString"dark"
gridSizeInt3
fontSizeFloat1.0f
animationSpeedFloat1.0f
layoutDensityString"comfortable"
serverUrlString""
tvCodeString""
qrCodeBitmapBitmap?null
isPairedBooleanfalse
appVersionString"1.0.0"

Key behavior:

  • saveServerSettings() — Validates and saves server config, generates QR code via ZXing
  • triggerLivelinessCheck() — Triggers manual channel health scan via ChannelHealthScanner
  • resetToDefaults() — Resets all preferences to defaults
  • clearCache() — Deletes app cache directory

PairingViewModel

State: PairingUiState

FieldTypeDefault
pinString"" (6-digit PIN)
statusMessageString""
countdownTextString""
isLoadingBooleanfalse
qrCodeBitmapBitmap?null
isPairedBooleanfalse

Key behavior:

  • requestNewPairing() — POST to /api/v1/tv/pairing/request with device info, receives 6-digit PIN
  • startPolling(pin) — Polls /api/v1/tv/pairing/status/{pin} every 3 seconds (max ~10 minutes)
  • onPairingSuccess(channelListCode, username) — Saves TV code to SharedPreferences
  • useDefaultChannelList() — 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)

MethodDescription
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)

MethodDescription
toUiModel(category)Category → CategoryUiModel
fromUiModel(uiModel)CategoryUiModel → Category

Screens

ScreenViewModelDescription
HomeScreenChannelsViewModelFeatured channels, recently watched, popular categories, category rows. Lifecycle-aware: refreshes on ON_RESUME via DisposableEffect + LifecycleEventObserver.
ChannelsScreenChannelsViewModelFull channel list with category filter tabs
CategoriesScreenChannelsViewModelCategory grid with channel counts
SearchScreenSearchViewModelSearch input with debounce, filter chips, recent searches, results grid
FavoritesScreenFavoritesViewModelOrdered favorites list with reorder and remove actions
PlayerScreenPlayerViewModelFull-screen ExoPlayer with overlay controls, position resume
SettingsScreenSettingsViewModelPreferences, server config, QR code, cache management
PairingScreenPairingViewModelPIN display, QR code, countdown timer, polling status
LegacySettingsScreenKept for backward compatibility

Reusable Components

ComponentDescription
ChannelCardTV-focused card with logo, name, health indicator, favorite icon, and optional thumbnail
ChannelCardSkeletonShimmer loading placeholder for channel cards
SideNavRailPersistent vertical navigation rail for sidebar routes
PlayerControlsOverlay controls for the player (play/pause, seek, channel info)
EmptyStateCentered message for empty lists
ErrorStateError message with retry action
LoadingIndicatorCentered 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 theme
  • Theme.ktFireVisionTheme composable wrapping Material3 dark color scheme
  • Type.kt — Typography definitions optimized for TV viewing distance