依存関係を追加する
Media3 ライブラリには、Jetpack Compose ベースの UI モジュールが含まれています。これを使用するには、次の依存関係を追加します。
Kotlin
implementation("androidx.media3:media3-ui-compose:1.8.0")
Groovy
implementation "androidx.media3:media3-ui-compose:1.8.0"
Compose を優先してアプリを開発するか、ビューの使用から移行することを強くおすすめします。
完全な Compose デモアプリ
media3-ui-compose ライブラリには、すぐに使用できる Composable(ボタン、インジケーター、画像、ダイアログなど)は含まれていませんが、PlayerView を AndroidView でラップするなどの相互運用性ソリューションを回避する、Compose で完全に記述されたデモアプリがあります。このデモアプリは、media3-ui-compose モジュールの UI 状態ホルダー クラスを利用し、Compose Material3 ライブラリを使用しています。
UI 状態ホルダー
UI 状態ホルダーとコンポーザブルの柔軟性をどのように活用できるかを理解するには、Compose がどのように状態を管理するかをご覧ください。
ボタンの状態ホルダー
一部の UI 状態については、ボタンのようなコンポーザブルで消費される可能性が高いと想定しています。
| 状態 | remember*State | タイプ | 
|---|---|---|
| PlayPauseButtonState | rememberPlayPauseButtonState | 2-Toggle | 
| PreviousButtonState | rememberPreviousButtonState | 定数 | 
| NextButtonState | rememberNextButtonState | 定数 | 
| RepeatButtonState | rememberRepeatButtonState | 3-Toggle | 
| ShuffleButtonState | rememberShuffleButtonState | 2-Toggle | 
| 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.resizeWithContentScale(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 を使用して Surface を目的のアスペクト比にスケーリングし(その他のタイプについては ContentScale のドキュメントを参照)、presentationState.coverSurface を使用して Surface を表示するタイミングが適切でない場合を把握します。この場合、サーフェスの準備が整うと消える不透明なシャッターをサーフェスの上に配置できます。
Flow はどこにありますか?
多くの 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 状態ホルダー クラスは次の処理を行います。
- Playerを受け取ります。
- コルーチンを使用して Playerに登録します。詳細については、Player.listenをご覧ください。
- 特定の Player.Eventsに応答して、内部状態を更新します。
- 適切な Player更新に変換されるビジネス ロジック コマンドを受け入れます。
- UI ツリーの複数の場所で作成でき、常にプレーヤーの状態の一貫したビューを維持します。
- コンポーザブルで消費して変更に動的に対応できる Compose Stateフィールドを公開します。
- コンポジション間でインスタンスを記憶するための 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 を無期限にリッスンできる suspend fun です。Media3 のさまざまな UI 状態の実装により、エンド デベロッパーは Player.Events について学習する必要がなくなります。
