Skip to content

Build a simple media player app

This guide will walk you through on how to build a very simple media player app for Wear OS, capable of playing a media which is hosted on the internet.

This guide assumes that you are familiar with:

  • How to create Wear OS projects in Android Studio;
  • Kotlin programming language;
  • Jetpack Compose;

Display a PlayerScreen

1 - Add dependency

Create a new project from Android Studio by choosing "Basic Wear App Without Associated Tiles" from "Wear OS" templates. Add dependency on media-ui to your project’s build.gradle:

implementation "com.google.android.horologist:horologist-media-ui:$horologist_version"

2 - Add PlayerScreen

Add the following code to your Activity’s onCreate function:

setContent {
    PlayerScreen(
        mediaDisplay = {
            TextMediaDisplay(
                title = "Song name",
                subtitle = "Artist name"
            )
        },
        controlButtons = {
            PodcastControlButtons(
                onPlayButtonClick = { },
                onPauseButtonClick = { },
                playPauseButtonEnabled = true,
                playing = false,
                onSeekBackButtonClick = { },
                seekBackButtonEnabled = true,
                onSeekForwardButtonClick = { },
                seekForwardButtonEnabled = true,
            )
        },
        buttons = { }
    )
}

This code is displaying PlayerScreen on the app. PlayerScreen is a full screen composable that contains slots parameters to pass the contents to be displayed for media display, control buttons and more.

In this sample, we are using the UI components TextMediaDisplay and PodcastControlButtons, provided by the UI library, as values to parameters of PlayerScreen.

Result

Run the app and you should see the following screen:

None of the controls are working, as they were not implemented yet.

Make the screen functional

1 - Add dependencies

Add the following dependencies to your project’s build.gradle:

implementation "com.google.android.horologist:horologist-media-data:$horologist_version"
implementation "com.google.android.horologist:horologist-audio-ui:$horologist_version"
implementation("androidx.media3:media3-exoplayer:$media3_version")

2 - Add ViewModel

Add a ViewModel extending PlayerViewModel, providing an instance of PlayerRepositoryImpl:

class MyViewModel(
    player: Player,
    playerRepository: PlayerRepositoryImpl = PlayerRepositoryImpl()
) : PlayerViewModel(playerRepository) {}

3 - Add init block

Add the following init block to the ViewModel to connect the Player to the PlayerRepository, set a media and update the position of the player every second:

init {
    viewModelScope.launch {
        playerRepository.connect(player) {}

        playerRepository.setMedia(
            Media(
                id = "wake_up_02",
                uri = "https://storage.googleapis.com/uamp/The_Kyoto_Connection_-_Wake_Up/02_-_Geisha.mp3",
                title = "Geisha",
                artist = "The Kyoto Connection"
            )
        )
    }
}

4 - Create an instance of the ViewModel

Change your Activity’s onCreate function to:

@SuppressLint("UnsafeOptInUsageError")
val player = ExoPlayer.Builder(this)
    .setSeekForwardIncrementMs(5000L)
    .setSeekBackIncrementMs(5000L)
    .build()
// ViewModels should NOT be created here like this
val viewModel = MyViewModel(player)
val volumeViewModel = createVolumeViewModel()

setContent {
    PlayerScreen(
        playerViewModel = viewModel,
        volumeViewModel = volumeViewModel,
        mediaDisplay = { playerUiState: PlayerUiState ->
          DefaultMediaInfoDisplay(playerUiState)
        },
        controlButtons = { playerUIController: PlayerUiController,
                           playerUiState: PlayerUiState ->
          PodcastControlButtons(
                  playerController = playerUIController,
                  playerUiState = playerUiState
          )
        },
        buttons = { }
    )
}

Add createVolumeViewModel function to create a VolumeViewModel:

fun createVolumeViewModel(): VolumeViewModel {
    val audioRepository = SystemAudioRepository.fromContext(application)
    val vibrator: Vibrator = application.getSystemService(Vibrator::class.java)
    return VolumeViewModel(audioRepository, audioRepository, onCleared = {
        audioRepository.close()
    }, vibrator)
}

We are creating an instance of ExoPlayer, passing it to the ViewModel.

Then for the PlayerScreen slots we are using:

  • the DefaultMediaDisplay component, which accepts a MediaUiModel instance as parameter;
  • the stateful version of PodcastControlButtons, which accepts instances of PlayerViewModel and PlayerUiStateas parameters to hook the controls with the ViewModel;

Result

Run the app again and this time, play with the screen controls as the app should be able to play, pause, and seek the media now: