In a Jetpack Compose app I have a LazyVerticalGrid of thumbnails, each of which needs to be drawn in a bitmap-backed Canvas at compose time.
The thumbnails draw correctly if I simply draw them in DrawScope of the Canvas, but the user experience is poor. When the user scrolls the LazyVerticalGrid, there is a lot of jank as each thumbnail draws itself.
I had thought that Jetpack Compose composed in background threads when needed, but it all seems to be happening on the main thread, leading to the severe jank, even on the latest phones.
I can solve the jank problem by drawing onto the Canvas's underlying bitmap on another thread, using LaunchedEffect withContext(IO). But the problem is, Compose doesn't know to recompose the Canvas when the bitmap is drawn, so I often end up with half-drawn thumbnails.
Is there a way to do work off the main thread and then recompose once that work is done?
Here is the janky code (edited for brevity), followed by the non-janky version that doesn't always recompose when the drawing is complete:
val imageBitmap = remember {Bitmap.createBitmap(515, 618, Bitmap.Config.ARGB_8888)}
val bitmapCanvas = remember { android.graphics.Canvas(imageBitmap) }
ElevatedCard() {
Canvas() {
bitmapCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
penStrokes.forEach {
inker.drawEvent(it)
}
this.drawImage(imageBitmap.asImageBitmap())
}
}
non-janky but still not right
val imageBitmap = remember {Bitmap.createBitmap(515, 618, Bitmap.Config.ARGB_8888)}
val bitmapCanvas = remember { android.graphics.Canvas(imageBitmap) }
LaunchedEffect(Unit) {
withContext(IO) {
bitmapCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
penStrokes.forEach {
inker.drawEvent(it)
}
}
}
ElevatedCard() {
Canvas() {
this.drawImage(imageBitmap.asImageBitmap())
}
}
In the end I used the common "invalidate++" kludge that people seem to use to force Jetpack Compose to redraw on command. It has a bit of code smell to it, I think, but it does the trick.
val imageBitmap = remember {Bitmap.createBitmap(515, 618,
Bitmap.Config.ARGB_8888)}
val bitmapCanvas = remember { android.graphics.Canvas(imageBitmap) }
var invalidate by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
withContext(IO) {
bitmapCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
penStrokes.forEach {
inker.drawEvent(it)
}
invalidate++
}
}
ElevatedCard() {
Canvas() {
invalidate.let {
this.drawImage(imageBitmap.asImageBitmap())
}
}
}