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.6.0")

Groovy

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

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

Aplikasi demo Compose sepenuhnya

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

Holder status UI

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

Holder status tombol

Untuk beberapa status UI, kami membuat asumsi 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 bahwa state tidak memiliki informasi tema, seperti ikon yang akan digunakan untuk memutar atau menjeda. Satu-satunya tanggung jawabnya adalah mengubah Player menjadi status UI.

Kemudian, Anda dapat menggabungkan tombol dalam tata letak preferensi 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 ditutupi 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 presentationState.videoSizeDp untuk menskalakan Platform ke rasio aspek yang diinginkan (lihat dokumen ContentScale untuk jenis lainnya) dan presentationState.coverSurface untuk mengetahui kapan waktunya tidak tepat untuk menampilkan Platform. Dalam hal ini, Anda dapat memosisikan shutter 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.

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

Poin lainnya mencakup:

  • Alur dengan semua Player.Events tidak akan mematuhi satu prinsip tanggung jawab, setiap konsumen harus memfilter peristiwa yang relevan.
  • Membuat alur 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. Harus menggunakan combine dapat menyebabkan UI ke status yang berpotensi ilegal.

Membuat status UI kustom

Anda dapat menambahkan status UI kustom jika status yang ada tidak memenuhi kebutuhan Anda. Lihat 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 mengetahui detail selengkapnya.
  3. Merespons Player.Events tertentu dengan memperbarui status internalnya.
  4. Menerima perintah logika bisnis yang akan diubah menjadi update Player yang sesuai.
  5. Dapat dibuat di beberapa tempat di hierarki UI dan akan selalu mempertahankan tampilan status Pemain 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 waktu. Penerapan Media3 dari berbagai status UI membantu developer akhir untuk tidak perlu mempelajari Player.Events.