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 우선 방식으로 앱을 개발하거나 뷰 사용에서 이전하는 것이 좋습니다.

완전 Compose 데모 앱

media3-ui-compose 라이브러리에는 기본적으로 컴포저블 (예: 버튼, 표시기, 이미지 또는 대화상자)이 포함되어 있지 않지만 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)
}

시각적 출력 상태 홀더

PresentationStatePlayerSurface의 동영상 출력을 표시할 수 있는 시점 또는 자리표시자 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 흐름을 찾고 있을 수 있습니다. 또는 원하는 방식으로 filter할 수 있는 Flow<Player.Events>을 제공하는 Player.eventsFlow와 같은 항목을 사용할 수 있습니다.

하지만 Player UI 상태에 흐름을 사용하면 몇 가지 단점이 있습니다. 주요 우려사항 중 하나는 데이터 전송의 비동기적 특성입니다. 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.Events를 무기한 수신할 수 있는 suspend funPlayer.listen를 사용하여 이를 포착하면 됩니다. 다양한 UI 상태의 Media3 구현은 최종 개발자가 Player.Events에 대해 학습하지 않아도 되도록 지원합니다.