I'm migrating a user profile screen from XML + Glide to Jetpack Compose using this Coil dependency:
io.coil-kt.coil3:coil-compose:3.1.0
The profile image URL is returned from an API and correctly logged in the ViewModel. The image URL returned from the API looks like this:
https://example.com/img/icon_user.png
← PNG over HTTPS
It's a static placeholder image used as the default profile icon, and it loads fine in a browser.
However, the image is not displayed on screen using rememberAsyncImagePainter
, even though:
StateFlow
and Compose recomposition occursclass UserViewModel : ViewModel() {
private val _imageUrl = MutableStateFlow("")
val imageUrl: StateFlow<String> = _imageUrl
fun loadUserProfile() {
viewModelScope.launch {
userProfileUseCase.execute().collect {
if (it is RequestState.Success) {
val body = it.response.result.body
val url = body.imageUrl
Log.d("UserViewModel", "imageUrl = $url") // ✅ URL is valid
_imageUrl.emit(url ?: "")
}
}
}
}
}
@Composable
fun UserProfileScreen(userViewModel: UserViewModel) {
val imageUrl by userViewModel.imageUrl.collectAsStateWithLifecycle()
UserImageThumbnail(
imageUrl = imageUrl,
onClick = { /* omitted */ }
)
}
@Composable
fun UserImageThumbnail(imageUrl: String?, onClick: () -> Unit) {
val context = LocalContext.current
Box(
modifier = Modifier
.size(107.dp)
.clickable { onClick() }
) {
if (!imageUrl.isNullOrBlank()) {
Image(
painter = rememberAsyncImagePainter(
ImageRequest.Builder(context)
.data(imageUrl)
.memoryCachePolicy(CachePolicy.DISABLED)
.diskCachePolicy(CachePolicy.DISABLED)
.build()
),
contentDescription = null,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop
)
}
Icon(
painter = painterResource(id = R.drawable.icon_camera),
contentDescription = null,
modifier = Modifier
.align(Alignment.Center)
.size(48.dp)
)
}
}
Why isn't Coil 3 displaying a valid image URL in Jetpack Compose, even though the URL is correct and Compose recomposes? Is there anything specific I need to handle when using Coil 3 with image URLs from APIs?
I expected the image to appear when I updated the StateFlow with a valid image URL from the API. This includes both user-uploaded profile images and default fallback images that are always provided by the API. So the URL is never null or empty.
I confirmed in logs that the URL is valid and Compose recomposes as expected. But the image is not displayed.
I tried disabling both memory and disk cache with Coil 3, and also verified that the same URL loads successfully in Glide and in a browser. Still, nothing is shown in Compose.
It seems like you are missing a network dependency for Coil to access the internet. This is a new requirement starting from version 3.
On Android you can simply use this:
io.coil-kt.coil3:coil-network-okhttp
It tells Coil to use the OkHttp client that Android already provides out-of-the-box.
If you already have Ktor in your dependencies (a feature-rich multiplatform networking library), you can tell Coil to use it instead. For more, see here: https://coil-kt.github.io/coil/network/