skydoves / landscapist

🌻 A pluggable, highly optimized Jetpack Compose and Kotlin Multiplatform image loading library that fetches and displays network images with Glide, Coil, and Fresco.

Home Page:https://skydoves.github.io/landscapist/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Unexpected Recompositions

jd565 opened this issue · comments

Please complete the following information:

  • Library Version v2.1.9
  • Affected Device(s) All

Describe the Bug:

This has ended up a bit long and it is possible I've not 100% understood how compose is skipping/recomposing things so if you think there is something wrong do let me know, but the below is my understanding of things and matches up with debugging and logging on the app.

Lets say you have a screen that takes a state data class State(name: String, image: String). You use glide to display the image and text for the name

fun Screen(state: State) {
    Column {
        GlideImage(imageModel = { state.image }, modifier = Modifier.size(56.dp))
        Text(state.name)
    }
}

if you call this function again with a new state where the name is different, but the image is the same, this causes GlideImage to recompose (the generated lambda captures the state, state has changed so composable is not skipped).

Inside the GlideImage, the lambda is calculated and put inside a StableHolder (side note, seems odd that the imageModel labda is invoked twice - once for the recomposeKey and once for the builder. For most cases this does not seem an issue but if you are doing some other calculations there does not seem ideal).

The StableHolder I assume is to allow compose to skip the inner GlideImage call if the actual model hasn't changed - it is marked as @stable, but then does not implement equals - compose will only consider these classes the same if the are the same instance, but GlideImage is creating a new StableHolder on every recomposition so the inner GlideImage will never be skipped. It seems to me that StableHolder should implement equals and delegate to the value as to whether it is equal to - though this may be risky if the value used can't be treated as stable (e.g. it has mutable properties), or the outer GlideImage should be creating the StableHolder with some sort of remember call e.g. recomposeKey = remember(invokedModel) { StableHolder(invokedModel) } (which would mean the same instance if the invoked model is equals).

As another recomposition bit of fun, we found some instances where the FlowCustomTarget was created twice, and one instance got the size callbacks registered on, and the other instance got the constraints given to it. It looks like new targets are created based on remember(recomposeKey, imageOptions), but the executeImageRequest is launched based on just LaunchedEffect(recomposeKey), so if just the options change then there is a new target where the constraints are set, but the glide request is still based on the original target.

Expected Behavior:

Passing in the same model (for models that implement equality) does not cause unnecessary recompositions/glide loads.

They @jd565, thanks for reporting important issues!
The StableHolder will be changed as a data class, and imageOptions will be used for a recomposition trigger in the ImageLoad composable in the next release.

@jd565 Now you're available the patch version with the latest snapshot. Thanks for reporting this issue!