Improving Simple Player: Adding a song repository – Part 2

Improving Simple Player: Adding a song repository – Part 2
  • Post Author:

The initial implementation of Simple Player had a basic architecture, an architecture that would result to spaghetti and hard to maintain code when more features are added as the application grows. I had no plans of adding more features to the app beyond what it is in my previous blog however, have changed my mind. I want to continue working on it and add more features and probably complicate the simple app to whatever end.

Changes will be added gradually as the app grows, in this post I’ll start by separating the songs fetching code from the MainFragment and putting it in a separate class (a repository) i.e adopting single responsibility principle.

Adding SongRepository

The SongRepository class will have the responsibility of getting songs from local storage and presenting it to the MainFragment. The MainFragment will then display the list of songs only, unlike the previous implementation where it had both responsibilities.

class SongRepository  constructor(private val context: Context) {

    var selection = MediaStore.Audio.Media.IS_MUSIC + " != 0"

    private val projection = arrayOf(
        MediaStore.Audio.Media._ID,
        MediaStore.Audio.Media.ARTIST,
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.DISPLAY_NAME,
        MediaStore.Audio.Media.ALBUM,
        MediaStore.Audio.Media.ALBUM_ID

    )

    private fun getColumnIndex(cursor: Cursor, columnName: String): Int{
        return cursor.getColumnIndex(columnName)
    }

    fun getSongs(): LiveData<List<Song>>{

        val songsLiveData = MutableLiveData<List<Song>>()
        val songList =  mutableListOf<Song>()

        //Get songs from provider
        context.contentResolver.query(
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
            projection,
            selection,
            null,
            "${MediaStore.Video.Media.DISPLAY_NAME} ASC" //Sort in alphabetical order based on display name.
        ).use {cursor ->
            if (cursor?.moveToFirst() == true) {
                do {
                    val id = 
                        cursor.getLong(getColumnIndex(cursor,MediaStore.Audio.Media._ID))
                    val title = 
                        cursor.getString(getColumnIndex(cursor,MediaStore.Audio.Media.TITLE))
                    val artist = 
                        cursor.getString(getColumnIndex(cursor,MediaStore.Audio.Media.ARTIST))
                    val album = 
                        cursor.getString(getColumnIndex(cursor,MediaStore.Audio.Media.ALBUM))
                    val albumId = 
                        cursor.getLong(getColumnIndex(cursor,MediaStore.Audio.Media.ALBUM_ID))
                    val sArtworkUri = 
                        Uri.parse("content://media/external/audio/albumart")
                    val albumArtUri = ContentUris.withAppendedId(sArtworkUri, albumId)
                    songList.add(
                        Song(
                            id = id,
                            title = title,
                            artUri = albumArtUri,
                            album = album,
                            artist = artist
                        )
                    )
                } while (cursor.moveToNext())
            }
        }

        return songsLiveData.apply {
            value = songList
        }

    }

    companion object{
        private var instance: SongRepository? = null

        fun getInstance(context: Context) =
            instance ?: synchronized(this) {
                instance ?: SongRepository(context)
                    .also { instance = it }
            }
    }
}

Song data class to hold song information to be presented to the MainFragment for displaying.

data class Song(
    val id: Long,
    val title: String,
    val artUri: Uri,
    val album: String,
    val artist: String
)

The new MainFragment

MainFragment without code to fetch songs from media provider. It’s cleaner and leaner. SongRepository class is accessed via a singleton.

class MainFragment : Fragment(R.layout.fragment_main), OnSongClickListener {

    private val PERMISSIONS_REQUEST_READ_STORAGE: Int = 1200


    private var curFilter = ""
    private lateinit var mAdapter: AudioListAdapter
    private lateinit var musicManager: MusicManager


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)


    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>, grantResults: IntArray
    ) {
        when (requestCode) {
            PERMISSIONS_REQUEST_READ_STORAGE -> {
                if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
                    loadSongs()
                }
                return
            }
            else -> {
                // Ignore all other requests.
            }
        }
    }

    private fun askPermission(){
        if (ContextCompat.checkSelfPermission(
                requireActivity(),
                Manifest.permission.READ_EXTERNAL_STORAGE
            )
            != PackageManager.PERMISSION_GRANTED
        ) {

            // Permission is not granted
            // Should we show an explanation?
            if (ActivityCompat.shouldShowRequestPermissionRationale(
                    requireActivity(),
                    Manifest.permission.READ_EXTERNAL_STORAGE
                )
            ) {

            } else {
                ActivityCompat.requestPermissions(
                    requireActivity(),
                    arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
                    PERMISSIONS_REQUEST_READ_STORAGE
                )

            }
        } else {
            loadSongs()
        }

    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        askPermission()

        mAdapter = AudioListAdapter(view.context, this)

        with(list) {
            adapter = mAdapter
            list.layoutManager = LinearLayoutManager(view.context)
        }

        musicManager = MusicManager(requireContext(), player_control_view)
    }

    private fun loadSongs() {
        SongRepository.getInstance(requireContext()).getSongs()
            .observe(viewLifecycleOwner, Observer { songs ->
                mAdapter.setData(songs)
            })
    }


    override fun onClick(songUri: Uri) {
        musicManager.play(songUri)
    }

    override fun onDestroy() {
        super.onDestroy()
        musicManager.release()
    }
}

Finally, AudioListAdapter class. Changed to handle a list of songs instead of a cursor.

class AudioListAdapter(private val context: Context, private val listener: OnSongClickListener) :
    RecyclerView.Adapter<AudioListAdapter.ViewHolder>() {

    private var songList: List<Song>? = null

    class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
        var title = v.findViewById<View>(R.id.song_title) as TextView
        var image = v.findViewById<View>(R.id.song_note_image) as ImageView
        var artist = v.findViewById<View>(R.id.song_artist) as TextView
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val cardview: View = LayoutInflater.from(context)
            .inflate(R.layout.item_song, parent, false)
        return ViewHolder(cardview)
    }

    fun setData(songs: List<Song>) {
        songList = songs
        notifyDataSetChanged()
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {

        songList?.get(position)?.let {song->
            holder.title.text = song.title
            holder.artist.text = song.artist
            Picasso.get()
                .load(song.artUri)
                .placeholder(R.drawable.ic_music_note_vector)
                .error(R.drawable.ic_music_note_vector)
                .into(holder.image)

            val contentUri: Uri =
                ContentUris.withAppendedId(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, song.id)

            holder.itemView.setOnClickListener {
                listener.onClick(contentUri)
            }
        }
    }

    override fun getItemCount(): Int {
        return songList?.size?: -1
    }

}

The full source code can be found in Github.