Blog Infos
Author
Published
Topics
, , , , ,
Published
From drawables, from bitmaps, from a url — from anywhere!

This the next in my series of blog posts all about widgets. Check out Widgets with Glance: Blending in and Widgets with Glance: Standing out for some Widget UI tricks and tips. And for advanced state handling see Widgets with Glance: Beyond String States.

If you are getting into widgets, pretty soon you are going to want to display images, whether that be a simple icon or a lovely graphic to brighten up your user’s home screen. Depending on where your image is stored (within your app as a drawable or from the internet) there are a few methods and some are more straightforward than others.

Let’s start simple with drawables…

Displaying drawables

Displaying a local icon or image is easy from an app drawable resource, all you need is to create an ImageProvider, pass in the drawable resource ID and then use it in a Image Glance composable:

Image(
provider = ImageProvider(R.drawable.ic_launcher_foreground_mono),
contentDescription = "My image",
colorFilter = ColorFilter.tint(
GlanceTheme.colors.primary
),
contentScale = ContentScale.Fit,
modifier = GlanceModifier
)
view raw ImageWidget.kt hosted with ❤ by GitHub

In the above I have applied a ColorFilter to tint the image and a contentDescription for accessibility. Any sizing or padding can be set via the GlanceModifier as usual and contentScale will control how the image fits within the Image boundaries (more on this later).

Just a simple happy drawable displaying on a widget. The drawable is emoji by Maxim Kulikov from The Noun Project

Now, this is pretty simple, what if we want the widget to display a pretty photo? If it is stored as a drawable resource then this is exactly the same:

Image(
provider = ImageProvider(R.drawable.mountain),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = GlanceModifier.fillMaxSize()
)
view raw ImageWidget.kt hosted with ❤ by GitHub

Looking pretty. Image from Lorem Picsum

Here I have expanded the image to fit the space and used ContentScale.Crop so it maintains the original aspect ratio and displays edge to edge in my widget.

Glance ContentScale options

Let’s go back to looking at ContentScale. As usual, with Jetpack Glance we have fewer options than we have with regular Jetpack Compose. The ContentScale options are:

  • ContentScale.Crop — Scale uniformly (maintaining aspect ratio) so that both dimensions (width and height) will be equal to or larger than the corresponding dimension of the image composable.
  • ContentScale.Fit — Scale uniformly (maintaining aspect ratio) so that both dimensions (width and height) will be equal to or less than the corresponding dimension of the image composable
  • ContentScale.FillBounds — Scale horizontal and vertically non-uniformly to fill the image composable bounds.
Displaying an image from a url

Now, we get to what you are probably reading this article for. How to display an image from the internet in a widget. Unfortunately we can’t just directly use your favourite image loading library like CoilGlide or Picasso as they don’t have Glance composables that we can put in Glance widget UI code (for various reasons). Instead, we need to load the image first into a bitmap, then use ImageProvider(bitmap) to load this in our Image Glance composable.

For this, we have two options depending on the use case:

  1. Load the image as a bitmap using a background thread
  2. Use a coroutine worker to download the image as a file
Image Bitmap with a background thread

This is the simplest option and is useful for widgets where the image is independent from the rest of the widget display (see below for when you wouldn’t want to use this option).

The first step is making use of Coil ImageRequest to download the image as a bitmap.

val context = LocalContext.current
var loadedBitmap by remember(imageUrl) { mutableStateOf<Bitmap?>(null) }
LaunchedEffect(imageUrl) {
withContext(Dispatchers.IO) {
val request = ImageRequest.Builder(context).data(imageUrl).apply {
memoryCachePolicy(CachePolicy.DISABLED)
diskCachePolicy(CachePolicy.DISABLED)
}.build()
// Request the image to be loaded and return null if an error has occurred
loadedBitmap = when (val result = context.imageLoader.execute(request)) {
is ErrorResult -> null
is SuccessResult -> result.drawable.toBitmapOrNull()
}
}
}
view raw ImageWidget.kt hosted with ❤ by GitHub

