Stefan Oltmann
07/07/2021, 10:46 AMCLOVIS
07/07/2021, 11:22 AMsuspend fun Image.createThumbnail() {
// Your logic here
}
Then when you have that, you can use normal flows if you want to, but I don't see any benefits (or maybe I didn't understand your objective) :
val thumbnails = flowOf(image1, image2, ...)
.map { it.createThumbnail() }
Stefan Oltmann
07/07/2021, 11:37 AMExecutors.newSingleThreadExecutor()
That way they get executed one after anotherCasey Brooks
07/07/2021, 2:18 PMStefan Oltmann
07/07/2021, 3:22 PMCLOVIS
07/07/2021, 4:56 PMStefan Oltmann
07/07/2021, 5:08 PMCasey Brooks
07/07/2021, 5:27 PMproduceState
with LazyVerticalGrid
to load the image for each view will keep things declarative, but also cancel loading when the view leaves the screen if it hasn’t finished yet (so you’re not wasting system resources loading images that won’t be seen).
Also to that point, if the screen feels sluggish and that’s why you’re looking into this, it might be due to the lazy grid (not the image-loading); there seem to be a lot of performance issues with that right now. Image-loading should all be happening in a background thread and would not cause UI jank or low framerates; at worst, unoptimized image-loading would just make it take slightly longer for the image to become visible, but should not impact the responsiveness of the UI itselfStefan Oltmann
07/07/2021, 5:33 PMinterface ImageLoader {
suspend fun loadFullImage(photo: Photo): ImageBitmap
suspend fun loadThumbnailImage(photo: Photo): ImageBitmap
}
And this is my Composable:
@Composable
fun AsyncImage(
photo: Photo,
imageLoader: ImageLoader
) {
val imageBitmap = remember { mutableStateOf<ImageBitmap?>(null) }
LaunchedEffect(photo) {
/* Load image if missing. */
if (imageBitmap.value == null) {
launch {
withContext(Dispatchers.Default) {
imageBitmap.value = imageLoader.loadThumbnailImage(photo)
}
}
}
}
imageBitmap.value?.let {
Image(
it,
null,
modifier = Modifier.padding(8.dp)
)
} ?: run {
Placeholder(100.dp)
}
}
The whole grid looks like this:
@Composable
fun App(
darkMode: Boolean,
store: PhotoStore,
imageLoader: ImageLoader
) {
AppTheme(
darkMode = darkMode
) {
val state = store.observeState().collectAsState()
val photos = state.value.photos
Column {
LazyVerticalGrid(cells = GridCells.Fixed(4)) {
items(photos.size) { index ->
val photo = photos[index]
AsyncImage(
photo,
imageLoader
)
}
}
}
}
}
Casey Brooks
07/07/2021, 6:19 PMLaunchedEffect
isn’t the only way to launch a coroutine; LaunchedEffect
is more of a “fire-and-forget” coroutine, but you also have produceState
which launches a coroutine and returns a result as a State
. You can also rememberCoroutineScope
to get a handle to a CoroutineScope
and do more custom stuff with it, if needed.
To your example, you should be using produceState
instead of LaunchedEffect
(and you should’t need to do an additional launch { }
inside LaunchedEffect
either, it’s already running a coroutine). https://developer.android.com/jetpack/compose/side-effects#producestate
If your image-loader already exposes a suspend
API, then it should clean itself up automatically when it gets cancelled. If it’s a callback-based API, you can clean that up with coroutines APIs when the coroutine is cancelled. It’s the job of Compose to decide when to cancel the coroutine, and the coroutine’s job to unsubscribe and/or cancel the background work in response to cancellation.
This snippet shows how one might tie a callback API to coroutines and cancel it properly (example uses OkHttp), entirely within the normal coroutines library:
https://github.com/gildor/kotlin-coroutines-okhttp/blob/master/src/main/kotlin/ru/gildor/coroutines/okhttp/CallAwait.kt#L36-L56
Or, produceState
includes its own awaitDispose
function for the same purpose:
https://developer.android.com/reference/kotlin/androidx/compose/runtime/ProduceStateScope#awaitDispose(kotlin.Function0)
And here’s how you could transform your snippet to using `produceState`:
kotlin
@Composable
fun AsyncImage(
photo: Photo,
imageLoader: ImageLoader
) {
val imageBitmap = produceState<ImageBitmap?>(initialValue = null, photo) {
value = withContext(Dispatchers.Default) {
imageBitmap.value = imageLoader.loadThumbnailImage(photo)
}
}
imageBitmap.value?.let {
Image(
it,
null,
modifier = Modifier.padding(8.dp)
)
} ?: run {
Placeholder(100.dp)
}
}
Stefan Oltmann
07/07/2021, 7:19 PMCasey Brooks
07/07/2021, 7:54 PMproduceState
gets cancelled when something is non-visible (like scrolling off-screen), it’s cancelled because the view gets disposed when its scrolled off-screen in a lazy listStefan Oltmann
07/07/2021, 9:21 PM