Cómo comenzar a usar la IU basada en Compose

Agrega la dependencia

La biblioteca de Media3 incluye un módulo de IU basado en Jetpack Compose. Para usarlo, agrega la siguiente dependencia:

Kotlin

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

Groovy

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

Te recomendamos que desarrolles tu app en un formato en el que se priorice Compose o que migres desde el uso de objetos View.

App de demostración de Compose completa

Si bien la biblioteca de media3-ui-compose no incluye elementos componibles listos para usar (como botones, indicadores, imágenes o diálogos), puedes encontrar una app de demostración escrita completamente en Compose que evita cualquier solución de interoperabilidad, como unir PlayerView en AndroidView. La app de demostración usa las clases de contenedor de estado de la IU del módulo media3-ui-compose y la biblioteca de Compose Material3.

Contenedores de estado de la IU

Para comprender mejor cómo puedes usar la flexibilidad de los contenedores de estado de la IU en comparación con los elementos componibles, consulta cómo Compose administra el estado.

Contenedores de estado de los botones

Para algunos estados de la IU, suponemos que es probable que los consuman elementos componibles similares a botones.

State remember*State Tipo
PlayPauseButtonState rememberPlayPauseButtonState 2 botones de activación
PreviousButtonState rememberPreviousButtonState Constante
NextButtonState rememberNextButtonState Constante
RepeatButtonState rememberRepeatButtonState 3 botones de activación
ShuffleButtonState rememberShuffleButtonState 2 botones de activación
PlaybackSpeedState rememberPlaybackSpeedState Menú o N-Toggle

Ejemplo de uso 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),
    )
  }
}

Observa que state no tiene información de temas, como el ícono que se usará para reproducir o pausar. Su única responsabilidad es transformar el Player en el estado de la IU.

Luego, puedes combinar los botones en el diseño que prefieras:

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

Contenedores de estado de salida visual

PresentationState contiene información sobre cuándo se puede mostrar el resultado de video en un PlayerSurface o si debe cubrirse con un elemento de IU de marcador de posición.

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

Aquí, podemos usar presentationState.videoSizeDp para escalar la superficie a la relación de aspecto deseada (consulta la documentación de ContentScale para obtener más tipos) y presentationState.coverSurface para saber cuándo no es el momento adecuado para mostrar la superficie. En este caso, puedes colocar un obturador opaco sobre la superficie, que desaparecerá cuando esta esté lista.

¿Dónde están los flujos?

Muchos desarrolladores de Android están familiarizados con el uso de objetos Flow de Kotlin para recopilar datos de IU en constante cambio. Por ejemplo, podrías estar buscando un flujo Player.isPlaying que puedas collect de forma consciente del ciclo de vida. O bien, algo como Player.eventsFlow para proporcionarte un Flow<Player.Events> que puedas filter como quieras.

Sin embargo, usar flujos para el estado de la IU de Player tiene algunas desventajas. Una de las principales preocupaciones es la naturaleza asíncrona de la transferencia de datos. Queremos garantizar la menor latencia posible entre un Player.Event y su consumo en el lado de la IU, evitando mostrar elementos de la IU que no estén sincronizados con el Player.

Otros puntos incluyen los siguientes:

  • Un flujo con todos los Player.Events no cumpliría con un principio de responsabilidad única, ya que cada consumidor tendría que filtrar los eventos relevantes.
  • Si creas un flujo para cada Player.Event, deberás combinarlos (con combine) para cada elemento de la IU. Hay una asignación de varios a varios entre un Player.Event y un cambio de elemento de la IU. Tener que usar combine podría llevar a la IU a estados potencialmente ilegales.

Crea estados de IU personalizados

Puedes agregar estados de IU personalizados si los existentes no satisfacen tus necesidades. Revisa el código fuente del estado existente para copiar el patrón. Una clase de contenedor de estado de IU típica hace lo siguiente:

  1. Recibe un Player.
  2. Se suscribe a Player con corrutinas. Consulta Player.listen para obtener más detalles.
  3. Responde a Player.Events en particular actualizando su estado interno.
  4. Acepta comandos de lógica empresarial que se transformarán en una actualización Player adecuada.
  5. Se puede crear en varios lugares del árbol de la IU y siempre mantendrá una vista coherente del estado del jugador.
  6. Expone los campos State de Compose que puede consumir un elemento componible para responder de forma dinámica a los cambios.
  7. Viene con una función remember*State para recordar la instancia entre composiciones.

Qué sucede tras bambalinas:

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

Para reaccionar a tu propio Player.Events, puedes detectarlo con Player.listen, que es un suspend fun que te permite ingresar al mundo de las corrutinas y escuchar Player.Events de forma indefinida. La implementación de Media3 de varios estados de la IU ayuda al desarrollador final a no preocuparse por aprender sobre Player.Events.