arrow left
Back to Developer Education

Creating a Nested Scroll Music Player App in Jetpack Compose

Creating a Nested Scroll Music Player App in Jetpack Compose

The nested scroll is one of the technical features one can implement in an app. With Jetpack Compose, we can achieve this by combining scrollable components with different orientations. <!--more--> This approach uses items as children of other items. Alternatively, we can make use of the NestedScroll API offered by Compose.

Goal

In this tutorial, we'll learn how to implement a nested scroll functionality in a simple music player app using the earlier mentioned approach as it is easier to implement, customize, and handle states.

Prerequisites

To follow along with this guide, the reader will need:

  • Android Studio IDE installed on your machine.
  • A good understanding of the Jetpack Compose core concepts.
  • A physical device or an emulator running Android 8 or greater.

Table of contents

Creating the project

Launch the IDE and start a new empty Compose project and fill the parameters as shown below:

New Compose Project

Setting up the project

Changing the Compose and Kotlin versions

Head to your build.gradle(project) file and change compose_version to 1.1.1. This is the latest version at the time of writing this tutorial.

buildscript {
    ext {
        compose_version = '1.1.1'
    }
}

This requires version 1.6.10 of the Kotlin compiler. Update it as shown below and sync the project:

plugins {
    ...
    id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
}

NOTE: Since Compose is evolving so fast, some features used in this tutorial may be changed or expelled in future versions.

Creating the user interface

Create a new package named composables under the root package. This package will hold our composable functions. For simplicity purposes, we're going to build a single screen app made up of the following composables:

The artist item

@Composable
fun ArtistItem() {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        ArtistImage( // cast the Image import to match this name
            painter = painterResource(id = R.drawable.juice_wrld),
            modifier = Modifier
                .padding(all = 8.dp)
                .size(60.dp)
                .clip(CircleShape),
            contentScale = Crop,
            contentDescription = "artist image"
        )
        Text(
            text = "Juice Wrld"
        )
    }
}

The function above displays a circular image and a text below it. They represent the image and name of an artist respectively.

The music item

@Composable
fun MusicItem() {
    Row(
        modifier = Modifier
            .padding(vertical = 4.dp)
            .fillMaxWidth(),
        verticalAlignment = CenterVertically
    ) {
        Icon(
            painter = painterResource(id = R.drawable.ic_audio),
            contentDescription = "audio icon"
        )
        Column(
            horizontalAlignment = Start,
            verticalArrangement = SpaceBetween,
            modifier = Modifier.padding(all = 8.dp)
        ) {
            Text(text = "Song title")
            Text(
                text = "Song artist",
                fontSize = 12.sp,
                color = Color.Gray
            )
        }
    }
}

This item shows a music icon and a title along with the artist of the song. This function can be modified to receive a song object as an input (parameter).

You can get the icons and images from this GitHub repository.

Implementing nested scroll

Switch to MainActivity.kt file and add the following code inside the Surface scope. Alternatively you can paste it directly in the setContent scope.

val state = rememberLazyListState()

LazyColumn(modifier = Modifier.fillMaxSize(fraction = 0.85F), state = state) {
    item {
        Text(
            text = "Artists",
            fontSize = 24.sp,
            fontWeight = Bold
        )
    }
    item {
        LazyRow(modifier = Modifier.fillMaxWidth()) {
            items(count = 10) {
                ArtistItem()
            }
        }
    }
    item {
        Text(
            text = "Songs",
            fontSize = 24.sp,
            fontWeight = Bold,
            modifier = Modifier.padding(vertical = 8.dp)
        )
    }
    items(count = 10) {
        MusicItem()
    }
}

As mentioned earlier, to create a nested scroll, we need to entangle the composables within the body of a scrollable component. This is made possible by the fact that the item and items functions are overloads of the LazyRow and LazyColumn functions that can hold other composables including scrollable ones.

The hierarchy/level of nesting is determined by the number of elements that are passed to the item or items functions. However, to achieve a smooth scrolling effect, it is advisable to avoid nesting too many items.

