Memulai UI berbasis Compose

Menambahkan dependensi

Library Media3 menyertakan modul UI berbasis Jetpack Compose. Untuk menggunakannya, tambahkan dependensi berikut:

Kotlin

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

Groovy

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

Sebaiknya Anda mengembangkan aplikasi dengan pendekatan Compose-first atau bermigrasi dari penggunaan View.

Aplikasi demo Compose sepenuhnya

Meskipun library media3-ui-compose tidak menyertakan Composable siap pakai (seperti tombol, indikator, gambar, atau dialog), Anda dapat menemukan aplikasi demo yang ditulis sepenuhnya di Compose yang menghindari solusi interoperabilitas seperti membungkus PlayerView di AndroidView. Aplikasi demo menggunakan class pemegang status UI dari modul media3-ui-compose dan memanfaatkan library Compose Material3.

Holder status UI

Untuk lebih memahami cara menggunakan fleksibilitas holder status UI versus composable, baca cara Compose mengelola Status.

Holder status tombol

Untuk beberapa status UI, kami berasumsi bahwa status tersebut kemungkinan besar akan digunakan oleh Composable seperti tombol.

Status remember*State Jenis
PlayPauseButtonState rememberPlayPauseButtonState 2-Toggle
PreviousButtonState rememberPreviousButtonState Konstanta
NextButtonState rememberNextButtonState Konstanta
RepeatButtonState rememberRepeatButtonState 3-Toggle
ShuffleButtonState rememberShuffleButtonState 2-Toggle
PlaybackSpeedState rememberPlaybackSpeedState Menu atau N-Toggle

Contoh penggunaan 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),
    )
  }
}

Perhatikan bagaimana state tidak memiliki informasi tema, seperti ikon yang digunakan untuk memutar atau menjeda. Satu-satunya tanggung jawabnya adalah mengubah Player menjadi status UI.

Kemudian, Anda dapat menggabungkan tombol dalam tata letak pilihan Anda:

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

Holder status output visual

PresentationState menyimpan informasi tentang kapan output video di PlayerSurface dapat ditampilkan atau harus dicakup oleh elemen UI placeholder.

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

Di sini, kita dapat menggunakan kedua presentationState.videoSizeDp untuk menskalakan Platform ke rasio aspek yang diinginkan (lihat dokumen ContentScale untuk jenis lainnya) dan presentationState.coverSurface untuk mengetahui kapan waktu yang tepat untuk menampilkan Platform. Dalam hal ini, Anda dapat memosisikan penutup buram di atas permukaan, yang akan menghilang saat permukaan siap.

Di mana Flow berada?

Banyak developer Android yang terbiasa menggunakan objek Flow Kotlin untuk mengumpulkan data UI yang terus berubah. Misalnya, Anda mungkin mencari flow Player.isPlaying yang dapat Anda collect dengan cara yang mendukung siklus proses. Atau sesuatu seperti Player.eventsFlow untuk memberi Anda Flow<Player.Events> yang dapat Anda filter sesuai keinginan Anda.

Namun, menggunakan alur untuk status UI Player memiliki beberapa kekurangan. Salah satu masalah utama adalah sifat transfer data yang asinkron. Kami ingin memastikan latensi sekecil mungkin antara Player.Event dan konsumsinya di sisi UI, sehingga tidak menampilkan elemen UI yang tidak sinkron dengan Player.

Poin lainnya mencakup:

  • Alur dengan semua Player.Events tidak akan mematuhi prinsip tanggung jawab tunggal, setiap konsumen harus memfilter peristiwa yang relevan.
  • Membuat flow untuk setiap Player.Event akan mengharuskan Anda menggabungkannya (dengan combine) untuk setiap elemen UI. Ada pemetaan many-to-many antara Player.Event dan perubahan elemen UI. Penggunaan combine dapat menyebabkan UI berada dalam status yang berpotensi ilegal.

Membuat status UI kustom

Anda dapat menambahkan status UI kustom jika status yang ada tidak memenuhi kebutuhan Anda. Periksa kode sumber status yang ada untuk menyalin pola. Class holder status UI standar melakukan hal berikut:

  1. Menerima Player.
  2. Berlangganan ke Player menggunakan coroutine. Lihat Player.listen untuk detail selengkapnya.
  3. Merespons Player.Events tertentu dengan memperbarui status internalnya.
  4. Menerima perintah logika bisnis yang akan diubah menjadi pembaruan Player yang sesuai.
  5. Dapat dibuat di beberapa tempat di seluruh hierarki UI dan akan selalu mempertahankan tampilan status Pemutar yang konsisten.
  6. Mengekspos kolom State Compose yang dapat digunakan oleh Composable untuk merespons perubahan secara dinamis.
  7. Dilengkapi dengan fungsi remember*State untuk mengingat instance di antara komposisi.

Yang terjadi di balik layar:

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

Untuk bereaksi terhadap Player.Events Anda sendiri, Anda dapat menangkapnya menggunakan Player.listen yang merupakan suspend fun yang memungkinkan Anda memasuki dunia coroutine dan memproses Player.Events tanpa batas. Penerapan Media3 untuk berbagai status UI membantu developer akhir tidak perlu mempelajari Player.Events.