Intermediate Android Compose - Playback Bar

Ken Ruiz Inoue
Deuk
Published in
9 min readJan 5, 2024

Introduction

Welcome to my latest tutorial! Today, I’m excited to guide you through creating a FloatingPlaybackBar() for a Music App. We will focus on enhancing user interaction by precisely tracking the PLAYING/PAUSED states using dynamic objects like SelectedTrackState.

For those eager to jump straight into the code, it’s available here.

Let’s not delay any further and dive in!

On Today’s Agenda

  • Use the image-loading library Coil to render an image optimally.
  • Implementing the FloatingPlaybackBar() composable.
  • Demonstrating how to integrate FloatingPlaybackBar() in MainActivity.kt, focusing on managing the PLAYING and PAUSED states.

Environment

  • Android Studio Hedgehog | 2023.1.1
  • Compose version: androidx.compose:compose-bom:2023.08.00
  • Pixel 5 API 32 Emulator
  • io.coil-kt:coil-compose:1.4.0

Step 1

As always, create an empty compose project.

Acquire Assets

Download the necessary assets from here and place them in the res/drawable directory of your project.

Setting Up Coil Dependency

To incorporate Coil for image loading, open your build.gradle.kts file in the :app module. Add the following implementation for Coil and then synchronize the project:

...

dependencies {

implementation("io.coil-kt:coil-compose:1.4.0")
...
}

Creating Shared Resources:

To ensure consistency and ease of modification, let’s define shared constants. Note that we are not focusing on UI theme customization here for simplicity, as ComposeTheming offers extensive flexibility.

  • Colors: In ui/constants/Colors.kt, define your color constants:
// Your package...

import androidx.compose.ui.graphics.Color

val PlaybackBarColor = Color(0xFF925700)
val PrimaryWhite = Color(0xFFF3F6F5)
  • Dimensions: For UI dimensions, create ui/constants/DpValues.kt:
// Your package...

import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val SmallDp: Dp = 4.dp
val MediumDp: Dp = 8.dp
val LargeDp: Dp = 16.dp
val FloatingPlaybackBarHeight: Dp = 56.dp
val FloatingPlaybackBarCoverSize: Dp = 48.dp
val FloatingPlaybackBarButtonSize: Dp = 40.dp
val FloatingPlaybackBarButtonIconSize: Dp = 28.dp
  • Text Styles: Lastly, add ui/constants/TextStyles.kt for text styling:
// Your package...

import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp

val FloatingPlaybackBarPrimaryTextStyle= TextStyle(
color = PrimaryWhite,
fontWeight = FontWeight.Bold,
fontSize = 16.sp,
letterSpacing = 0.5.sp
)

val FloatingPlaybackBarSecondaryTextStyle = TextStyle(
color = PrimaryWhite,
fontWeight = FontWeight.SemiBold,
fontSize = 14.sp,
letterSpacing = 0.5.sp
)

Step 2: Defining Data Classes

In this step, we focus on establishing the foundation for managing track information and playback states. We will define some data classes that will be pivotal for our FloatingPlaybackBar() composable.

TrackItemData Class

This class models the essential information of a music track. Create a file named TrackItemData.kt in your data package and define the class as follows:

data class TrackItemData(
val id: Int = -1,
val title: String = "No Title",
val artist: String = "No Artist",
val coverDrawableId: Int = -1
)

This class holds the track’s ID, title, artist, and drawable ID for the album cover.

PlaybackState Class

Next, define an enum class representing the playback states: PLAYING and PAUSED. In a new file, data/PlaybackState.kt include:

enum class PlaybackState {
PLAYING, PAUSED
}

This enum helps in managing the playback state of a music app.

SelectedTrackState Class

Finally, we need a class to represent the currently selected track along with its playback state. Create the data/SelectedTrackState.kt file with the following content:

data class SelectedTrackState(
val track: TrackItemData = TrackItemData(),
val playbackState: PlaybackState = PlaybackState.PAUSED
)

The FloatingPlaybackBar() composable will use an instance of this class to accurately reflect the UI based on the currently selected track and its playback status.

Step 3: Crafting the FloatingPlaybackBar Composable

We are now moving to the exciting part — building the FloatingPlaybackBar() composable! This component will be the heart of a music app's user interface, providing users with an interactive and visually appealing way to control the music playback.

Create the composables/FloatingPlaybackBar.kt file :

// Your package ...

