Premiers pas avec l'UI basée sur Compose

Ajouter la dépendance

La bibliothèque Media3 inclut un module d'UI basé sur Jetpack Compose. Pour l'utiliser, ajoutez la dépendance suivante:

Kotlin

implementation("androidx.media3:media3-ui-compose:1.6.0")

Groovy

implementation "androidx.media3:media3-ui-compose:1.6.0"

Nous vous encourageons vivement à développer votre application en privilégiant Compose ou à passer des vues à Compose.

Application de démonstration entièrement Compose

Bien que la bibliothèque media3-ui-compose n'inclue pas de composables prêts à l'emploi (tels que des boutons, des indicateurs, des images ou des boîtes de dialogue), vous trouverez une application de démonstration entièrement écrite en Compose qui évite toute solution d'interopérabilité, comme l'encapsulation de PlayerView dans AndroidView. L'application de démonstration utilise les classes de conteneur d'état de l'UI du module media3-ui-compose et la bibliothèque Compose Material3.

Conteneurs d'état de l'UI

Pour mieux comprendre comment utiliser la flexibilité des conteneurs d'état de l'UI par rapport aux composables, consultez la page sur la façon dont Compose gère l'état.

Conteneurs d'état des boutons

Pour certains états d'UI, nous partons du principe qu'ils seront probablement consommés par des composables ressemblant à des boutons.

État remember*État Type
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState Constante
NextButtonState rememberNextButtonState Constante
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState Menu ou bouton N-Toggle

Exemple d'utilisation de PlayPauseButtonState:

@Composable
fun PlayPauseButton(player: Player, modifier: Modifier = Modifier) {
  val state = rememberPlayPauseButtonState(player)

  IconButton(onClick = state::onClick, modifier = modifier, enabled = state.isEnabled) {
    Icon(
      imageVector = if (state.showPlay) Icons.Default.PlayArrow else Icons.Default.Pause,
      contentDescription =
        if (state.showPlay) stringResource(R.string.playpause_button_play)
        else stringResource(R.string.playpause_button_pause),
    )
  }
}

Notez que state ne contient aucune information de thématisation, comme l'icône à utiliser pour la lecture ou la mise en pause. Sa seule responsabilité est de transformer la Player en état de l'UI.

Vous pouvez ensuite combiner les boutons dans la mise en page de votre choix:

Row(
  modifier = modifier.fillMaxWidth(),
  horizontalArrangement = Arrangement.SpaceEvenly,
  verticalAlignment = Alignment.CenterVertically,
) {
  PreviousButton(player)
  PlayPauseButton(player)
  NextButton(player)
}

Conteneurs d'état de sortie visuelle

PresentationState contient des informations sur le moment où la sortie vidéo dans un PlayerSurface peut être affichée ou doit être recouverte par un élément d'UI d'espace réservé.

val presentationState = rememberPresentationState(player)
val scaledModifier = Modifier.resize(ContentScale.Fit, presentationState.videoSizeDp)

Box(modifier) {
  // Always leave PlayerSurface to be part of the Compose tree because it will be initialised in
  // the process. If this composable is guarded by some condition, it might never become visible
  // because the Player won't emit the relevant event, e.g. the first frame being ready.
  PlayerSurface(
    player = player,
    surfaceType = SURFACE_TYPE_SURFACE_VIEW,
    modifier = scaledModifier,
  )

  if (presentationState.coverSurface) {
    // Cover the surface that is being prepared with a shutter
    Box(Modifier.background(Color.Black))
  }

Ici, nous pouvons utiliser à la fois presentationState.videoSizeDp pour mettre à l'échelle la surface au format souhaité (voir la documentation ContentScale pour en savoir plus sur les types) et presentationState.coverSurface pour savoir quand le moment n'est pas propice à l'affichage de la surface. Dans ce cas, vous pouvez placer un volet opaque au-dessus de la surface, qui disparaîtra lorsque la surface sera prête.

Où se trouvent les flux ?

De nombreux développeurs Android sont habitués à utiliser des objets Flow Kotlin pour collecter des données d'interface utilisateur en constante évolution. Par exemple, vous pouvez rechercher un flux Player.isPlaying que vous pouvez collect de manière tenant compte du cycle de vie. Ou quelque chose comme Player.eventsFlow pour vous fournir un Flow<Player.Events> que vous pouvez filter comme vous le souhaitez.

Toutefois, l'utilisation de flux pour l'état de l'UI Player présente certains inconvénients. L'une des principales préoccupations concerne la nature asynchrone du transfert de données. Nous souhaitons assurer une latence aussi faible que possible entre un Player.Event et sa consommation côté UI, en évitant d'afficher des éléments d'UI qui ne sont pas synchronisés avec le Player.

Voici d'autres points à prendre en compte:

  • Un flux avec tous les Player.Events ne respecterait pas un principe de responsabilité unique. Chaque consommateur devrait filtrer les événements pertinents.
  • Si vous créez un flux pour chaque Player.Event, vous devrez les combiner (avec combine) pour chaque élément d'interface utilisateur. Il existe un mappage de plusieurs à plusieurs entre un Player.Event et un changement d'élément d'interface utilisateur. L'utilisation de combine peut entraîner des états potentiellement illégaux dans l'UI.

Créer des états d'interface utilisateur personnalisés

Vous pouvez ajouter des états d'interface utilisateur personnalisés si les états existants ne répondent pas à vos besoins. Vérifiez le code source de l'état existant pour copier le modèle. Une classe de conteneur d'état d'UI type effectue les opérations suivantes:

  1. Accepte un Player.
  2. S'abonne à Player à l'aide de coroutines. Pour en savoir plus, consultez Player.listen.
  3. Répond à un Player.Events particulier en mettant à jour son état interne.
  4. Acceptez les commandes de logique métier qui seront transformées en mise à jour Player appropriée.
  5. Peut être créé à plusieurs endroits dans l'arborescence de l'UI et conserve toujours une vue cohérente de l'état du joueur.
  6. Exposer les champs State Compose pouvant être utilisés par un composable pour répondre dynamiquement aux modifications.
  7. Inclut une fonction remember*State pour mémoriser l'instance entre les compositions.

Que se passe-t-il en arrière-plan ?

class SomeButtonState(private val player: Player) {
  var isEnabled by mutableStateOf(player.isCommandAvailable(Player.COMMAND_ACTION_A))
    private set

  var someField by mutableStateOf(someFieldDefault)
    private set

  fun onClick() {
    player.actionA()
  }

  suspend fun observe() =
    player.listen { events ->
      if (
        events.containsAny(
          Player.EVENT_B_CHANGED,
          Player.EVENT_C_CHANGED,
          Player.EVENT_AVAILABLE_COMMANDS_CHANGED,
        )
      ) {
        someField = this.someField
        isEnabled = this.isCommandAvailable(Player.COMMAND_ACTION_A)
      }
    }
}

Pour réagir à votre propre Player.Events, vous pouvez les intercepter à l'aide de Player.listen, qui est un suspend fun qui vous permet d'entrer dans le monde des coroutines et d'écouter indéfiniment Player.Events. L'implémentation de divers états d'interface utilisateur par Media3 permet au développeur final de ne pas avoir à se soucier de l'apprentissage de Player.Events.