coil: Reload image after a failed load in Jetpack Compose

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.

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 15
  • Comments: 16 (4 by maintainers)

Most upvoted comments

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") }
    }
}

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.

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.

would be nice to add a support for this

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.

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.