Scroll to top
Final product imageFinal product imageFinal product image
What You'll Be Creating

A good approach to becoming proficient in a new programming language or library is to try and create something useful with it. In my tutorial on simplifying Android development with Anko, I introduced you to Anko's domain-specific language and helper functions. Although I'm sure you found them impressive, you might still be apprehensive about using them in large and complex apps, since they are so different from traditional Android classes and methods.

So today, we're going to use Kotlin and Anko to create a music player app for Android, one that can automatically pick and play random songs from the user's device. Its reasonably complex user interface, which will have several different widgets interacting with each other, should help you gain a better understanding of how Anko apps work.

Prerequisites

To be able to follow this step-by-step tutorial, you'll need:

  • the latest version of Android Studio
  • a phone or tablet running Android 5.0 or higher
  • and a few MP3 albums

If you haven't done so already, do read the following tutorial before proceeding:

1. Creating a New Project

Launch Android Studio and press the Start a new Android Studio project button to start the project creation wizard. In the next screen, give your app a name and make sure that the Include Kotlin support field is checked.

Project creation wizardProject creation wizardProject creation wizard

Next, target API level 21 or higher and choose the Empty Activity template. Because we won't be needing any layout XML files, make sure you deselect the Generate Layout File field.

Activity configuration screenActivity configuration screenActivity configuration screen

Finally, press Finish to create the project.

2. Adding Dependencies

To add Anko to the project, add the following implementation dependency in the build.gradle file of the app module:

1
implementation 'org.jetbrains.anko:anko:0.10.1'

We'll be using Kotlin's coroutines to perform a few operations asynchronously, so add a dependency for it next.

1
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.19.3'

Our music player, because it plays songs randomly, needs a list of all songs that are available on the device. In order to avoid implementing the logic for creating such a list, add DroidMelody, a library I created specifically for this tutorial, as the last dependency. Because it's approved and published by jcenter, Android Studio's default repository, adding it is no different from adding any other dependency.

1
implementation 'com.progur.droidmelody:droidmelody:1.0.2'

Additionally, we'll be needing a few media-related icons, so open the Vector Asset Studio next. Inside it, navigate to the AV category and choose the icons with the play, pause, and shuffle symbols.

AV section of the Vector Asset StudioAV section of the Vector Asset StudioAV section of the Vector Asset Studio

At this point, the following files should be present in the res/drawable folder of your project:

  • ic_play_arrow_black_24dp.xml
  • ic_pause_black_24dp.xml
  • ic_shuffle_black_24dp.xml

3. Requesting Permissions

Most users store their songs on external storage media. Therefore, on devices running Android Marshmallow or higher, we will need to explicitly request the READ_EXTERNAL_STORAGE permission at run time.

Before you request the permission, however, you must check if the user has already granted it. You can do so by calling the ContextCompat.checkSelfPermission() method inside the onCreate() method of your activity. If the permission has not been granted, you can ask for it by calling the ActivityCompat.requestPermissions() method.

Accordingly, add the following code:

1
if(ContextCompat.checkSelfPermission(this, 
2
                Manifest.permission.READ_EXTERNAL_STORAGE)
3
                != PackageManager.PERMISSION_GRANTED) {
4
    
5
    // Ask for the permission

6
    ActivityCompat.requestPermissions(this,
7
            arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
8
            0)
9
10
} else {
11
    // Start creating the user interface

12
    createPlayer()
13
}

Note that the above code calls a method named createPlayer() if the permission has been granted already. We'll be creating that method in the next step.

After asking for the permission, you must override the onRequestPermissionsResult() method of the activity to determine if the user has accepted your request. If they did, you must again call the createPlayer() method. If they didn't, display an error message using Anko's longToast() helper and close the app.

1
override fun onRequestPermissionsResult(requestCode: Int,
2
                            permissions: Array<out String>,
3
                            grantResults: IntArray) {
4
    super.onRequestPermissionsResult(requestCode, 
5
                            permissions, grantResults)
6
7
    if(grantResults[0] == PackageManager.PERMISSION_GRANTED) {
8
        createPlayer()
9
    } else {
10
        longToast("Permission not granted. Shutting down.")
11
        finish()
12
    }
13
}

4. Fetching All Songs

It's now time to define the createPlayer() method, which will be responsible for both the looks and the functionality of our app.

1
private fun createPlayer() {
2
    // More code here

3
}

