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 라이브러리에는 즉시 사용 가능한 컴포저블 (예: 버튼, 표시기, 이미지, 대화상자)이 포함되어 있지 않지만 AndroidView에서 PlayerView를 래핑하는 것과 같은 상호 운용성 솔루션을 피하는 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 상태로 변환하는 것만이 Compose 컴포저블의 유일한 책임입니다.

그런 다음 원하는 레이아웃에서 버튼을 조합할 수 있습니다.

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를 사용하여 노출 영역을 원하는 가로세로 비율로 조정하고 (자세한 유형은 ContentScale 문서 참고) presentationState.coverSurface를 사용하여 노출 영역을 표시하기에 적절하지 않은 시기를 파악할 수 있습니다. 이 경우 노출 영역 위에 불투명한 셔터를 배치할 수 있으며, 노출 영역이 준비되면 셔터가 사라집니다.

흐름은 어디에 있나요?

많은 Android 개발자는 Kotlin Flow 객체를 사용하여 끊임없이 변화하는 UI 데이터를 수집하는 방법을 잘 알고 있습니다. 예를 들어 수명 주기에 맞게 collect할 수 있는 Player.isPlaying 흐름을 찾고 있을 수 있습니다. 또는 Player.eventsFlow와 같이 원하는 방식으로 filter할 수 있는 Flow<Player.Events>를 제공합니다.

그러나 Player UI 상태에 흐름을 사용하면 몇 가지 단점이 있습니다. 주요 우려사항 중 하나는 데이터 전송의 비동기 특성입니다. Player.Event와 UI 측의 소비 간에 최대한 지연 시간을 줄여 Player와 동기화되지 않은 UI 요소가 표시되지 않도록 해야 합니다.

기타 사항은 다음과 같습니다.

  • 모든 Player.Events가 있는 흐름은 단일 책임 원칙을 준수하지 않으므로 각 소비자가 관련 이벤트를 필터링해야 합니다.
  • Player.Event의 흐름을 만들려면 각 UI 요소에 대해 Player.Event를 결합해야 합니다(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를 사용하여 Player.Events를 포착하면 됩니다. 다양한 UI 상태의 Media3 구현은 최종 개발자가 Player.Events에 관해 알아야 하는 부담을 덜어줍니다.