Running the app

When you run the app at this point, you should see the following:

Nested Scroll

Playing audio in Compose

To play audio in Compose, we need to pass the context and the resource (audio file) to the MediaPlayer constructor.

For more advanced audio controls and customization, you can use ExoPlayer. ExoPlayer is a third-party media playing library for Android. It offers a lot of features that are currently not available in the default MediaPlayer API.

You can learn more about ExoPlayer here.

Providing the audio file

We can provide a media file from different sources such as the local storage of the device or the internet. Similarly, an audio file can be enclosed within the app resources.

Create a raw file within the resources folder and add an audio file of your choice.

Instantiating the MediaPlayer

Here, we need to reference and play the audio file from the raw folder. Create a function that generates an instance of the MediaPlayer as shown below:

@Composable
fun PlaySampleAudio(context: Context) {
    val sampleSong: MediaPlayer by remember {
        mutableStateOf(
            MediaPlayer.create(
                context,
                R.raw.sample_song // your audio file
            )
        )
    } // track the playing state
    var isPlaying by remember {
        mutableStateOf(false)
    }
}

Adding the media control

The following code adds a round-shaped card with a button to play or pause the audio. Paste it in the PlaySampleAudio function.

Card( // Icon and button holder
    modifier = Modifier
        .padding(8.dp)
        .clip(RoundedCornerShape(8.dp))
        .fillMaxWidth()
) {
    // this is similar to the MusicItem properties
    Row(
        modifier = Modifier
            .padding(horizontal = 8.dp)
            .fillMaxWidth(),
            // Align items end to end
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Row(
            modifier = Modifier.padding(vertical = 4.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {
            Icon(
                painter = painterResource(id = R.drawable.ic_audio),
                contentDescription = "audio icon"
            )
            Column(
                horizontalAlignment = Alignment.Start,
                verticalArrangement = Arrangement.SpaceBetween,
                modifier = Modifier.padding(all = 8.dp)
            ) {
                Text(text = "Sample Song")
                Text(
                    text = "Artist",
                    fontSize = 12.sp,
                    color = Color.Gray
                )
            }
        }

        // Add control buttons
        Row(
            modifier = Modifier.padding(vertical = 4.dp),
            verticalAlignment = Alignment.CenterVertically
        ) {

            Icon(
                painter = painterResource(id = R.drawable.ic_prev),
                contentDescription = "back button"
            )
            // check state and set/update the icon
            IconButton(onClick = {
                if (isPlaying) {
                    isPlaying = false
                    sampleSong.pause()
                } else {
                    isPlaying = true
                    sampleSong.start()
                }
            }) {
                Icon(
                    painter = painterResource(
                        id =
                        if (isPlaying) R.drawable.ic_pause else R.drawable.ic_play
                    ),
                    contentDescription = "play/pause button"
                )
            }

            Icon(
                painter = painterResource(id = R.drawable.ic_next),
                contentDescription = "next button"
            )
        }
    }
}

The above code allows us to play or pause the audio based on the value of the isPlaying state.

Running the app

Call the PlaySampleAudio function below the scrollable list and run the app. You should expect it to look like the one below:

Player

What's beyond this tutorial?

This project can be extended to include more features such as listing the songs on the device, indicating the current playing song, showing notifications, and so on. This would require a foreground service implementation for a smoother user interaction and resources consumption.

You can learn more about services in Compose in this guide

Conclusion

In this tutorial, we learned what a nested scroll is and how to implement it using entangled items. We have also covered the basics of loading and playing audio in Compose using the MediaPlayer API. Continue exploring to learn more about playing media in Jetpack Compose.

Happy coding!


Peer Review Contributions by: Eric Gacoki

Published on: Jun 29, 2022
Updated on: Jul 25, 2024
CTA

Start your journey with Cloudzilla

With Cloudzilla, apps freely roam across a global cloud with unbeatable simplicity and cost efficiency