Compose ベースの UI のスタートガイド

依存関係を追加する

Media3 ライブラリには、Jetpack Compose ベースの UI モジュールが含まれています。使用するには、次の依存関係を追加します。

Kotlin

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

Groovy

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

Compose を優先してアプリを開発するか、ビューから移行することを強くおすすめします。

完全に Compose で記述されたデモアプリ

media3-ui-compose ライブラリには、ボタン、インジケーター、画像、ダイアログなどのすぐに使えるコンポーザブルは含まれていませんが、AndroidViewPlayerView をラップするなどの相互運用性ソリューションを回避する、Compose で完全に記述されたデモアプリがあります。デモアプリは、media3-ui-compose モジュールの UI 状態ホルダー クラスを使用し、Compose Material3 ライブラリを使用します。

UI 状態ホルダー

UI 状態ホルダーとコンポーザブルの柔軟性をどのように使用できるかについて詳しくは、Compose で状態を管理する方法をご覧ください。

ボタンの状態ホルダー

一部の UI 状態は、ボタンのようなコンポーザブルによって使用される可能性が高いと想定されます。

状態 remember*State タイプ
PlayPauseButtonState rememberPlayPauseButtonState 2 つの切り替えボタン
PreviousButtonState rememberPreviousButtonState 定数
NextButtonState rememberNextButtonState 定数
RepeatButtonState rememberRepeatButtonState 3 トグル
ShuffleButtonState rememberShuffleButtonState 2 つの切り替えボタン
PlaybackSpeedState rememberPlaybackSpeedState メニューまたは N-Toggle

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

state には、再生や一時停止に使用するアイコンなどのテーマ設定情報がないことに注意してください。Player を UI 状態に変換することのみがその責任です。

次に、好みのレイアウトにボタンを組み合わせます。

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

ビジュアル出力の状態ホルダー

PresentationState には、PlayerSurface の動画出力を表示できる場合や、プレースホルダ UI 要素で覆う必要がある場合の情報が保持されます。

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

ここでは、presentationState.videoSizeDp を使用してサーフェスを目的のアスペクト比にスケーリングし(その他のタイプについては ContentScale のドキュメントをご覧ください)、presentationState.coverSurface を使用してサーフェスを表示するタイミングが適切でないかどうかを確認できます。この場合、サーフェスの上に不透明なシャッターを配置し、サーフェスの準備が整うと消えるようにできます。

フローとは

多くの Android デベロッパーは、Kotlin の Flow オブジェクトを使用して、絶えず変化する UI データを収集することに慣れています。たとえば、ライフサイクルに応じて collect できる Player.isPlaying フローを見つけることができます。または、Player.eventsFlow のようなもので、任意の方法で filter できる Flow<Player.Events> を取得します。

ただし、Player UI 状態にフローを使用するにはいくつかのデメリットがあります。主な懸念事項の 1 つは、データ転送の非同期性です。Player.Event と UI 側での消費の間にできるだけレイテンシを少なくし、Player と同期していない UI 要素が表示されないようにする必要があります。

その他のポイントは次のとおりです。

  • すべての Player.Events を含むフローでは、単一責任の原則に準拠しないため、各コンシューマが関連するイベントを除外する必要があります。
  • Player.Event ごとにフローを作成する場合は、UI 要素ごとにそれらを組み合わせる(combine を使用して)必要があります。Player.Event と UI 要素の変更の間には多対多のマッピングがあります。combine を使用すると、UI が違法な状態になる可能性があります。

カスタム UI 状態を作成する

既存の UI 状態がニーズを満たさない場合は、カスタム UI 状態を追加できます。既存の状態のソースコードをチェックアウトして、パターンをコピーします。一般的な UI 状態ホルダー クラスは、次のことを行います。

  1. Player を受け取ります。
  2. コルーチンを使用して Player に登録します。詳細については、Player.listen をご覧ください。
  3. 特定の Player.Events に応答して内部状態を更新します。
  4. 適切な Player 更新に変換されるビジネス ロジック コマンドを受け入れます。
  5. UI ツリー内の複数の場所に作成でき、プレーヤーの状態の一貫したビューを常に維持します。
  6. Compose State フィールドを公開します。このフィールドは、コンポーザブルで使用して変更に動的に応答できます。
  7. コンポジション間のインスタンスを記憶する remember*State 関数が付属しています。

具体的な仕組みを説明しましょう。

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

独自の Player.Events に反応するには、Player.listen を使用して Player.Events をキャッチします。これは、コルーチンの世界に入り、Player.Events を無期限にリッスンできる suspend fun です。さまざまな UI 状態の Media3 実装により、エンドデベロッパーは Player.Events の学習に煩わされることがなくなります。