Sujal Kumar
Sujal Kumar

Reputation: 1164

AsyncImage (Coil) Caching not working inside LazyColumn

I am having a composable named ProjectCard. Inside that, I have an AsyncImage composable to retrieve project's icon from Firebase Storage (in case it is supposed to be already uploaded there). In the Search Page (where I want to get details of a certain user), I am using a LazyColumn to show their projects and I was expecting that once I load all images, there's no need to do it anymore but the problem is that... upon scrolling the LazyColumn, the ProjectCard(s) which are getting out of the visible part of screen are trying to load the image back when they get back focus. Moreover, the loading is considerably slow, thus pointing out the fact that it's trying to fetch from my Firebase Storage only and definitely not from any Cache.

ProjectCard.kt :

@Composable
fun ProjectCard(
    project: Project
) {
    val context = LocalContext.current
    var uriOfProjectIconFromFirebase by remember {
        mutableStateOf<Uri?>(null)
    }

    LaunchedEffect(Unit) {
        if (project.storedInFirebase) {
            Log.i(TAG, "Trying to get uri of Project Icon...")
            Firebase.storage.reference.child(
                "projectIcons/${project.belongsTo}/${project.name}.jpg"
            ).downloadUrl.addOnSuccessListener { uri ->
                uriOfProjectIconFromFirebase = uri
            }
        }
    }

    Card(
        modifier = Modifier
            .fillMaxWidth()
    ) {
        Box(
            modifier = Modifier.background(
                Brush.verticalGradient(
                    listOf(
                        project.tintColor,
                        Color.White
                    )
                )
            )
        )
        {
            Column(
                modifier = Modifier.padding(15.dp)
            ) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    AsyncImage(
                        model =
                        if (project.storedInFirebase) {
                            uriOfProjectIconFromFirebase
                        } else if (project.iconPath != null) {
                            BitmapFactory.decodeFile(project.iconPath)
                        } else {
                            project.iconId
                        },
                        contentDescription = "Project Icon",
                        contentScale = ContentScale.Crop,
                        modifier = Modifier
                            .height(50.dp)
                            .width(50.dp)
                            .clip(RoundedCornerShape(12.dp))
                    )

                    Spacer(modifier = Modifier.width(15.dp))

                    Text(
                        text = project.name,
                        fontSize = 18.sp,
                        fontFamily = FontFamily.SansSerif,
                        fontWeight = FontWeight.SemiBold,
                        color = Color.Black
                    )
                    Spacer(modifier = Modifier.width(10.dp))

                    Image(
                        painter = painterResource(id = R.drawable.link_icon),
                        contentDescription = "",
                        modifier = Modifier.clickable {
                            val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(project.link))
                            context.startActivity(browserIntent)
                        }
                    )
                }

                HorizontalDivider(
                    Modifier.padding(vertical = 5.dp),
                    color = Color.DarkGray
                )

                Text(
                    text = project.description,
                    color = Color.DarkGray
                )
            }
        }

    }
}

The LazyColumn:

LazyColumn(verticalArrangement = Arrangement.spacedBy(5.dp)) {
                        items(projectWrapperOfPersonSearched.projectsList.size)
                        {
                            ProjectCard(project = projectWrapperOfPersonSearched.projectsList[it])
                        }
                    }

I tried using LaunchedEffect but it didn't help either.

Upvotes: 3

Views: 221

Answers (2)

Sujal Kumar
Sujal Kumar

Reputation: 1164

I solved it by doing the Uri fetching part in the parent composable itself and then simply passed them as a parameter to the ProjectCard composable. The parent composable is supposed to recompose very rarely so it somehow does the work.

In the parent composable (SearchPage), when a search is performed:

projectWrapperOfPersonSearched.projectsList.forEach {
                                Firebase.storage.reference.child(
                                    "projectIcons/${it.belongsTo}/${it.name}.jpg"
                                ).downloadUrl.addOnSuccessListener { uri ->
                                    iconUrls[it.name] = uri
                                }
                            }

The LazyColumn looks like this now:

LazyColumn(verticalArrangement = Arrangement.spacedBy(5.dp)) {
                        itemsIndexed(projectWrapperOfPersonSearched.projectsList)
                        { _, item ->
                            ProjectCard(
                                project = item,
                                iconUrl = iconUrls.getOrDefault(item.name, null)
                            )
                        }
                    }

where iconUrls is defined like this:

val iconUrls: SnapshotStateMap<String, Uri> = remember {
        mutableStateMapOf()
    }

Finally, the ProjectCard transforms into this:

@Composable
fun ProjectCard(
    project: Project,
    iconUrl: Uri? = null
) {
    val context = LocalContext.current

    Card(
        modifier = Modifier
            .fillMaxWidth()
    ) {
        Box(
            modifier = Modifier.background(
                Brush.verticalGradient(
                    listOf(
                        project.tintColor,
                        Color.White
                    )
                )
            )
        )
        {
            Column(
                modifier = Modifier.padding(15.dp)
            ) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    AsyncImage(
                        model =
                        if (project.storedInFirebase && iconUrl!=null) {
                            iconUrl
                        } else if (project.iconPath != null) {
                            BitmapFactory.decodeFile(project.iconPath)
                        } else {
                            project.iconId
                        },
                        imageLoader = context.imageLoader,
                        contentDescription = "Project Icon",
                        contentScale = ContentScale.Crop,
                        modifier = Modifier
                            .height(50.dp)
                            .width(50.dp)
                            .clip(RoundedCornerShape(12.dp))
                    )

                    Spacer(modifier = Modifier.width(15.dp))

                    Text(
                        text = project.name,
                        fontSize = 18.sp,
                        fontFamily = FontFamily.SansSerif,
                        fontWeight = FontWeight.SemiBold,
                        color = Color.Black
                    )
                    Spacer(modifier = Modifier.width(10.dp))

                    Image(
                        painter = painterResource(id = R.drawable.link_icon),
                        contentDescription = "",
                        modifier = Modifier.clickable {
                            val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(project.link))
                            context.startActivity(browserIntent)
                        }
                    )
                }

                HorizontalDivider(
                    Modifier.padding(vertical = 5.dp),
                    color = Color.DarkGray
                )

                Text(
                    text = project.description,
                    color = Color.DarkGray
                )
            }
        }

    }
}

Upvotes: 0

Code Poet
Code Poet

Reputation: 8013

  1. You need to implement the caching strategy in a way similar to this:

    class ProjectApplication : Application(), ImageLoaderFactory {
    override fun newImageLoader(): ImageLoader {
        return ImageLoader(this).newBuilder()
            .memoryCachePolicy(CachePolicy.ENABLED)
            .memoryCache {
                MemoryCache.Builder(this)
                    .maxSizePercent(0.25)
                    .strongReferencesEnabled(true)
                    .build()
            }
            .diskCachePolicy(CachePolicy.ENABLED)
            .diskCache {
                DiskCache.Builder()
                    .maxSizePercent(0.02)
                    .directory(cacheDir)
                    .build()
            }
            .logger(DebugLogger())
            .build()
    }}
    

More details here.

  1. You need to use a Boolean to tell your app not to fetch your images remotely when you navigate back. For instance, if isNavigatingBack is true, do not make the call again.

Upvotes: 1

Related Questions