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

依存関係を追加する

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

Kotlin

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

Groovy

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

Compose を優先してアプリを開発するか、View の使用から移行することを強くおすすめします。

完全な Compose デモアプリ

media3-ui-compose ライブラリには、すぐに使用できる Composable(ボタン、インジケーター、画像、ダイアログなど)は含まれていませんが、PlayerViewAndroidView でラップするなどの相互運用性ソリューションを回避する、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.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 を使用して Surface を目的のアスペクト比にスケーリングし(その他のタイプについては ContentScale のドキュメントを参照)、presentationState.coverSurface を使用して Surface を表示するタイミングが適切でない場合を把握します。この場合、サーフェスの準備が整うと消える不透明なシャッターをサーフェスの最前面に配置できます。

フローはどこにありますか?

多くの 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 を無期限にリッスンできる suspend fun です。Media3 のさまざまな UI 状態の実装により、エンド デベロッパーは Player.Events の学習を気にすることなく済みます。