import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import coil.compose.rememberImagePainter
import YOUR_PACKAGE_NAME.R
import YOUR_PACKAGE_NAME.data.PlaybackState
import YOUR_PACKAGE_NAME.data.SelectedTrackState
import YOUR_PACKAGE_NAME.ui.constants.FloatingPlaybackBarButtonIconSize
import YOUR_PACKAGE_NAME.ui.constants.FloatingPlaybackBarButtonSize
import YOUR_PACKAGE_NAME.ui.constants.FloatingPlaybackBarCoverSize
import YOUR_PACKAGE_NAME.ui.constants.FloatingPlaybackBarHeight
import YOUR_PACKAGE_NAME.ui.constants.FloatingPlaybackBarPrimaryTextStyle
import YOUR_PACKAGE_NAME.ui.constants.FloatingPlaybackBarSecondaryTextStyle
import YOUR_PACKAGE_NAME.ui.constants.LargeDp
import YOUR_PACKAGE_NAME.ui.constants.MediumDp
import YOUR_PACKAGE_NAME.ui.constants.PlaybackBarColor
import YOUR_PACKAGE_NAME.ui.constants.PrimaryWhite
import YOUR_PACKAGE_NAME.ui.constants.SmallDp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

@Composable
fun FloatingPlaybackBar(
// 1. Dynamic State Management with Flow
selectedTrackStateFlow: Flow<SelectedTrackState> = flowOf(SelectedTrackState()),
// 2. Interactive Controls through Lambda Functions
onPreviousClicked: () -> Unit = {},
onPlayPauseClicked: () -> Unit = {},
onNextClicked: () -> Unit = {}
) {
// 3. Real-Time UI Updates with State Observing
val selectedTrackState = selectedTrackStateFlow.collectAsState(initial = SelectedTrackState()).value
// 4. Efficient Image Rendering with Coil
val imagePainter = rememberImagePainter(
data =
// 5. Image Handling with Fallbacks
if (selectedTrackState.track.coverDrawableId == -1) null
else selectedTrackState.track.coverDrawableId,
builder = {
fallback(R.drawable.fallback_album_cover)
}
)
Card(
modifier = Modifier
.padding(MediumDp)
.height(FloatingPlaybackBarHeight)
.background(Color.Transparent)
.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = LargeDp),
shape = RoundedCornerShape(MediumDp)
) {
Row(
modifier = Modifier
.fillMaxSize()
.background(PlaybackBarColor)
.padding(SmallDp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(FloatingPlaybackBarCoverSize)
.clip(RoundedCornerShape(MediumDp)),
contentAlignment = Alignment.Center
) {
Image(
painter = imagePainter,
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
}
Column(
modifier = Modifier
.padding(start = LargeDp)
.weight(1f)
) {
Text(
style = FloatingPlaybackBarPrimaryTextStyle,
text = selectedTrackState.track.title,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
style = FloatingPlaybackBarSecondaryTextStyle,
text = selectedTrackState.track.artist,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
// 6. Wrapping Icon inside IconButton
IconButton(
onClick = { onPreviousClicked() },
modifier = Modifier.size(FloatingPlaybackBarButtonSize)
) {
Icon(
painter = painterResource(id = R.drawable.ic_previous),
contentDescription = null,
tint = PrimaryWhite,
modifier = Modifier.size(FloatingPlaybackBarButtonIconSize)
)
}
// 7. Responsive Control Icons
val iconId =
if (selectedTrackState.playbackState == PlaybackState.PLAYING) {
R.drawable.ic_pause
} else {
R.drawable.ic_play
}
IconButton(
onClick = { onPlayPauseClicked() },
modifier = Modifier.size(FloatingPlaybackBarButtonSize)
) {
Icon(
painter = painterResource(id = iconId),
contentDescription = null,
tint = PrimaryWhite,
modifier = Modifier.size(FloatingPlaybackBarButtonIconSize)
)
}
IconButton(
onClick = { onNextClicked() },
modifier = Modifier.size(FloatingPlaybackBarButtonSize)
) {
Icon(
painter = painterResource(id = R.drawable.ic_next),
contentDescription = null,
tint = PrimaryWhite,
modifier = Modifier.size(FloatingPlaybackBarButtonIconSize)
)
}
}
}
}

@Preview
@Composable
fun FloatingPlaybackBarPreview() {
FloatingPlaybackBar()
}
  1. Dynamic State Management with Flow: The FloatingPlaybackBar() composable function receives a Flow of SelectedTrackState, providing a reactive way to update the UI based on the current track state.
  2. Interactive Controls through Lambda Functions: Defines lambda functions for previous, play/pause, and next actions. These abstracted actions allow for flexible implementation of user interactions.
  3. Real-Time UI Updates with State Observing: The selectedTrackStateFlow is collected and observed. This ensures the composable reacts and updates its UI in response to changes in the track state.
  4. Efficient Image Rendering with Coil: Coil is used for image loading, optimizing the rendering of PNG images. This provides an efficient way to display album covers.
  5. Image Handling with Fallbacks: A fallback image is used when no cover image is available for the current track. This ensures a graceful handling of missing images.
  6. Wrapping Icon inside IconButton: The IconButton(), sized by FloatingPlaybackBarButtonSize(40.dp), ensures a touch-friendly area, while the Icon displays an image sized by FloatingPlaybackBarButtonIconSize(28.dp), tinted in PrimaryWhite for visual consistency. This IconButton(){ Icon() }wrapping pattern enhances UI intuitiveness and accessibility.
  7. Responsive Control Icons: The play/pause button icon dynamically changes based on the playback state. This visual cue immediately indicates whether the current track is playing or paused.

If you execute the preview function, you should see the following result:

The preview function showcases the default UI state when the FloatingPlaybackBar() is utilized without specific parameters. This design choice highlights the importance of creating Composables that are flexible and can be easily rendered in various contexts. Such a practice ensures robustness and versatility in your components, facilitating ease of testing and collaboration.

Step 4

Let’s bring our FloatingPlaybackBar() to life within the MainActivity.kt. Update the Activity as follows:

// Your package...

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.sp
import YOUR_PACKAGE_NAME.composable.FloatingPlaybackBar
import YOUR_PACKAGE_NAME.data.PlaybackState
import YOUR_PACKAGE_NAME.data.SelectedTrackState
import YOUR_PACKAGE_NAME.data.TrackItemData
import kotlinx.coroutines.flow.flowOf

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// 1. Simulated Track State
val trackState = remember {
mutableStateOf(
SelectedTrackState(
track = TrackItemData(
title = "Some Title",
artist = "Some Artist",
coverDrawableId = R.drawable.fallback_album_cover
),
playbackState = PlaybackState.PAUSED
)
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
FloatingPlaybackBar(
selectedTrackStateFlow = flowOf(trackState.value),
// 2. Play/Pause Interaction Logic
onPlayPauseClicked = {
trackState.value = trackState.value.copy(
playbackState = if (trackState.value.playbackState == PlaybackState.PLAYING)
PlaybackState.PAUSED else PlaybackState.PLAYING
)
}
)
// 3. Playback Status Display
Text(
text =
if (trackState.value.playbackState == PlaybackState.PLAYING) "Playing"
else "Paused",
fontSize = 20.sp
)
}
}
}
}
  1. Simulated Track State: Establishes a simulated track state to demonstrate the UI’s functionality. It’s essential for visualizing the FloatingPlaybackBar() with various data scenarios, allowing for effective UI testing and development. Feel free to play with different values.
  2. Play/Pause Interaction Logic: This feature is implemented in the onPlayPauseClicked() lambda function. It changes the playback state between PLAYING and PAUSED when the Play/Pause button is clicked. This is a crucial interaction in any music player, allowing the user to control the track's playback.
  3. Playback Status Display: The MainActivity.kt dynamically displays the current playback state in sync with the FloatingPlaybackBar(). This is achieved by observing the trackState and updating the string value in a Text() composable accordingly.