In the above I am explicitly disabling the image caching in the request, but you can alter this depending what your image is used for.

By placing this in a LaunchedEffect with a a context of Dispatches.IO we can run this code on the background thread and not lock up the UI thread.

Saving the result into a mutableState then enables the rest of the composable to respond once the image has been downloaded:

loadedBitmap.let { bitmap ->
if (bitmap != null) {
Image(
provider = ImageProvider(bitmap),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = modifier,
)
} else {
CircularProgressIndicator()
}
}
view raw ImageWidget.kt hosted with ❤ by GitHub

While the loadedBitmap state is null we can show a loading indicator and once it is ready, display the image using the Image Glance composable and a ImageProvider.

Now this works well:

The author information is fetched during the first “Loading”, the image is downloaded while the spinner is displayed.

Except in this case, it doesn’t make sense to show the image author before we actually show the image. Also, if an error occurs during loading we just have this circular progress indicator without any way of recovery.

We could fix this by adding more logic to the UI to set success & error states based on the image download but this is more brittle especially when we have data coming from different sources (in this case metadata from one endpoint and image from another — for the full example see my widget demo github repository).

So ideally we don’t want to use this method if showing the rest of the widget without the image makes no sense.

To load everything together and respond to the whole data state use a Coroutine Worker.

Image file with a Coroutine Worker

This method, while more complex, is useful if you are doing other background processing (such as fetching other data from a remote source) and want the image and the new data state to be available at the same time.

For this you can use string states using the standard PreferencesGlanceStateDefinition but I recommend using a CustomGlanceStateDefinition so that the different widget data states (e.g. success, loading & error) are more clearly defined. You can find more details on how to implement this and the Coroutine Worker itself in my previous blog post Widgets with Glance: Beyond String States.

To achieve this the first task is to download the image as a temporary file on the device that we can read later from the widget. We can do this using Coil and the image cache:

@OptIn(ExperimentalCoilApi::class)
private suspend fun downloadImage(
url: String,
context: Context,
applicationContext: Context,
force: Boolean
): String {
val request = ImageRequest.Builder(context)
.data(url)
.build()
// Request the image to be loaded and throw error if it failed
with(context.imageLoader) {
if (force) {
diskCache?.remove(url)
memoryCache?.remove(MemoryCache.Key(url))
}
val result = execute(request)
if (result is ErrorResult) {
throw result.throwable
}
}
// Next... find the image in the cache
}

In this case we have enabled the Coil image cache — we want the image to be cached so we can fetch the file from the cache.

So here, the image file is downloaded into the cache using the url as the cache key so we can retrieve it later (the url is the key by default).

This method should be in your Coroutine Worker for the widget and called within the doWork method when you update the rest of the widget state.

Next, we need to access the Coil image cache. For this we need a FileProvider and specify which files we will be accessing:

<manifest>
<application>
...
<provider
android:name=".widget.image.ImageFileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/image_paths" />
</provider>
</application>
</manifest>
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="widget_images" path="image_cache/" />
</paths>
view raw image_paths.xml hosted with ❤ by GitHub
class ImageFileProvider : FileProvider()

Setting up this provider enables us to access the files in theimage_cache directory which is the default used by Coil.

Now, we can use this in the downloadImage method:

private suspend fun downloadImage(...): String {
...
// Get the path of the loaded image from DiskCache.
val path = context.imageLoader.diskCache?.openSnapshot(url)?.use { snapshot ->
val imageFile = snapshot.data.toFile()
// Use the FileProvider to create a content URI
val contentUri = getUriForFile(
context,
"${applicationContext.packageName}.provider",
imageFile,
)
// return the path
contentUri.toString()
}
return requireNotNull(path) {
"Couldn't find cached file"
}
}

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Building Modern Responsive Widgets with Jetpack Glance

In this lightning talk, we’ll explore how to get started with building responsive widgets using Jetpack Glance, a modern toolkit designed to simplify and enhance the widget development experience on Android.
Watch Video

Building Modern Responsive Widgets with Jetpack Glance

