3

I would like to let user add image to each item (Card) in LazyColumn. But it seems that images are getting deleted on recomposition. How can I fix that?

@Composable
fun PhotoUpload(
) {
    val imageUri = remember {
        mutableStateOf<Uri?>(null)
    }
    val context = LocalContext.current
    val bitmap = remember {
        mutableStateOf<Bitmap?>(null)
    }

    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.GetContent()
    ) { uri: Uri? ->
        imageUri.value = uri
    }

    imageUri.value?.let {
        LaunchedEffect(Unit) {
            if (Build.VERSION.SDK_INT < 28) {
                bitmap.value = MediaStore.Images
                    .Media.getBitmap(context.contentResolver, it)
            } else {
                val source = ImageDecoder
                    .createSource(context.contentResolver, it)
                bitmap.value = ImageDecoder.decodeBitmap(source)
            }
        }
    }

    bitmap.value?.let { btm ->
        Image(
            bitmap = btm.asImageBitmap(),
            contentDescription = null,
            modifier = Modifier.size(400.dp)
        )
    }

    Button(onClick = {
        launcher.launch("image/*")
    }) {
        Icon(Icons.Filled.PhotoAlbum, "")
    }
}

example

Images get deleted (they're not coming back, it's a loop gif)

PS: For LazyColumn I do use keys. And I also tried using AsyncImage from Coil, but it had the same issue

Paul Sizon
  • 247
  • 4
  • 12

1 Answers1

2

It's getting removed because when you scroll away from that uploaded image, the LazyColumn remove the items that are not longer visible for better performance, LazyList uses SubcomposeLayout under the hood which recomposes visible items and one that about to be visible in scroll direction, which is not possible cause you didn't save them somewhere out of the recomposition (e.g view model)!

You can store Uris or Uris as String inside a data class such as

@Immutable
data class MyModel(
    val title: String,
    val description: String,
    val uri: Uri? = null
)

And create a ViewModel with items and a function to update Uri when it's set

class MyViewModel : ViewModel() {
    val items = mutableStateListOf<MyModel>()
        .apply {
            repeat(15) {
                add(MyModel(title = "Title$it", description = "Description$it"))
            }
        }

    fun update(index: Int, uri: Uri) {
        val item = items[index].copy(uri = uri)
        items[index] = item
    }
}

And getting Uri via clicking a Button

@Composable
fun PhotoUpload(
    onError: (() -> Unit)? = null,
    onImageSelected: (Uri) -> Unit
) {

    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.GetContent()
    ) { uri: Uri? ->
        if (uri == null) {
            onError?.invoke()
        } else {
            onImageSelected(uri)
        }

    }

    Button(onClick = {
        launcher.launch("image/*")
    }) {
        Icon(Icons.Filled.PhotoAlbum, "")
    }
}

Row that returns Uri via callback when it's set

@Composable
private fun MyRow(
    title: String,
    description: String,
    uri: Uri?,
    onImageSelected: (Uri) -> Unit
) {
    Column(
        modifier = Modifier
            .shadow(ambientColor = Color.LightGray, elevation = 4.dp)
            .fillMaxWidth()
            .background(Color.White, RoundedCornerShape(8.dp))
            .padding(8.dp)
    ) {
        Text(title)
        Text(description)
        uri?.let { imageUri ->
            AsyncImage(
                modifier = Modifier
                    .fillMaxWidth()
                    .aspectRatio(4 / 3f),
                model = imageUri, contentDescription = null
            )
        }
        PhotoUpload(onImageSelected = onImageSelected)
    }
}

Usage

@Composable
private fun ImageUploadSample(viewModel: MyViewModel) {
    LazyColumn {

        itemsIndexed(
            key = { _, item ->
                item.hashCode()
            },
            items = viewModel.items
        ) { index, myModel ->
            MyRow(
                title = myModel.title,
                description = myModel.description,
                uri = myModel.uri,
                onImageSelected = { uri ->
                    viewModel.update(index, uri)
                }
            )
        }
    }
}

Result

enter image description here

Thracian
  • 43,021
  • 16
  • 133
  • 222
  • Thank you for very detailed answer! Your solution helped with dissapearing images. But I encountered 2 other issues: image appear only on recomposition (so images appear only when I scoll somewhere and back), and lazyColumn became generally laggy. Have you encountered these problems? – Paul Sizon Dec 05 '22 at 14:58
  • Update: It turned out, I didn't manage my list properly, as I had two lists with my item, one in viewModel and another in my composable function. I fixed that, and now it is working properly) although a bit laggy still – Paul Sizon Dec 06 '22 at 14:29
  • You should test actual performance in release mode with `minifiedEnabled true`. And yes, it's not, still, not as good as RV performancewise. But using `mutableStateListOf` instead of mutableState you only trigger composition for only one item. You can check out this answer as well https://stackoverflow.com/a/74700668/5457853 – Thracian Dec 06 '22 at 15:21