How to correctly upload image to an item in LazyList in Jetpack Compose?
Problem Description:
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, "")
}
}
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
Solution – 1
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