Jonathan Petit-Frere
Software Engineer,
Android at Netflix

Building Modern Responsive Widgets with Jetpack Glance

Jonathan Petit-Fre ...
Software Engineer,
Android at Netflix

Building Modern Responsive Widgets with Jetpack Glance

Jonathan Petit-F ...
Software Engineer,
Android at Netflix

Jobs

No results found.

Permission handling for File Providers

If you are using a FileProvider you may encouter something like this error:

Permission Denial: opening provider com.your.package.ImageFileProvider from ProcessRecord{...} that is not exported from UID xxxxx

This will usually occur if you are displaying (or changing) an image in your widget a long time after your app was last open. What happens here is that the permission for the URI is lost when the MainActivity is lost (such as being naturally closed in the background). If you have recently installed or updated your app (such as debugging and deploying to your device) you are unlikely to see this as your activity is still open. (Thanks to this Stack Overflow post for the explanation).

To fix this we need to request the permission prior to accessing the image. This won’t show anything to the user as it is not a restricted or special permission. To do this, add the following to the downloadImage method:

private suspend fun downloadImage(...): String {
...
// Get the path of the loaded image from DiskCache.
val path = context.imageLoader.diskCache?.openSnapshot(url)?.use { snapshot ->
...
// Find the current launcher every time to ensure it has read permissions
val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_HOME) }
val resolveInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.resolveActivity(
intent,
PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong())
)
} else {
context.packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
}
val launcherName = resolveInfo?.activityInfo?.packageName
if (launcherName != null) {
// Request the permission
context.grantUriPermission(
launcherName,
contentUri,
FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_PERSISTABLE_URI_PERMISSION,
)
}
// return the path
contentUri.toString()
}
return requireNotNull(path) {
"Couldn't find cached file"
}
}

Now, the image and data load at the same time!

The complete downloadImage method that returns the file url:

@OptIn(ExperimentalCoilApi::class)
private suspend fun downloadImage(
url: String,
context: Context,
applicationContext: Context,
force: Boolean
): String {
val request = ImageRequest.Builder(context)
.data(url)
.build()
// Request the image to be loaded and throw error if it failed
with(context.imageLoader) {
if (force) {
diskCache?.remove(url)
memoryCache?.remove(MemoryCache.Key(url))
}
val result = execute(request)
if (result is ErrorResult) {
throw result.throwable
}
}
// Get the path of the loaded image from DiskCache.
val path = context.imageLoader.diskCache?.openSnapshot(url)?.use { snapshot ->
val imageFile = snapshot.data.toFile()
// Use the FileProvider to create a content URI
val contentUri = getUriForFile(
context,
"${applicationContext.packageName}.provider",
imageFile,
)
// Find the current launcher everytime to ensure it has read permissions
val intent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_HOME) }
val resolveInfo = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.packageManager.resolveActivity(
intent,
PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()),
)
} else {
@Suppress("DEPRECATION")
context.packageManager.resolveActivity(
intent,
PackageManager.MATCH_DEFAULT_ONLY,
)
}
val launcherName = resolveInfo?.activityInfo?.packageName
if (launcherName != null) {
context.grantUriPermission(
launcherName,
contentUri,
FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_PERSISTABLE_URI_PERMISSION,
)
}
// return the path
contentUri.toString()
}
return requireNotNull(path) {
"Couldn't find cached file"
}
}

. . .

To see a full example, see my sample widget app:

. . .

 

For more widget blog posts, check out the others in my Widgets with glance series:
Widgets with Glance: Blending inWidgets with Glance: Standing out and Widgets with Glance: Beyond String States

This article is previously published on proandroiddev.com.

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Using annotations in Kotlin has some nuances that are useful to know
READ MORE
blog
One of the latest trends in UI design is blurring the background content behind the foreground elements. This creates a sense of depth, transparency, and focus,…
READ MORE
blog
Now that Android Studio Iguana is out and stable, I wanted to write about…
READ MORE
blog
The suspension capability is the most essential feature upon which all other Kotlin Coroutines…
READ MORE
Menu