Inside the method, the first thing you need to do is generate a list of all the songs available on the device. Because this is, as you might expect, a potentially long-running operation, you can launch it in a new coroutine using the async() function.

Inside the coroutine, create a new instance of DroidMelody's SongFinder class and call its prepare() method, which uses your activity's content resolver to actually find all the songs and place them in a list named allSongs. Once the method completes, you can simply return allSongs from the coroutine. The following code shows you how:

1
val songsJob = async {
2
    val songFinder = SongFinder(contentResolver)
3
    songFinder.prepare()
4
    songFinder.allSongs
5
}

The above coroutine runs asynchronously. To wait for its result, you must call the await() method inside another coroutine created using the launch() function. Because we'll be using the result to create our app's user interface, the new coroutine should run on the UI thread. This is specified by passing kotlinx.coroutines.experimental.android.UI as an argument to launch().

1
launch(kotlinx.coroutines.experimental.android.UI) {
2
    val songs = songsJob.await()
3
4
    // More code here

5
}

You now have a list of Song objects. Each Song object will have several important details about the song it references, such as its title, artist, album art, and URI.

5. Creating an Anko Component

By default, Anko's DSL is directly available only inside the onCreate() method of an activity. To be able to use it inside the createPlayer() method, you can either depend on the UI() function or create a new Anko component. In this tutorial, we'll go with the latter approach because it is more reusable.

To create a new Anko component, you must extend the abstract AnkoComponent class and override its createView() method, inside which you will have access to the DSL.

1
val playerUI = object:AnkoComponent<MainActivity> {
2
    override fun createView(ui: AnkoContext<MainActivity>) 
3
        = with(ui) {
4
            // DSL code here

5
        }
6
}

6. Defining the User Interface

Because our app is a random music player—and not one that can work with playlists—it will have a slightly unconventional user interface. Here are the visible widgets it will contain:

  • an ImageView widget to display the currently playing song's album art
  • an ImageButton widget that allows the user to pause or resume the song
  • an ImageButton widget that allows the user to pick another random song
  • a TextView widget to display the song's title
  • a TextView widget to display the name of the song's artist

Accordingly, add the following fields to the Anko Component:

1
var albumArt: ImageView? = null
2
3
var playButton: ImageButton? = null
4
var shuffleButton:ImageButton? = null
5
6
var songTitle: TextView? = null
7
var songArtist: TextView? = null

Additionally, we'll need a RelativeLayout and a couple of LinearLayout containers to position the above widgets and to establish relationships between them. The following diagram shows the view hierarchy we will be creating next:

View hierarchy of the music playerView hierarchy of the music playerView hierarchy of the music player

Because the RelativeLayout widget is at the root of the view hierarchy, you must create it first by adding the following code inside the createView() method of the Anko component:

1
relativeLayout {
2
    backgroundColor = Color.BLACK
3
4
    // More code here

5
}

Note that we've used the backgroundColor property of the widget to give it a black color. We'll be using several such properties throughout this tutorial to make our app look better. You're free to change their values to match your preferences.

Inside the RelativeLayout widget, add the ImageView widget for the album art. It should take up all the space available on the screen, so use the lparams() method after adding it and pass the matchParent constant to it twice, once for the width and once for the height. Here's how:

1
albumArt = imageView {
2
    scaleType = ImageView.ScaleType.FIT_CENTER
3
}.lparams(matchParent, matchParent)

The lparams() method, as its name suggests, allows you to specify the layout parameters that should be associated with a widget. Using it, you can quickly specify details such as the margins a widget should have, its dimensions, and its position.

Next, create a LinearLayout widget with a vertical orientation by calling the verticalLayout() function and add the TextView widgets to it. The layout must be placed at the bottom of the screen, so you must call the alignParentBottom() function while specifying its layout parameters.

1
verticalLayout {
2
    backgroundColor = Color.parseColor("#99000000")
3
4
    songTitle = textView {
5
        textColor = Color.WHITE
6
        typeface = Typeface.DEFAULT_BOLD
7
        textSize = 18f
8
    }
9
10
    songArtist = textView {
11
        textColor = Color.WHITE
12
    }
13
14
    // More code here

15
16
}.lparams(matchParent, wrapContent) {
17
    alignParentBottom()
18
}

Similarly, create the LinearLayout widget with a horizontal orientation by calling the linearLayout() function, and add the two ImageButton widgets to it. Make sure you use the imageResource property of the buttons to specify the icons they should display. The following code shows you how:

