Introdução à interface baseada no Compose

Adicionar a dependência

A biblioteca Media3 inclui um módulo de interface baseado no Jetpack Compose. Para usá-lo, adicione a seguinte dependência:

Kotlin

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

Groovy

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

Recomendamos que você desenvolva seu app com foco no Compose ou migre de visualizações.

App de demonstração totalmente do Compose

Embora a biblioteca media3-ui-compose não inclua elementos combináveis prontos para uso (como botões, indicadores, imagens ou caixas de diálogo), você pode encontrar um app de demonstração totalmente desenvolvido no Compose que evita soluções de interoperabilidade, como o agrupamento de PlayerView em AndroidView. O app de demonstração usa as classes de detentor de estado da IU do módulo media3-ui-compose e faz uso da biblioteca Compose Material3.

Detentores de estado da interface

Para entender melhor como usar a flexibilidade dos detentores de estado da interface em comparação com os elementos combináveis, leia sobre como o Compose gerencia o estado.

Detentores de estado do botão

Para alguns estados da interface, presumimos que eles provavelmente serão consumidos por elementos combináveis semelhantes a botões.

Estado remember*State Tipo
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState Constante
NextButtonState rememberNextButtonState Constante
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState Menu ou N-Toggle

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

Observe como state não tem informações de tema, como o ícone a ser usado para reproduzir ou pausar. A única responsabilidade dele é transformar o Player no estado da interface.

Você pode misturar e combinar os botões no layout de sua preferência:

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

Detentores de estado de saída visual

PresentationState armazena informações sobre quando a saída de vídeo em um PlayerSurface pode ser mostrada ou deve ser coberta por um elemento de interface de marcador de posição.

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

Aqui, podemos usar presentationState.videoSizeDp para dimensionar a plataforma para a proporção desejada (consulte os documentos do ContentScale para mais tipos) e presentationState.coverSurface para saber quando o momento não é certo para mostrar a plataforma. Nesse caso, você pode posicionar uma cortina opaca sobre a superfície, que vai desaparecer quando a superfície estiver pronta.

Onde estão os fluxos?

Muitos desenvolvedores Android já sabem usar objetos Flow do Kotlin para coletar dados de interface em constante mudança. Por exemplo, você pode estar procurando um fluxo Player.isPlaying que possa ser collect de acordo com o ciclo de vida. Ou algo como Player.eventsFlow para fornecer um Flow<Player.Events> que você possa filter como quiser.

No entanto, o uso de fluxos para o estado da interface Player tem algumas desvantagens. Uma das principais preocupações é a natureza assíncrona da transferência de dados. Queremos garantir a menor latência possível entre um Player.Event e o consumo dele no lado da interface, evitando mostrar elementos da interface que estão fora de sincronia com o Player.

Outros pontos incluem:

  • Um fluxo com todos os Player.Events não adere a um único princípio de responsabilidade. Cada consumidor precisa filtrar os eventos relevantes.
  • Ao criar um fluxo para cada Player.Event, você precisa combiná-los (com combine) para cada elemento da interface. Há um mapeamento de muitos para muitos entre um Player.Event e uma mudança de elemento da interface. O uso de combine pode levar a interface a estados potencialmente ilegais.

Criar estados de interface personalizados

É possível adicionar estados de IU personalizados se os atuais não atenderem às suas necessidades. Confira o código-fonte do estado atual para copiar o padrão. Uma classe de detentor de estado de interface típica faz o seguinte:

  1. Recebe um Player.
  2. Faz a assinatura do Player usando corrotinas. Consulte Player.listen para mais detalhes.
  3. Responde a Player.Events específicos atualizando o estado interno.
  4. Aceita comandos de lógica de negócios que serão transformados em uma atualização Player adequada.
  5. Pode ser criado em vários lugares na árvore da interface e sempre mantém uma visualização consistente do estado do jogador.
  6. Expõe campos State do Compose que podem ser consumidos por um elemento combinável para responder dinamicamente às mudanças.
  7. Vem com uma função remember*State para lembrar a instância entre composições.

O que acontece nos bastidores:

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 reagir à sua própria Player.Events, você pode detectá-la usando Player.listen, que é um suspend fun que permite que você entre no mundo da corrotina e detecte a Player.Events indefinidamente. A implementação do Media3 de vários estados de IU ajuda o desenvolvedor final a não se preocupar em aprender sobre Player.Events.