In Compose how to access drawable once Coil loads image from URL - android

I am using accompanist-coil:0.12.0. I want to load image from a url and then pass the drawable to a method. I am using this:
val painter = rememberCoilPainter(
request = ImageRequest.Builder(LocalContext.current)
.data(imageUrl)
.target {
viewModel.calcDominantColor(it) { color ->
dominantColor = color
}
}
.build(),
fadeIn = true
)
and then passing the painter to Image like this:
Image(
painter = painter,
contentDescription = "Some Image",
)
The image loads without any problem but the method calcDominantColor is never called.
Am I doing it the wrong way?
UPDATE:
I was able to call the method using Transformation in requestBuilder but I am not sure, if this is how it is supposed to be done because I am not actually transforming the Bitmap itself:
val painter = rememberCoilPainter(
request = entry.imageUrl,
requestBuilder = {
transformations(
object: Transformation{
override fun key(): String {
return entry.imageUrl
}
override suspend fun transform(
pool: BitmapPool,
input: Bitmap,
size: Size
): Bitmap {
viewModel.calcDominantColor(input) { color ->
dominantColor = color
}
return input
}
}
)
}
)
This works fine for first time but when the composable recomposes, transformation is returned from cache and my method doesn't run.

I think you want to use LaunchedEffect along with an ImageLoader to access the bitmap from the loader result.
val context = LocalContext.current
val imageLoader = ImageLoader(context)
val request = ImageRequest.Builder(context)
.transformations(RoundedCornersTransformation(12.dp.value))
.data(imageUrl)
.build()
val imagePainter = rememberCoilPainter(
request = request,
imageLoader = imageLoader
)
LaunchedEffect(key1 = imagePainter) {
launch {
val result = (imageLoader.execute(request) as SuccessResult).drawable
val bitmap = (result as BitmapDrawable).bitmap
val vibrant = Palette.from(bitmap)
.generate()
.getVibrantColor(defaultColor)
// do something with vibrant color
}
}

