Initial upload: iptv-android-tv-hermes
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
# Architecture d'iMedia TV+
|
||||
|
||||
## Principes fondateurs
|
||||
|
||||
- **Clean Architecture** simplifiée : UI → ViewModel → Repository → Data
|
||||
- **Unidirectional Data Flow (UDF)** : les events montent, les states descendent
|
||||
- **Single Activity** : une seule `MainActivity`, navigation par écrans Compose
|
||||
- **Programmation réactive** : Kotlin Coroutines + Flow pour tout ce qui est async
|
||||
- **Séparation claire** : chaque couche ignore l'existence des couches au-dessus d'elle
|
||||
|
||||
## Flux de données complet
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User as Utilisateur (Télécommande)
|
||||
participant UI as Compose Screen
|
||||
participant VM as ViewModel
|
||||
participant Repo as Repository
|
||||
participant API as Retrofit / Xtream
|
||||
participant DB as Room (Cache)
|
||||
participant Store as EncryptedSP
|
||||
|
||||
User->>UI: Appuie sur OK (connexion)
|
||||
UI->>VM: Event: Login(server, user, pass)
|
||||
VM->>Repo: authRepository.login(server, user, pass)
|
||||
Repo->>API: POST /player_api.php?username=...
|
||||
API-->>Repo: Response { auth: "1", status: "Active" }
|
||||
Repo->>Store: saveCredentials(encrypted)
|
||||
Repo-->>VM: Result.Success(userInfo)
|
||||
VM->>VM: _uiState = UiState.Success
|
||||
VM-->>UI: StateFlow: HomeScreen data
|
||||
UI-->>User: Affiche la liste des chaînes
|
||||
```
|
||||
|
||||
## Diagramme de navigation des écrans
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> SplashScreen
|
||||
SplashScreen --> LoginScreen : Pas de session
|
||||
SplashScreen --> HomeScreen : Session valide
|
||||
|
||||
LoginScreen --> HomeScreen : Connexion OK
|
||||
LoginScreen --> LoginScreen : Erreur → reste
|
||||
|
||||
HomeScreen --> PlayerScreen : Sélection chaîne Live
|
||||
HomeScreen --> ProgramGuideScreen : Appui Guide TV
|
||||
HomeScreen --> ReplayGuideScreen : Appui Replay
|
||||
HomeScreen --> VodScreen : Appui VOD
|
||||
HomeScreen --> SeriesScreen : Appui Séries
|
||||
|
||||
ProgramGuideScreen --> PlayerScreen : "Regarder"
|
||||
ReplayGuideScreen --> PlayerScreen : "Revoir"
|
||||
VodScreen --> PlayerScreen : Lecture film
|
||||
SeriesScreen --> PlayerScreen : Lecture épisode
|
||||
|
||||
PlayerScreen --> HomeScreen : Retour × 2
|
||||
PlayerScreen --> ProgramGuideScreen : Retour × 1 (si venu du guide)
|
||||
PlayerScreen --> ReplayGuideScreen : Retour × 1 (si venu du replay)
|
||||
|
||||
HomeScreen --> ConfirmQuit : Retour
|
||||
ConfirmQuit --> [*] : Confirmer
|
||||
ConfirmQuit --> HomeScreen : Annuler
|
||||
```
|
||||
|
||||
## Organisation des ViewModels
|
||||
|
||||
Chaque ViewModel expose :
|
||||
- Un `StateFlow<UiState<T>>` pour l'état de l'UI
|
||||
- Des fonctions `onAction(X)` pour les intents utilisateur
|
||||
- Une vue unique (`viewModelScope`) pour les coroutines
|
||||
|
||||
```kotlin
|
||||
class HomeViewModel(
|
||||
private val liveRepo: LiveRepository,
|
||||
private val epgRepo: EpgRepository,
|
||||
private val favRepo: FavoritesRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow<UiState<HomeData>>(UiState.Loading)
|
||||
val uiState: StateFlow<UiState<HomeData>> = _uiState.asStateFlow()
|
||||
|
||||
sealed interface Action {
|
||||
data class SelectCategory(val id: Int) : Action
|
||||
data class ToggleFavorite(val streamId: Int) : Action
|
||||
data class PinChannel(val streamId: Int) : Action
|
||||
object Refresh : Action
|
||||
}
|
||||
|
||||
fun onAction(action: Action) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Injection de dépendances
|
||||
|
||||
Utilisation d'un **manual DI** simple via `AppContainer` (pas de framework lourd comme Hilt/Dagger pour un projet de cette taille).
|
||||
|
||||
```kotlin
|
||||
// AppContainer.kt
|
||||
class AppContainer(context: Context) {
|
||||
private val secureStorage = SecureStorage(context)
|
||||
private val okHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(ErrorInterceptor())
|
||||
.addInterceptor(AuthInterceptor(secureStorage))
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private val retrofit = Retrofit.Builder()
|
||||
.baseUrl("http://placeholder") // overridden per-call
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build()
|
||||
|
||||
val xtreamApi = retrofit.create(XtreamApi::class.java)
|
||||
|
||||
// Repositories
|
||||
val authRepository = AuthRepository(xtreamApi, secureStorage)
|
||||
val liveRepository = LiveRepository(xtreamApi)
|
||||
val epgRepository = EpgRepository(xtreamApi, EpgDatabase.getInstance(context))
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Décisions d'architecture
|
||||
|
||||
| Décision | Alternative écartée | Raison |
|
||||
|----------|-------------------|--------|
|
||||
| Single Activity | Multi-Activity | Navigation + état simplifiés |
|
||||
| Manual DI | Hilt / Koin | Pas de overhead pour projet solo |
|
||||
| Room pour cache EPG | Simple JSON file | Requêtes typées, ACID, migrations |
|
||||
| EncryptedSharedPreferences | DataStore | Chiffrement natif, API simple |
|
||||
| ViewModel + StateFlow | MVVM + LiveData | Flow plus puissant, compose-ready |
|
||||
| sealed interface pour actions | Callbacks | Testable, traçable, immutable |
|
||||
Reference in New Issue
Block a user