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 (concombine
) 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 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. 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:
- Recibe un
Player
. - Se suscribe a
Player
con corrutinas. ConsultaPlayer.listen
para obtener más detalles. - Responde a
Player.Events
en particular actualizando su estado interno. - Acepta comandos de lógica empresarial que se transformarán en una actualización
Player
adecuada. - Se puede crear en varios lugares del árbol de la IU y siempre mantendrá una vista coherente del estado del jugador.
- Expone los campos
State
de Compose que puede consumir un elemento componible para responder de forma dinámica a los cambios. - 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
.