Now, you should be able to toggle between the PLAYING and PAUSED states, accompanied by Text() feedback!

Next Steps

In this installment, we explored the FloatingPlaybackBar() — a sleek and intuitive control panel essential for navigating and controlling the selected track in a music app. What’s exciting is that this composable is part of a larger project: the MusicStreamingDemoApp, where we are piecing together a complete music streaming application from scratch!

MusicStreamingDemoApp

The beauty of this project lies in its modular design. You can create the four main components in any order you prefer, giving you the flexibility to approach the development process in a way that suits your style or needs best.

The Four Key UI Components

PlaybackBar: The control center of the music experience, which you have just built in this tutorial. It allows users to play, pause, and navigate through the music.

TopBar: This is where users select music from a list.

TrackList: A dynamic display showcasing various tracks. It’s an interactive segment where users can browse and choose their next listen. (coming soon!)

BottomBar: Navigation hub of the app. (coming soon!)

After creating the individual components, the last step involves integrating them into the main screen of the music app. This is where you see the harmony of the TopBar, TrackList, PlaybackBar, and BottomBar in action, culminating in a user-friendly and visually appealing music streaming interface. (coming soon!)

For those eager to dive into the most current iteration of the project, the MusicStreamingDemoApp repository awaits your exploration here.

Discover More

Enjoying these tutorials? Dive deeper into modern Android development with my series on intermediate topics:

Wrapping Up

I hope this tutorial has not only equipped you with the skills to craft dynamic and visually appealing UIs but also sparked innovative ideas for your Android projects. The versatility and creative potential of Jetpack Compose, as demonstrated by the FloatingPlaybackBar(), are crucial in crafting feature-rich and intuitive applications.

As we continue to explore the fusion of functionality and design in app development, your engagement and support are greatly appreciated. Feel free to follow me for more insightful tutorials in this series. Your claps and comments help fuel this educational journey! Until our next adventure in Android app innovation, happy coding and embrace the creative process!

Deuk Services: Your Gateway to Leading Android Innovation

Are you looking to boost your business with top-tier Android solutions?Partner with Deuk services and take your projects to unparalleled heights.

--

--