
This is a Jetpack Compose music button UI design. It contains 3 states – play, pause, and loading. The loading state is managed by the view model. You can download the source code for free below.
The UI contains OutlinedButton with Icon in it. The icon changes according to the button state. When the song is getting loaded in the background, it shows a CircularProgressIndicator. When the song is paused, it shows play icon. When the song is played, a pause icon is displayed. You have to use the view model to download the song in the background. Instead of downlaoding, I’m setting a delay in the viewModelScope.launch block.
To set the state at the proper time, I declared 3 constants. They are playIcon, loadingBar, and pauseIcon.
The Jetpack Compose music button takes two parameters – context and viewModel. The context is used to display the toast message and the view model is for managing the state. There are two methods – playTheSong() and pauseTheSong(). As the name suggests, play and pause the song in the methods.
The PlayArrow and Pause icons are downloaded from the material icons SDK. If you want to use it, you need to enable proguard because of its large size.
You need to download the latest Android Studio. Jetpack Compose doesn’t work in the older versions.
Related:
Final output:
If the video isn’t working, watch it on the YouTube.
Helpful links to understand the code:
- Jetpack Compose State Using ViewModel
- CircularProgressIndicator with Examples
- Kotlin Higher Order Functions
Here are the Gradle files used in the project.
Here is the Source Code:
MainActivity.kt:
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/*
You can use the following code for commercial purposes with some restrictions.
Read the full license here: https://semicolonspace.com/semicolonspace-license/
For more designs with source code,
visit: https://semicolonspace.com/jetpack-compose-samples/
*/
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BlogPostsTheme(darkTheme = false) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
MusicButton()
}
}
}
}
}
private const val playIcon = 1
private const val loadingBar = 2
private const val pauseIcon = 3
@Composable
fun MusicButton(
viewModel: MyViewModel = androidx.lifecycle.viewmodel.compose.viewModel(),
context: Context = LocalContext.current.applicationContext
) {
// Initially song is not loaded
val songLoaded by viewModel.songLoaded.observeAsState(false)
// This is used to remember the icon of the button
// Its values are playIcon, loadingBar, and pauseIcon
// Initially display the playIcon
var buttonIcon by remember {
mutableStateOf(playIcon)
}
OutlinedButton(
modifier = Modifier
.size(72.dp),
shape = CircleShape,
contentPadding = PaddingValues(12.dp),
elevation = ButtonDefaults.elevation(4.dp),
colors = ButtonDefaults.outlinedButtonColors(
backgroundColor = Color(0xFF35898F)
),
border = BorderStroke(0.dp, Color.Transparent),
onClick = {
if (!songLoaded) {
// If song is NOT loaded
Toast.makeText(context, "Loading...", Toast.LENGTH_SHORT).show()
viewModel.loadTheSong()
// Assign buttonIcon to loadingBar so that progress indicator is displayed
buttonIcon = loadingBar
} else {
// If the song is already loaded
if (buttonIcon == playIcon) {
// If the current icon is play Icon
// change it to pause icon
buttonIcon = pauseIcon
} else if (buttonIcon == pauseIcon) {
// If the current icon is pause icon
// change it to play icon
buttonIcon = playIcon
}
}
}
) {
when (buttonIcon) {
loadingBar -> {
if (songLoaded) {
// If the song is already loaded
// set the pause icon
buttonIcon = pauseIcon
} else {
// If the song is NOT loaded
// show the progress indicator
CircularProgressIndicator(
modifier = Modifier
.fillMaxSize()
.padding(8.dp),
color = Color.White
)
}
}
playIcon -> {
// Set the play icon
SetButtonIcon1(
icon = Icons.Filled.PlayArrow,
iconDescription = "Play Song"
)
// If the song is loaded, pause the actual song
if (songLoaded) {
pauseTheSong(context = context)
}
}
pauseIcon -> {
// Set the pause icon
SetButtonIcon1(Icons.Filled.Pause, iconDescription = "Pause Song")
// If the song is loaded, play the actual song
if (songLoaded) {
playTheSong(context = context)
}
}
}
}
}
private fun playTheSong(context: Context) {
// Here play the song
Log.i("SemicolonSpace", "playTheSong()")
Toast.makeText(context, "Playing....", Toast.LENGTH_SHORT).show()
}
private fun pauseTheSong(context: Context) {
// Here pause the song
Log.i("SemicolonSpace", "pauseTheSong")
Toast.makeText(context, "Paused", Toast.LENGTH_SHORT).show()
}
@Composable
private fun SetButtonIcon1(
icon: ImageVector,
iconDescription: String
) {
Icon(
modifier = Modifier
.fillMaxSize(),
imageVector = icon,
contentDescription = iconDescription,
tint = Color.White
)
}
class MyViewModel : ViewModel() {
// Music Button
val songLoaded = MutableLiveData<Boolean>()
fun loadTheSong() {
viewModelScope.launch {
withContext(Dispatchers.Default) {
// Perform background task to get the song
// After the task is completed, call songLoadedSuccessful()
// Instead of background task, I'm setting 4 seconds delay
delay(4000)
}
songLoadedSuccessful()
}
}
private fun songLoadedSuccessful() {
songLoaded.value = true
}
}
Thanks for the great article. I tried it but failed:
https://stackoverflow.com/questions/74849022/jetpack-compose-lifecycle-viewmodel-unresolved-reference-compose
Maybe the article is missing some information? If the repository linked in the stackoverflow question works for you (i.e. you can execute the program), then just ignore this post, then it might be a local problem.
I added mat-icons & lifecycle as dependency of course, but it still doesn’t work.
You didn’t add view model related dependencies. See this page: http://semicolonspace.com/gradle-files-jetpack-compose/ These are the gradle files used in the project (music button)