I would suggest using the new coil-compose library. Just copy the following and add it to the app build.gradle file:
implementation "io.coil-kt:coil-compose:1.4.0"
I was also following the tutorial and got stuck at this point. I would suggest copying and pasting the following code:
Column {
val painter = rememberImagePainter(
data = entry.imageUrl
)
val painterState = painter.state
Image(
painter = painter,
contentDescription = entry.pokemonName,
modifier = Modifier
.size(120.dp)
.align(CenterHorizontally),
)
if (painterState is ImagePainter.State.Loading) {
CircularProgressIndicator(
color = MaterialTheme.colors.primary,
modifier = Modifier
.scale(0.5f)
.align(CenterHorizontally)
)
}
else if (painterState is ImagePainter.State.Success) {
LaunchedEffect(key1 = painter) {
launch {
val image = painter.imageLoader.execute(painter.request).drawable
viewModel.calcDominantColor(image!!) {
dominantColor = it
}
}
}
}
Text(
text = entry.pokemonName,
fontFamily = RobotoCondensed,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
}

Replace your "AsyncImage" with "AsyncImageWithDrawable" :
#Composable
fun AsyncImageWithDrawable(
model: Any?,
contentDescription: String?,
modifier: Modifier = Modifier,
placeholderResId: Int? = null,
errorResId: Int? = null,
fallbackResId: Int? = errorResId,
contentScale: ContentScale,
onDrawableLoad: (Drawable?) -> Unit) {
val painter = rememberAsyncImagePainter(
ImageRequest.Builder(LocalContext.current).data(data = model)
.apply(block = fun ImageRequest.Builder.() {
crossfade(true)
placeholderResId?.let { placeholder(it) }
errorResId?.let { error(it) }
fallbackResId?.let { fallback(it) }
allowHardware(false)
}).build()
)
val state = painter.state
Image(
painter = painter,
contentDescription = contentDescription,
modifier = modifier,
contentScale = contentScale
)
when (state) {
is AsyncImagePainter.State.Success -> {
LaunchedEffect(key1 = painter) {
launch {
val drawable: Drawable? =
painter.imageLoader.execute(painter.request).drawable
onDrawableLoad(drawable)
}
}
}
else -> {}
}
}
I've created this Composable inspired by #Aknk answer and Coil source code.
Hint: You can use this Composable to Load and Render your Image from Url and get your Image Palette by the returned Drawable.

You can use the onSuccess callback to get the drawable:
AsyncImage(
model = url,
contentDescription = null,
onSuccess = { success ->
val drawable = success.result.drawable
}
)
You can use the same approach also with rememberAsyncImagePainter.

Related

Scrolling is quite laggy in my implementation using jetpack compose paging

I tried to implement pagination in my app using compose. Here you can find full code : https://github.com/alirezaeiii/TMDb-Compose
First I am showing a loading indicator, and then loads items from TMDb API. So I have following composable method :
#Composable
fun <T : TMDbItem> PagingScreen(
viewModel: BasePagingViewModel<T>,
onClick: (TMDbItem) -> Unit,
) {
val lazyTMDbItems = viewModel.pagingDataFlow.collectAsLazyPagingItems()
when (lazyTMDbItems.loadState.refresh) {
is LoadState.Loading -> {
TMDbProgressBar()
}
is LoadState.Error -> {
val message =
(lazyTMDbItems.loadState.refresh as? LoadState.Error)?.error?.message ?: return
lazyTMDbItems.apply {
ErrorScreen(
message = message,
modifier = Modifier.fillMaxSize(),
refresh = { retry() }
)
}
}
else -> {
LazyTMDbItemGrid(lazyTMDbItems, onClick)
}
}
}
As you see in the else section it calls LazyTMDbItemGrid composable function :
#Composable
private fun <T : TMDbItem> LazyTMDbItemGrid(
lazyTMDbItems: LazyPagingItems<T>,
onClick: (TMDbItem) -> Unit
) {
LazyVerticalGrid(
columns = GridCells.Fixed(COLUMN_COUNT),
contentPadding = PaddingValues(
start = Dimens.GridSpacing,
end = Dimens.GridSpacing,
bottom = WindowInsets.navigationBars.getBottom(LocalDensity.current)
.toDp().dp.plus(
Dimens.GridSpacing
)
),
horizontalArrangement = Arrangement.spacedBy(
Dimens.GridSpacing,
Alignment.CenterHorizontally
),
content = {
items(lazyTMDbItems.itemCount) { index ->
val tmdbItem = lazyTMDbItems[index]
tmdbItem?.let {
TMDbItemContent(
it,
Modifier
.height(320.dp)
.padding(vertical = Dimens.GridSpacing),
onClick
)
}
}
lazyTMDbItems.apply {
when (loadState.append) {
is LoadState.Loading -> {
item(span = span) {
LoadingRow(modifier = Modifier.padding(vertical = Dimens.GridSpacing))
}
}
is LoadState.Error -> {
val message =
(loadState.append as? LoadState.Error)?.error?.message ?: return#apply
item(span = span) {
ErrorScreen(
message = message,
modifier = Modifier.padding(vertical = Dimens.GridSpacing),
refresh = { retry() })
}
}
else -> {}
}
}
})
}
In order to load images in the ImageView asynchronously, I have following function :
#Composable
private fun BoxScope.TMDbItemPoster(posterUrl: String?, tmdbItemName: String) {
val painter = rememberAsyncImagePainter(
model = posterUrl,
error = rememberVectorPainter(Icons.Filled.BrokenImage),
placeholder = rememberVectorPainter(Icons.Default.Movie)
)
val colorFilter = when (painter.state) {
is AsyncImagePainter.State.Loading, is AsyncImagePainter.State.Error -> ColorFilter.tint(
MaterialTheme.colors.imageTint
)
else -> null
}
val scale =
if (painter.state !is AsyncImagePainter.State.Success) ContentScale.Fit else ContentScale.FillBounds
Image(
painter = painter,
colorFilter = colorFilter,
contentDescription = tmdbItemName,
contentScale = scale,
modifier = Modifier
.fillMaxSize()
.align(Alignment.Center)
)
}
The problem is when I am scrolling very fast in the part of screen that images are not loaded yet, it is quite laggy. Based on my research there is no problem with my image loading using coil library, but I do not know why it is laggy. Do you have any suggestion about this?
Addenda :
I have reported the issue here : https://issuetracker.google.com/issues/264847068
You can try to refer to this issue address on github to solve it. At present, Coil is combined with Compose's LazyVerticalGrid and LazyColumn, and the sliding sense is lagging:
https://github.com/coil-kt/coil/issues/1337

How to make initials icons using Coil in Jetpack Compose

So I am using the Coil library for our image processing and I noticed in the place holder it only takes an int. I want however to display initials if a user does not have an avatar or incase of any error show initials, like this image see below. Problem is, I am new in jetpack compose and not sure how I can achieve this. See my code below.
I have this card that has icon, and some details my Profile Card
ProfileCard(
personName = String.format("%s %s", e.firstName, e.lastName),
personC = entity.program ?: "",
painter = rememberAsyncImagePainter(model = getProfileAvatar(entity.id)),
onCardClick = {})
My getProfileAvatar()
private fun getProfileAvatar(id: String) : ImageRequest {
val url = ServiceAPI.photoUrl(id)
return ImageRequest.Builder(requireContext())
.data(url)
.addHeader() )
.build() }
Will appreciate feedback, I did see a couple of post, but don't address the Jetpack part.
Coil has no built-in support for composable placeholders.
However you have different options.
You can use the SubcomposeAsyncImage using the painter.state to define different Composables:
SubcomposeAsyncImage(
model = url,
contentDescription = "contentDescription",
contentScale = ContentScale.Crop,
modifier = Modifier.clip(CircleShape)
) {
val state = painter.state
if (state is AsyncImagePainter.State.Loading || state is AsyncImagePainter.State.Error) {
//text with a background circle
Text(
modifier = Modifier
.padding(16.dp)
.drawBehind {
drawCircle(
color = Teal200,
radius = this.size.maxDimension
)
},
text = "NG",
style = TextStyle(color = Color.White, fontSize = 20.sp)
)
} else {
SubcomposeAsyncImageContent()
}
Also the placeholder parameter in the AsyncImage accepts a Painter. You can define your custom TextPainter
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.build(),
placeholder = TextPainter(
circleColor= Teal200,
textMeasurer = rememberTextMeasurer(),
text="NG",
circleSize = Size(200f, 200f)
),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.padding(16.dp)
)
where:
class TextPainter(val circleColor: Color,
val circleSize : Size,
val textMeasurer: TextMeasurer,
val text : String,
) : Painter() {
val textLayoutResult: TextLayoutResult =
textMeasurer.measure(
text = AnnotatedString(text),
style = TextStyle(color = Color.White, fontSize = 20.sp)
)
override val intrinsicSize: Size get() = circleSize
override fun DrawScope.onDraw() {
//the circle background
drawCircle(
color = circleColor,
radius = size.maxDimension/2
)
val textSize = textLayoutResult.size
//The text
drawText(
textLayoutResult = textLayoutResult,
topLeft = Offset(
(this.size.width - textSize.width) / 2f,
(this.size.height - textSize.height) / 2f
)
)
}
}
You can use coil's SubcomposeAsyncImage for that. It allows you to use any composable function as a placeholder/error state:
SubcomposeAsyncImage(
model = getProfileAvatar()
) {
val state = painter.state
if (state is AsyncImagePainter.State.Loading || state is AsyncImagePainter.State.Error) {
Text(text = "NG")
} else {
SubcomposeAsyncImageContent()
}
}
Example:
val personName by remember{ mutableStateOf(String.format("%s %s", entity.firstName, entity.lastName)) }
val painter = rememberAsyncImagePainter(
model = ImageRequest.Builder(LocalContext.current)
.allowHardware(false)
.data("https://xxxx.xxxx.user_avatar.jpg")
.size(Size.ORIGINAL)
.build()
)
val isErrorState = painter.state is AsyncImagePainter.State.Error
val textMeasure = rememberTextMeasurer()
val textLayoutResult = textMeasure.measure(text = buildAnnotatedString { append(personName) }, style = TextStyle(color = Color.White, fontSize = 16.sp))
ProfileCard(
modifier = Modifier.drawBehind {
if(isErrorState) {
drawText(textLayoutResult = textLayoutResult)
}
},
personName = personName,
personC = entity.program ?: "",
painter = rememberAsyncImagePainter(model = getProfileAvatar(entity.id)),
onCardClick = {}
)

