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.7.1")
Groovy
implementation "androidx.media3:media3-ui-compose:1.7.1"
Te recomendamos que desarrolles tu app con Compose como prioridad o que migres desde el uso de Views.
App de demostración completamente en Compose
Si bien la biblioteca media3-ui-compose
no incluye elementos Composables 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 envolver PlayerView
en AndroidView
. La app de demostración utiliza las clases de titulares del estado de la IU del módulo media3-ui-compose
y usa la biblioteca 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, lee sobre cómo Compose administra el estado.
Contenedores de estado del botón
Para algunos estados de la IU, suponemos que es más probable que los consuman elementos componibles similares a botones.
Estado | remember*State | Tipo |
---|---|---|
PlayPauseButtonState |
rememberPlayPauseButtonState |
2-Toggle |
PreviousButtonState |
rememberPreviousButtonState |
Constante |
NextButtonState |
rememberNextButtonState |
Constante |
RepeatButtonState |
rememberRepeatButtonState |
3-Toggle |
ShuffleButtonState |
rememberShuffleButtonState |
2-Toggle |
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 cómo state
no posee información de temas, como el ícono que se debe usar para reproducir o pausar. Su única responsabilidad es transformar el Player
en 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 video en un PlayerSurface
o cuándo se debe cubrir 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 tanto presentationState.videoSizeDp
para ajustar la escala de Surface a la relación de aspecto deseada (consulta la documentación de ContentScale para obtener más tipos) como presentationState.coverSurface
para saber cuándo no es el momento adecuado para mostrar Surface. En este caso, puedes colocar un obturador opaco sobre la superficie, que desaparecerá cuando la superficie esté lista.
¿Dónde están los Flows?
Muchos desarrolladores de Android saben cómo usar objetos Flow
de Kotlin para recopilar datos de la IU que cambian constantemente. Por ejemplo, podrías estar buscando un flujo de Player.isPlaying
que puedas collect
de una manera consciente del ciclo de vida. O algo como Player.eventsFlow
para proporcionarte un Flow<Player.Events>
que puedes 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, y evitar 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 solo principio de responsabilidad, y cada consumidor tendría que filtrar los eventos pertinentes. - Crear un flujo para cada
Player.Event
requerirá que los combines (concombine
) para cada elemento de la IU. Existe una asignación de varios a varios entre un Player.Event y un cambio en un elemento de la IU. Tener que usarcombine
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. Consulta 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:
- Toma un
Player
. - Se suscribe a
Player
con corrutinas. ConsultaPlayer.listen
para obtener más detalles. - Responde a un
Player.Events
en particular actualizando su estado interno. - Aceptar comandos de lógica empresarial que se transformarán en una actualización de
Player
adecuada - Se puede crear en varios lugares del árbol de IU y siempre mantendrá una vista coherente del estado del reproductor.
- Expone los campos
State
de Compose que un elemento componible puede consumir para responder de forma dinámica a los cambios. - Incluye una función
remember*State
para recordar la instancia entre composiciones.
Qué sucede en segundo plano:
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 tus propios Player.Events
, puedes detectarlos 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
.