1

I am trying to write a music player app by using Jetpack Compose. I have a MusicCardModel like below

data class MusicCardModel(
    val contentUri: Uri?,
    val songId: Long?,
    val cover: Bitmap?,
    val songTitle: String?,
    val artist: String?,
    val duration: String?
)

When I start the app I am scanning all the music files by using MediaStore below function

@SuppressLint("Recycle")
fun Context.musicList(): MutableList<MusicCardModel> {
    val list = mutableListOf<MusicCardModel>()
    val collection =
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
        } else {
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
        }
    val projection = arrayOf(
        MediaStore.Audio.Media._ID,
        MediaStore.Audio.Media.DISPLAY_NAME,
        MediaStore.Audio.Media.DURATION,
        MediaStore.Audio.Media.TITLE,
        MediaStore.Audio.Media.ALBUM_ID,
        MediaStore.Audio.Media.ARTIST
    )
    val selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0"
    val sortOrder = "${MediaStore.Audio.Media.DISPLAY_NAME} ASC"
    val query = this.contentResolver.query(
        collection,
        projection,
        selection,
        null,
        sortOrder
    )

    query?.use { cursor ->
        val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
        val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
        val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
        val artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
        val albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)

        while (cursor.moveToNext()) {
            val id = cursor.getLong(idColumn)
            val duration = cursor.getInt(durationColumn)
            val title = cursor.getString(titleColumn)
            val artist = cursor.getString(artistColumn)
            val albumId = cursor.getLong(albumIdColumn)
            val contentUri: Uri = ContentUris.withAppendedId(
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                id
            )
            
            val bitmap = getAlbumArt(this, contentUri)
            val durationString = convertMili(duration)
            list.add(MusicCardModel(contentUri, id, bitmap, title, artist, durationString))
        }

    }
    return list
}

fun getAlbumArt(context: Context, uri: Uri): Bitmap{
    val mmr = MediaMetadataRetriever()
    mmr.setDataSource(context, uri)
    val data = mmr.embeddedPicture
    return if(data != null){
        BitmapFactory.decodeByteArray(data, 0, data.size)
    }else{
        BitmapFactory.decodeResource(context.resources, R.drawable.note)
    }
}

These part of code showing that list

Box(modifier = Modifier
                   .padding(bottom = if (isPlaying.value) 80.dp else 0.dp)){

               LazyColumn {
                   items(list) { index ->
                       MusicCard(
                           uri = index.contentUri!!,
                           songId = index.songId,
                           artist = index.artist!!,
                           name = index.songTitle!!,
                           duration = index.duration!!,
                           isPlaying = isPlaying,
                           playingSong = playingSong
                       )
                   }
               }
           }
@Composable
fun MusicCard(
    uri: Uri,
    artist: String,
    name: String,
    cover: Bitmap?,
    duration: String,
    isPlaying: MutableState<Boolean>,
    playingSong: MutableState<MusicCardModel>,
    songId: Long?,
    playState: MutableState<Boolean>
) {

    val context = LocalContext.current

    Card(modifier = Modifier
        .fillMaxWidth()
        .clickable {
            playMusic(context, uri)
            playState.value = true
            isPlaying.value = true
            val playingSongModel = MusicCardModel(uri, songId,
                null, name, artist, duration)
            playingSong.value = playingSongModel

       }
    ) {
        Row(
            modifier = Modifier.padding(10.dp),
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            Row(
                modifier = Modifier.weight(1f),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Image(
                    modifier = Modifier.size(70.dp),
                    bitmap = cover!!.asImageBitmap(),
                    contentDescription = "Cover Photo",
                )

                Column(
                    modifier = Modifier
                        .padding(horizontal = 10.dp)
                ) {
                    Text(
                        modifier = Modifier.padding(vertical = 5.dp),
                        text = artist
                    )
                    Text (name, maxLines = 1)
                }
            }
            Text(text = duration)
        }
    }
}

There are 2 problems. Number one is creating this list with album arts taking too much time and it is waiting so long to get that list on the screen. Without album art its very fast but I want to show album arts. How can I lazily load the metadatas of music files? Second problem is loading all this list data to the memory gives a TransactionTooLargeException when I switching to another app and the app stops. How can I solve these problems?

1 Answers1

1

I suggest you moving this logic into a view model and loading the bitmap only once the needed cell appears.

data class MusicCardModel(
    val contentUri: Uri,
    val songId: Long,
    val cover: Bitmap?,
    val songTitle: String,
    val artist: String,
    val duration: String
)