Get image from Uri and show in composable with ViewModel

I have this composable that used to work fine, but now after some libraries update it doesn't.
I'm using a ViewModel to save an image returned from ActivityResultContracts.TakePicture() and show it in an Image within a Box.
The PhotoButton composable is working fine and return the correct image url, but the image string in my main composable is always null.
SamplePage.kt
#Composable
fun SamplePage(navController: NavController) {
val inputViewModel = InputViewModel()
val context = LocalContext.current
Column{
InputFields(inputViewModel, navController, setPerm)
}
}
#Composable
fun InputFields(inputViewModel: InputViewModel, navController: NavController) {
val image: String by inputViewModel.image.observeAsState("")
Column() {
Row(verticalAlignment = Alignment.CenterVertically) {
Box(contentAlignment = Alignment.Center) {
val painter = rememberImagePainter(data = image)
Image(
painter = painter,
contentScale = ContentScale.FillWidth,
contentDescription = null
)
if (painter.state !is ImagePainter.State.Success) {
Icon(
painter = painterResource(id = R.drawable.icon),
contentDescription = null
)
}
}
PhotoButton() {
inputViewModel.onImageChange(it)
}
}
}
}
class InputViewModel : ViewModel() {
private val _image: MutableLiveData<String> = MutableLiveData("")
val image: LiveData<String> = _image
fun onImageChange(newImage: String) {
_image.value = newImage
}
}
PhotoButton.kt
#Composable
fun PhotoButton(onValChange: ((String) -> Unit)?){
val context = LocalContext.current
val storageDir: File? = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val file = File(storageDir, "picFromCamera")
val uri = FileProvider.getUriForFile(
context,
context.packageName.toString() + ".provider",
file
)
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.TakePicture()) {
if (onValChange != null) {
onValChange(uri.toString())
}
}
FAB() {
launcher.launch(uri)
}
}
You're creating a new view model on each recomposition:
val inputViewModel = InputViewModel()
Instead you should use viewModel(): it'll create a new view model on the first call, and store it for the future calls:
val inputViewModel = viewModel<InputViewModel>()
Check out more about view models usage in compose state documentation.

