coil icon indicating copy to clipboard operation
coil copied to clipboard

Reload image after a failed load in Jetpack Compose

Open FishHawk opened this issue 2 years ago • 17 comments

Is your feature request related to a problem? Please describe.

val painter = rememberImagePainter(url)
Image(
    modifier = Modifier.fillMaxSize(),
    painter = painter,
    contentDescription = null,
    contentScale = ContentScale.Fit
)
when (val state = painter.state) {
    is ImagePainter.State.Error -> {
        TextButton(onClick = { }) { Text("retry") }
    }
}

After a failed load, the user should be able to reload the image via the retry button.

Describe the solution you'd like It is better to provide reload method, but it seems that setting request in ImagePainter to public would also work.

FishHawk avatar Sep 08 '21 08:09 FishHawk

Still figuring out a good public API for this, but if you need this today you can force retry by changing a parameter:

var retryHash by remember { mutableStateOf(0) }
val painter = rememberAsyncImagePainter(
    model = ImageRequest.Builder(LocalContext.current)
        .data(url)
        .setParameter("retry_hash", retryHash)
        .build()
)
Image(
    painter = painter,
    contentDescription = null,
    contentScale = ContentScale.Fit,
    modifier = Modifier.fillMaxSize(),
)
when (val state = painter.state) {
    is AsyncImagePainter.State.Error -> {
        TextButton(onClick = { retryHash++ }) { Text("retry") }
    }
}

Currently, I'm thinking the public API should be something like this, but let me know what you think!

val requestHandle = rememberAsyncImageRequestHandle()
val painter = rememberAsyncImagePainter(
    request = ImageRequest.Builder(LocalContext.current)
        .data(url)
        .requestHandle(requestHandle)
        .build()
)
Image(
    painter = painter,
    contentDescription = null,
    contentScale = ContentScale.Fit,
    modifier = Modifier.fillMaxSize(),
)
when (val state = painter.state) {
    is AsyncImagePainter.State.Error -> {
        TextButton(onClick = { requestHandle.restart() }) { Text("retry") }
    }
}

colinrtwhite avatar Nov 22 '21 21:11 colinrtwhite

Thanks! I'll try it later.

The api is indeed a problem. Your example is very similar to focusRequester. I'm ok with it. But since there must be an ImagePainter object here, I think it would be simpler to have an ImagePainter with a retry method.

FishHawk avatar Nov 23 '21 05:11 FishHawk

Yep, adding a method to ImagePainter might be ok (and more discoverable), though I think it might not work well with the new AsyncImage component that'll be added in Coil 2.0 since the ImagePainter is only exposed in AsyncImageScope. Having the retry handler as part of the request works for both ImagePainter and AsyncImage.

colinrtwhite avatar Nov 23 '21 19:11 colinrtwhite

I see. In that case, I would prefer code like this:

AsyncImage(...) { state ->
    if (state is AsyncImagePainter.State.Error) {
        TextButton(onClick = { painter.retry() }) { Text("retry") }
    } else {
        AsyncImageContent()
    }
}

I don't think the retry function should be used outside of AsyncImageScope. But some global components like BottomSheet do cause this situation (I personally take it as a design mistake). In that case, a simple lambda should be enough: val retryHandle = { painter.retry() }

If someone needs more complex control logic (like refreshing multiple images at once), he can use a Flow<RefreshEvent> and collect it inside AsyncImageScope.

Anyway, I prefer the direct api, mainly because focusRequester messed up my code.

FishHawk avatar Nov 24 '21 02:11 FishHawk

A retry handle would be desirable. @colinrtwhite - Is this feature something you see in a near future release?

sarseneaultrp avatar Nov 30 '21 18:11 sarseneaultrp

No immediate plans for adding this (currently focused on making sure AsyncImage is solid), though it'll likely be in the final 2.0 release or 2.1.

colinrtwhite avatar Dec 12 '21 08:12 colinrtwhite

Are there any updates on this issue, or a workaround?

BenjyTec avatar Jan 09 '23 13:01 BenjyTec

would be nice to add a support for this

trOnk12 avatar Jan 18 '23 14:01 trOnk12

is this implemented and exposed to the API? if not, is there any estimation on when it will be available?

NasiaKoutsopoulou avatar Jul 17 '23 14:07 NasiaKoutsopoulou

Only works for me if .setParameter("retry_hash", retryHash, memoryCacheKey = null) in @colinrtwhite 's solution is used without the memoryCacheKey argument, thus .setParameter("retry_hash", retryHash).

Also if retryHash is a Boolean, the model toggles between its first two instances, so it does need to have a range.

eecs441staff avatar Aug 11 '23 18:08 eecs441staff

I am getting this Error when i add an image URI with an "i".

Heres the Code: AsyncImage( rememberAsyncImagePainter(my_image_uri), contentDescription = "image")

Getting this Runtime Error if this helps:

ava.lang.NoClassDefFoundError: Failed resolution of: Landroidx/compose/runtime/PrimitiveSnapshotStateKt; at coil.compose.AsyncImagePainter.(AsyncImagePainter.kt:166) at coil.compose.AsyncImagePainterKt.rememberAsyncImagePainter-5jETZwI(AsyncImagePainter.kt:141)

PythonVader avatar Dec 20 '23 16:12 PythonVader

Any updates? 👀

massivemadness avatar Jan 10 '24 13:01 massivemadness

Hi folks, quick update on this. This is definitely something I want to address properly in Coil 3.0. Ideally, we can kill two birds with one stone and hoist AsyncImagePainter so that way users can also observe its other properties (like state or request). For example:

val painter = rememberAsyncImagePainter(
    model = "https://example.com/image.jpg"
)

AsyncImage(
    model = painter,
    contentDescription = null,
)

when (painter.state) {
    is AsyncImagePainter.State.Error -> {
        ErrorButton(onClick = { painter.restart() })
    }
}

There are some implementation details that might cause this to not work in practice, but it's the API I'd like!

In the meantime for 2.x I'd continue to use this solution, which isn't ideal, but should force the request to restart.

colinrtwhite avatar Feb 01 '24 07:02 colinrtwhite

Related to this I'd love to have the ability to disable "autostarting" the request, I recently had very slow connections speeds and it would have been nice to not automatically trigger some image loads.

I assume I could wrap those images and instantly fail them with an interceptor but I think it would be even nicer UX to be able to differentiate the initial request from retries. Thanks for all the work so far!

brinsche avatar Apr 03 '24 15:04 brinsche

Related to this I'd love to have the ability to disable "autostarting" the request, I recently had very slow connections speeds and it would have been nice to not automatically trigger some image loads.

I assume I could wrap those images and instantly fail them with an interceptor but I think it would be even nicer UX to be able to differentiate the initial request from retries. Thanks for all the work so far!

This is not related and you are overthinking this. You can just hide your painter and async image before the user manually clicks it.

Jeanno avatar Apr 23 '24 06:04 Jeanno