Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reload image after a failed load in Jetpack Compose #884

Open
FishHawk opened this issue Sep 8, 2021 · 17 comments
Open

Reload image after a failed load in Jetpack Compose #884

FishHawk opened this issue Sep 8, 2021 · 17 comments
Labels
enhancement New feature or request

Comments

@FishHawk
Copy link

FishHawk commented Sep 8, 2021

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 FishHawk added the enhancement New feature or request label Sep 8, 2021
@colinrtwhite
Copy link
Member

colinrtwhite commented Nov 22, 2021

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") }
    }
}
@FishHawk
Copy link
Author

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.

@colinrtwhite
Copy link
Member

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.

@FishHawk
Copy link
Author

FishHawk commented Nov 24, 2021

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.

@sarseneaultrp
Copy link

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

@colinrtwhite
Copy link
Member

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.

@BenjyTec
Copy link

BenjyTec commented Jan 9, 2023

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

@trOnk12
Copy link

trOnk12 commented Jan 18, 2023

would be nice to add a support for this

@NasiaKoutsopoulou
Copy link

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

@eecs441staff
Copy link

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.

@PythonVader
Copy link

PythonVader commented Dec 20, 2023

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)

@massivemadness
Copy link

Any updates? 👀

@colinrtwhite
Copy link
Member

colinrtwhite commented Feb 1, 2024

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.

@brinsche
Copy link

brinsche commented Apr 3, 2024

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!

@Jeanno
Copy link

Jeanno commented Apr 23, 2024

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.

@brinsche
Copy link

Sure, but I'd want to show the same placeholder and reload due to error and reload due to non autostart are conceptually similar in my mind, but it's obviously fine if coil doesn't want to take on the api burden for such a niche use case.

@Andrew0000
Copy link

Andrew0000 commented May 29, 2024

Maybe not the optimal solution from the recomposition perspective but seems like we can use the .key() function to easily "reload" the AsyncImage in Coil 3.

Something like:

val failure = remember(url) { mutableIntStateOf(0) }
val attempt = remember(url) { mutableIntStateOf(0) }
LaunchedEffect(failure.intValue) {
    val delay = (failure.intValue * 1_000L).coerceAtMost(10_000L)
    delay(delay)
    attempt.intValue = failure.intValue
}

key(attempt.intValue) {
    AsyncImage(
        ...
        onState = { state: AsyncImagePainter.State ->
            if (state is AsyncImagePainter.State.Error) {
                failure.intValue++
            }
        },
    )
}

p.s. the most efficient solution would be to perform a reloading at the network loader level (which could take the reload strategy as a parameter).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request