137 lines
4.7 KiB
Markdown
137 lines
4.7 KiB
Markdown
# 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 |
|