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.
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.
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:
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.
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.
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.
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:
if(ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // Ask for the permission ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), 0) } else { // Start creating the user interface createPlayer() }
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.
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if(grantResults[0] == PackageManager.PERMISSION_GRANTED) { createPlayer() } else { longToast("Permission not granted. Shutting down.") finish() } }
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.
private fun createPlayer() { // More code here }
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:
val songsJob = async { val songFinder = SongFinder(contentResolver) songFinder.prepare() songFinder.allSongs }
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()
.
launch(kotlinx.coroutines.experimental.android.UI) { val songs = songsJob.await() // More code here }
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.
val playerUI = object:AnkoComponent<MainActivity> { override fun createView(ui: AnkoContext<MainActivity>) = with(ui) { // DSL code here } }
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:
var albumArt: ImageView? = null var playButton: ImageButton? = null var shuffleButton:ImageButton? = null var songTitle: TextView? = null 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:
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:
relativeLayout { backgroundColor = Color.BLACK // More code here }
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:
albumArt = imageView { scaleType = ImageView.ScaleType.FIT_CENTER }.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.
verticalLayout { backgroundColor = Color.parseColor("#99000000") songTitle = textView { textColor = Color.WHITE typeface = Typeface.DEFAULT_BOLD textSize = 18f } songArtist = textView { textColor = Color.WHITE } // More code here }.lparams(matchParent, wrapContent) { alignParentBottom() }
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:
linearLayout { playButton = imageButton { imageResource = R.drawable.ic_play_arrow_black_24dp onClick { playOrPause() } }.lparams(0, wrapContent, 0.5f) shuffleButton = imageButton { imageResource = R.drawable.ic_shuffle_black_24dp onClick { playRandom() } }.lparams(0, wrapContent, 0.5f) }.lparams(matchParent, wrapContent) { topMargin = dip(5) }
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.
fun playRandom() { // More code here }
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.
private var mediaPlayer: MediaPlayer? = null override fun onDestroy() { mediaPlayer?.release() super.onDestroy() }
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.
Collections.shuffle(songs) 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.
mediaPlayer?.reset() mediaPlayer = MediaPlayer.create(ctx, song.uri) mediaPlayer?.setOnCompletionListener { playRandom() }
The contents of the ImageView
and TextView
widgets too must be updated to display the details associated with the new song.
albumArt?.imageURI = song.albumArt songTitle?.text = song.title 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.
mediaPlayer?.start() 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.
fun playOrPause() { // More code here }
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.
val songPlaying: Boolean? = mediaPlayer?.isPlaying if(songPlaying == true) { mediaPlayer?.pause() playButton?.imageResource = R.drawable.ic_play_arrow_black_24dp } else { mediaPlayer?.start() playButton?.imageResource = R.drawable.ic_pause_black_24dp }
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.
playerUI.setContentView(this@MainActivity) 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:
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!
- Android SDKQuick Tip: Write Cleaner Code With Kotlin SAM Conversions
- Android SDKJava vs. Kotlin: Should You Be Using Kotlin for Android Development?
- Android SDKIntroduction to Android Architecture Components
- Android SDKWhat Are Android Instant Apps?
- Android SDKHow to Create an Android Chat App Using Firebase