Files

4.7 KiB
Raw Permalink Blame History

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

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

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

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