Data Layer
April 4, 2026 · View on GitHub
Implements domain repository interfaces. Manages local persistence (Room), remote API (Retrofit), and data mapping between DTOs, entities, and domain models.
flowchart LR
API["Retrofit API"] --> DTO
DTO --> Mapper
Mapper --> Entity["Room Entity"]
Entity --> Room["Room DB"]
Room --> Flow["Flow<Entity>"]
Flow --> Mapper2["Domain Mapper"]
Mapper2 --> Domain["Domain Model"]
Domain --> ViewModel
Package: com.cadnative.firevisioniptv.data
Package Structure
data/
├── mapper/ # Bidirectional data mappers
│ ├── CategoryMapper.kt
│ └── ChannelMapper.kt
├── model/
│ ├── dto/ # API response/request DTOs
│ │ ├── CategoriesResponse.kt
│ │ ├── CategoryDto.kt
│ │ ├── ChannelDto.kt
│ │ ├── ChannelsResponse.kt
│ │ └── FavoritesRequest.kt
│ └── Result.kt # Sealed Result<T> wrapper
├── repository/ # Repository implementations
│ ├── CategoryRepositoryImpl.kt
│ ├── ChannelRepositoryImpl.kt
│ ├── EpgRepositoryImpl.kt
│ ├── FavoriteRepositoryImpl.kt
│ ├── PlaybackRepositoryImpl.kt
│ ├── SearchHistoryRepositoryImpl.kt
│ └── UserPreferencesRepositoryImpl.kt
└── source/
├── local/
│ ├── dao/ # Room DAOs
│ │ ├── CategoryDao.kt
│ │ ├── ChannelDao.kt
│ │ ├── ChannelHealthDao.kt
│ │ ├── FavoriteDao.kt
│ │ ├── PlaybackPositionDao.kt
│ │ └── SearchHistoryDao.kt
│ ├── entity/ # Room entities
│ │ ├── CategoryEntity.kt
│ │ ├── ChannelEntity.kt
│ │ ├── ChannelHealthEntity.kt
│ │ ├── FavoriteEntity.kt
│ │ ├── PlaybackPositionEntity.kt
│ │ └── SearchHistoryEntity.kt
│ ├── FireVisionDatabase.kt
│ ├── CategoryLocalDataSource.kt
│ ├── ChannelLocalDataSource.kt
│ ├── FavoriteLocalDataSource.kt
│ ├── PlaybackLocalDataSource.kt
│ └── SearchHistoryLocalDataSource.kt
└── remote/
├── FireVisionApiService.kt
├── CategoryRemoteDataSource.kt
└── ChannelRemoteDataSource.kt
Room Database
FireVisionDatabase
data/source/local/FireVisionDatabase.kt
- Version: 3
- Schema export: Enabled (schemas stored in
app/schemas/) - Migration strategy:
fallbackToDestructiveMigration()(development mode)
Provides 6 DAOs:
abstract fun channelDao(): ChannelDao
abstract fun categoryDao(): CategoryDao
abstract fun favoriteDao(): FavoriteDao
abstract fun searchHistoryDao(): SearchHistoryDao
abstract fun playbackPositionDao(): PlaybackPositionDao
abstract fun channelHealthDao(): ChannelHealthDao
Entities
channels
| Column | Type | Constraints | Description |
|---|---|---|---|
id | TEXT | PRIMARY KEY | Channel identifier |
name | TEXT | NOT NULL, INDEXED | Display name |
streamUrl | TEXT | NOT NULL | Streaming URL |
logoUrl | TEXT | nullable | Logo image URL |
categoryId | TEXT | NOT NULL, INDEXED | Category reference |
language | TEXT | nullable | Language code |
country | TEXT | nullable | Country code |
groupTitle | TEXT | nullable | M3U group title |
tvgId | TEXT | nullable | TVG identifier |
tvgName | TEXT | nullable | TVG name |
isActive | INTEGER | NOT NULL, INDEXED, default 1 | Active flag |
lastUpdated | INTEGER | NOT NULL | Timestamp |
categories
| Column | Type | Constraints |
|---|---|---|
id | TEXT | PRIMARY KEY |
name | TEXT | NOT NULL |
displayOrder | INTEGER | NOT NULL, default 0 |
channelCount | INTEGER | NOT NULL, default 0 |
channel_health
| Column | Type | Constraints |
|---|---|---|
channelId | TEXT | PRIMARY KEY, FK → channels(id) ON DELETE CASCADE |
status | TEXT | NOT NULL |
lastCheckedAt | INTEGER | NOT NULL, INDEXED, default 0 |
responseTimeMs | INTEGER | nullable |
errorMessage | TEXT | nullable |
thumbnailPath | TEXT | nullable |
Unique index on channelId. Additional index on status.
favorites
| Column | Type | Constraints |
|---|---|---|
id | INTEGER | PRIMARY KEY AUTOINCREMENT |
channelId | TEXT | NOT NULL, INDEXED, FK → channels(id) ON DELETE CASCADE |
addedAt | INTEGER | NOT NULL |
displayOrder | INTEGER | NOT NULL, default 0 |
playback_positions
| Column | Type | Constraints |
|---|---|---|
channelId | TEXT | PRIMARY KEY |
position | INTEGER | NOT NULL |
duration | INTEGER | NOT NULL |
lastPlayed | INTEGER | NOT NULL, INDEXED |
search_history
| Column | Type | Constraints |
|---|---|---|
id | INTEGER | PRIMARY KEY AUTOINCREMENT |
query | TEXT | NOT NULL |
timestamp | INTEGER | NOT NULL, INDEXED |
Key DAO Queries
ChannelDao:
getAllChannels()— Active channels ordered by name, returnsFlowsearchChannels(query)— Case-insensitive LIKE search onnameandgroupTitlereplaceAllChannels(channels)—@Transaction: atomic delete-all + insert-all
ChannelHealthDao:
upsertPreservingThumbnail(...)— Updates health status while preserving existingthumbnailPathgetOnlineChannelIdsWithoutThumbnail()— Returns ONLINE channels with NULLthumbnailPathgetAllChannelIdsByPriority()— Ordered bylastCheckedAtASC (oldest first)cleanupOrphaned()— Deletes health records with no matching channel
FavoriteDao:
getFavoriteChannels()— JOIN query:favorites INNER JOIN channels, ordered bydisplayOrderASC,addedAtDESCisFavorite(channelId)— EXISTS subquery returningFlow<Boolean>
Remote API
FireVisionApiService
data/source/remote/FireVisionApiService.kt
Retrofit interface:
| Method | Endpoint | Request | Response |
|---|---|---|---|
GET | /api/v1/channels | — | Response<ChannelsResponse> |
GET | /api/v1/channels/{id} | @Path("id") | Response<ChannelDto> |
GET | /api/v1/categories | — | Response<CategoriesResponse> |
POST | /api/v1/favorites | @Body FavoritesRequest | Response<Unit> |
GET | /api/v1/playlist.m3u | — | Response<ResponseBody> |
DTOs
ChannelsResponse:
data class ChannelsResponse(
@SerializedName("success") val success: Boolean,
@SerializedName("data") val data: List<ChannelDto>
)
ChannelDto:
data class ChannelDto(
@SerializedName("channelId") val id: String,
@SerializedName("channelName") val name: String,
@SerializedName("channelUrl") val url: String,
@SerializedName("channelImg") val channelImg: String?,
val tvgLogo: String?,
@SerializedName("channelGroup") val groupTitle: String?,
@SerializedName("channelDrmKey") val drmKey: String?,
@SerializedName("channelDrmType") val drmType: String?,
val tvgLanguage: String?,
val tvgCountry: String?,
val tvgId: String?,
val tvgName: String?,
val isActive: Boolean,
val metadata: ChannelMetadataDto?
)
// Computed: logoUrl = tvgLogo ?: channelImg
CategoriesResponse:
data class CategoriesResponse(
@SerializedName("categories") val categories: List<CategoryDto>,
@SerializedName("total") val total: Int?
)
FavoritesRequest:
data class FavoritesRequest(
@SerializedName("channel_ids") val channelIds: List<String>,
@SerializedName("device_id") val deviceId: String?,
@SerializedName("timestamp") val timestamp: Long
)
Error Handling
Remote data sources map HTTP errors to typed exceptions:
| HTTP Code | Exception |
|---|---|
| Network failure | NetworkException |
| 400 | BadRequestException |
| 401 | UnauthorizedException |
| 403 | ForbiddenException |
| 404 | NotFoundException |
| 500 | ServerException |
| 503 | ServiceUnavailableException |
| Other | UnknownException |
Data Mappers
ChannelMapper
data/mapper/ChannelMapper.kt — @Singleton
| Method | From | To | Notes |
|---|---|---|---|
toDomain(entity, isFavorite) | ChannelEntity | Channel | Accepts optional favorite flag |
toEntity(dto) | ChannelDto | ChannelEntity | Resolves logo from tvgLogo or channelImg, defaults category to "uncategorized" |
fromDomain(channel) | Channel | ChannelEntity | Reverse mapping |
CategoryMapper
data/mapper/CategoryMapper.kt — @Singleton
| Method | From | To |
|---|---|---|
toDomain(entity) | CategoryEntity | Category |
toEntity(dto) | CategoryDto | CategoryEntity |
fromDomain(category) | Category | CategoryEntity |
Result Wrapper
data/model/Result.kt
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
val isSuccess: Boolean
val isError: Boolean
fun getOrNull(): T?
fun exceptionOrNull(): Exception?
companion object {
fun <T> success(data: T): Result<T>
fun error(exception: Exception): Result<Nothing>
}
}
Repository Implementations
All repositories are @Singleton and follow the offline-first pattern: local database is the single source of truth, remote data refreshes the cache.
ChannelRepositoryImpl
getChannels()— CombinesChannelDao.getAllChannels()withFavoriteDao.getAllFavorites()usingflow.combine()to enrich channels with favorite statusrefreshChannels()— Fetches from API, maps DTOs → entities, atomically replaces all local channels via@Transaction
FavoriteRepositoryImpl
addFavorite()/removeFavorite()— Updates local DB, then fires background sync toPOST /api/v1/favoritessyncFavorites()— Collects all local favorite channel IDs and sends to server
EpgRepositoryImpl
data/repository/EpgRepositoryImpl.kt — @Singleton
In-memory EPG cache. Fetches guide data from GET /api/v1/epg/guide on first access, then serves from memory.
ensureLoaded()— Double-checked locking withMutex. Fetches EPG once, mapsEpgProgramDto→EpgProgram, stores inMap<tvgId, List<EpgProgram>>.getNowNext(tvgId)— Suspend. CallsensureLoaded()if needed, then finds current/next programs by comparingInstant.now()against start/end times.getNowNextIfCached(tvgId)— Synchronous, non-blocking. Returnsnullwhen!cacheLoaded(EPG not fetched yet). This is the key performance optimization:ChannelsViewModelcalls this during channel rendering so channels appear instantly without waiting for EPG network call.
Network failures are silently swallowed — EPG is non-critical, the app degrades gracefully (no "Now Playing" titles).
UserPreferencesRepositoryImpl
- Uses
SharedPreferences(not Room) withMutableStateFlowfor reactive preference updates clearCache()— Recursively deletescontext.cacheDir