class MusicListViewModel: ViewModel() {
    val musicCards = mutableStateListOf<MusicCardModel>()

    private var initialized = false
    private val backgroundScope = viewModelScope.plus(Dispatchers.Default)

    fun initializeListIfNeeded(context: Context) {
        if (initialized) return
        val collection =
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
            } else {
                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
            }
        val projection = arrayOf(
            MediaStore.Audio.Media._ID,
            MediaStore.Audio.Media.DISPLAY_NAME,
            MediaStore.Audio.Media.DURATION,
            MediaStore.Audio.Media.TITLE,
            MediaStore.Audio.Media.ALBUM_ID,
            MediaStore.Audio.Media.ARTIST
        )
        val selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0"
        val sortOrder = "${MediaStore.Audio.Media.DISPLAY_NAME} ASC"
        val query = context.contentResolver.query(
            collection,
            projection,
            selection,
            null,
            sortOrder
        )

        query?.use { cursor ->
            val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
            val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION)
            val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE)
            val artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST)
            val albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID)

            while (cursor.moveToNext()) {
                val id = cursor.getLong(idColumn)
                val duration = cursor.getInt(durationColumn)
                val title = cursor.getString(titleColumn)
                val artist = cursor.getString(artistColumn)
                val albumId = cursor.getLong(albumIdColumn)
                val contentUri: Uri = ContentUris.withAppendedId(
                    MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
                    id
                )

                val durationString = convertMili(duration)
                musicCards.add(MusicCardModel(contentUri, id, null, title, artist, durationString))
            }
        }
        initialized = true
    }

    fun loadBitmapIfNeeded(context: Context, index: Int) {
        if (musicCards[index].cover != null) return
        // if this is gonna lag during scrolling, you can move it on a background thread
        backgroundScope.launch {
            val bitmap = getAlbumArt(context, musicCards[index].contentUri)
            musicCards[index] = musicCards[index].copy(cover = bitmap)
        }
    }

    private fun getAlbumArt(context: Context, uri: Uri): Bitmap{
        val mmr = MediaMetadataRetriever()
        mmr.setDataSource(context, uri)
        val data = mmr.embeddedPicture
        return if(data != null){
            BitmapFactory.decodeByteArray(data, 0, data.size)
        }else{
            BitmapFactory.decodeResource(context.resources, R.drawable.note)
        }
    }
}

Use it like this:

@Composable
fun MusicListScreen(
    viewModel: MusicListViewModel = viewModel()
) {
    val musicCards = viewModel.musicCards
    val context = LocalContext.current
    LaunchedEffect(Unit) {
        viewModel.initializeListIfNeeded(context)
    }
    LazyColumn {
        itemsIndexed(musicCards) { index, card ->
            LaunchedEffect(Unit) {
                viewModel.loadBitmapIfNeeded(context, index)
            }
            if (card.cover != null) {
                Image(bitmap = card.cover.asImageBitmap(), "...")
            } else {
                // some placeholder
            }
        }
    }
}
Phil Dukhov
  • 67,741
  • 15
  • 184
  • 220
  • It is loading lazily but when I scrolling down its lagging as you wrote there but I don't know how to move it on a background thread also when I reach the end of the list by scrolling, the app is closing itself. – Ahmet Saraç Nov 24 '21 at 21:23
  • @AhmetSaraç I've moved it on the background, check it out. As to the close, it's probably crashing, you should inspect the logs. – Phil Dukhov Nov 25 '21 at 01:34
  • It's better but not that good because it's still laggy while scrolling. I think the problem is storing the cover bitmap within model. That data is big and it is taking time to get and show. If I can list first the text values I don't think that the scrolling will be slow. I have a MusicCard composable to list a song I just added to the question. I think I should seperate the cover bitmap from model. – Ahmet Saraç Nov 25 '21 at 02:15
  • @AhmetSaraç try scaling it to some reasonable size with `Bitmap.createScaledBitmap(b, 120, 120, false)` before adding it to the list (e.g. on the background coroutine). Or try using Coil `rememberImagePainter`, it may do this job for you – Phil Dukhov Nov 25 '21 at 02:26
  • 1
    Scaling didn't work out I will try Coil if it won't work that means I think I have the problem about logic of modelling the data I will try another modelling also I don't know anything about ViewModel. I will learn how to use ViewModel. I am just a beginner on Android development and I wanted to start with Jetpack Compose. Thank you anyway – Ahmet Saraç Nov 25 '21 at 03:29