1
linearLayout {
2
3
    playButton = imageButton {
4
        imageResource = R.drawable.ic_play_arrow_black_24dp
5
        onClick {
6
            playOrPause()
7
        }
8
    }.lparams(0, wrapContent, 0.5f)
9
    
10
    shuffleButton = imageButton {
11
        imageResource = R.drawable.ic_shuffle_black_24dp
12
        onClick {
13
            playRandom()
14
        }
15
    }.lparams(0, wrapContent, 0.5f)
16
    
17
}.lparams(matchParent, wrapContent) {
18
    topMargin = dip(5)
19
}

You can see that the above code has click event handlers for both the ImageButton widgets. Inside the handlers, there are calls to two intuitively named methods: playOrPause() and playRandom(). We'll create them in the next few steps.

At this point, we've finished defining the looks of our app. 

7. Playing Songs

Our app is still incapable of actually playing any music. Let's fix that by creating the playRandom() method.

1
fun playRandom() {
2
    // More code here

3
}

We'll be using an instance of the MediaPlayer class to play the music. It is a rather expensive resource, and should be released when the user closes the app. Therefore, it must be defined as a field of the activity—and not the Anko component—and released inside the onDestroy() method of the activity.

1
private var mediaPlayer: MediaPlayer? = null
2
3
override fun onDestroy() {
4
    mediaPlayer?.release()
5
    super.onDestroy()
6
}

Inside the playRandom() method, we can now pick a random song from the list of songs we generated earlier by simply shuffling the list and picking the first element. This approach is not very efficient, but it is very concise.

1
Collections.shuffle(songs)
2
val song = songs[0]

You can now initialize the media player with the URI of the newly chosen song. Additionally, use the setOnCompletionListener() method to make sure that a new random song starts playing as soon as the current song completes.

1
mediaPlayer?.reset()
2
mediaPlayer = MediaPlayer.create(ctx, song.uri)
3
mediaPlayer?.setOnCompletionListener {
4
    playRandom()
5
}

The contents of the ImageView and TextView widgets too must be updated to display the details associated with the new song.

1
albumArt?.imageURI = song.albumArt
2
songTitle?.text = song.title
3
songArtist?.text = song.artist

Finally, to actually start playing the song, you can call the start() method of the media player. Now would also be the right time to update the ImageButton widget to change the "play" icon to a "pause" icon.

1
mediaPlayer?.start()
2
playButton?.imageResource = R.drawable.ic_pause_black_24dp

8. Pausing and Resuming

In an earlier step, we called a method named playOrPause() inside the click-handler of one of the ImageButton widgets. Define it as another method of the Anko component.

1
fun playOrPause() {
2
    // More code here

3
}

The logic you need to implement inside this method should be fairly obvious. If the media player is already playing a song, which you can check by using the isPlaying property, call its pause() method and display the "play" icon in the ImageButton. Otherwise, call the start() method and display the "pause" icon.

1
val songPlaying: Boolean? = mediaPlayer?.isPlaying
2
3
if(songPlaying == true) {
4
    mediaPlayer?.pause()
5
    playButton?.imageResource = R.drawable.ic_play_arrow_black_24dp
6
} else {
7
    mediaPlayer?.start()
8
    playButton?.imageResource = R.drawable.ic_pause_black_24dp
9
}

Our Anko component is now ready.

9. Displaying the Anko Component

Just creating an Anko component is not enough. You must also make sure you render it by calling its setContentView() method and passing an activity to it. For now, you can pass the main activity to it.

Optionally, if you want the app to start playing a song as soon as the user opens it, you can call the playRandom() method again now.

1
playerUI.setContentView(this@MainActivity)
2
playerUI.playRandom()

Our app is ready. If you run it on a device that has one or more MP3 files with properly formatted ID3 tags and embedded album art, you should see something similar to this:

A screenshot of the appA screenshot of the appA screenshot of the app

Conclusion

In this tutorial, you learned how to create a music player app with a complex view hierarchy using just Anko and Kotlin. While doing so, you worked with several advanced features of both, such as coroutines and layout parameter helpers. You also learned how to make Anko code more modular and reusable by creating Anko components.

Feel free to add more functionality to the app or modify its looks to give it a personal touch!

And while you're here, check out some of our other posts on Kotlin and coding Android apps!

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.