How to set an image URL as error placeholder on Coil in Jetpack Compose

Coil accepts a drawable resource as an error placeholder. Is there a way to use an image URL here instead?
Here is the code I am working on:
// Global variables
var currentlySelectedImageUri = mutableStateOf<Uri?>(null)
var previousImageUri: Uri? = null
// #Composable fun() {...
Image(
painter = rememberImagePainter(
if (currentlySelectedImageUri.value != null) { // use the currently selected image
currentlySelectedImageUri.value
} else {
if (previousImageUri != null) { // use the previously selected image
previousImageUri
} else {
R.drawable.blank_profile_picture // use the placeholder image
}
}, builder = {
placeholder(R.drawable.blank_profile_picture)
error(R.drawable.blank_profile_picture) // FIXME: Set the previously selected image
}),
contentDescription = "profile image",
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxWidth()
)
In Coil 2.0.0 both AsyncImage and rememberAsyncImagePainter have error parameter that takes any other painter:
AsyncImage(
model = imageURL,
contentDescription = null,
error = painterResource(R.drawable.error)
)
Coil 1.4.0 version:
You can check painter.state value.
Initially it's ImagePainter.State.Empty, while image is loading it's ImagePainter.State.Loading, and if it failed - it becomes ImagePainter.State.Error. At this point you can change coil url, as an example, using local remember variable:
val localImagePainterUrl = remember { mutableStateOf<Uri?>(null) }
val painter = rememberImagePainter(
data = localImagePainterUrl.value
?: currentlySelectedImageUri.value
?: previousImageUri
?: R.drawable.blank_profile_picture,
builder = {
placeholder(R.drawable.blank_profile_picture)
})
val isError = painter.state is ImagePainter.State.Error
LaunchedEffect(isError) {
if (isError) {
localImagePainterUrl.value = previousImageUri
}
}
Image(
painter = painter,
...
)
There is a function inside coil ImageRequest Builder class
fun placeholder(#DrawableRes drawableResId: Int) = apply {
this.placeholderResId = drawableResId
this.placeholderDrawable = null
}
Usage:
Image(
painter = rememberImagePainter(
data = url,
builder = {
placeholder(R.drawable.your_placeholder_drawable) // or bitmap
}
)
)
UPDATE:
Also use: com.google.accompanist.placeholder
Dependency gradle: com.google.accompanist:accompanist-placeholder:accompanist_version
// accompanist_version = 0.19.0
Modifier.placeholder(
visible = true/false,
color = color,
highlight = PlaceholderHighlight.shimmer(color),
shape = imageShape // RectangleShape)
Actually there is an easier way from coil compose api , you can just add error(R.drawable.your_placeholder_drawable) to the builder and that's it
Image(painter = rememberImagePainter(data = url, builder = {error(R.drawable.your_placeholder_drawable)}))

Reload Coil Image Jetpack Compose

I need to reload/refresh the coil image because it has been rewritten to disk/storage
The code I am using
val context = LocalContext.current
val loader = if (!isSvg) {
LocalImageLoader.current ?: remember { ImageLoader(context) }
} else {
remember {
ImageLoader.Builder(context)
.componentRegistry {
add(SvgDecoder(context))
}.build()
}
}
val painter = rememberCoilPainter(request = request, imageLoader = loader)
LaunchedEffect(key1 = invalidate, block = {
val result = loader.execute(
ImageRequest.Builder(context)
.diskCachePolicy(if (fromCache) CachePolicy.ENABLED else CachePolicy.DISABLED)
.data(request).build()
)
Log.d("TL_ImageLoadRes", result.toString())
})
Box(modifier = Modifier.background(MaterialTheme.colors.onBackground.copy(.4f))) {
Image(
modifier = modifier,
painter = painter,
contentDescription = stringResource(R.string.random_image),
contentScale = contentScale
)
}
Every time invalidate changes , it means the image has been rewritten and now may be found and I execute the LaunchedEffect but after getting the success result I don't know where to put it , it contains a Drawable , I don't know what to do with it

Categories

Resources