glance-appwidget-updates-and-memory
Skill1k starsChanged 4 days agoApache-2.0View on GitHub
Manages Android AppWidget state updates, background syncs, and RemoteViews
We split this skill into the rules it gives an agent
12 rules. About 0 are common, 1 is distinctive, and 11 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
11
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 49
When configuring a Glance widget, see Create an app widget with Glance.
- Very specific: line 50
When targeting Android 17 (targetSdk = 37), see Android 17 widget memory limits.
- Very specific: line 51
When maintaining a classic AppWidgetProvider, see Create an app widget.
- Very specific: line 72
Read the companion reference files in references/ when implementing each step, and update the sample package declaration and class names to …
- Very specific: line 86
Read gradle/libs.versions.toml, app/build.gradle.kts, and app/src/main/AndroidManifest.xml first.
- Very specific: line 109
Read WidgetBitmapUtils.kt and implement the memory cap and downsampling helpers:
- Very specific: line 137
Read SampleGlanceWidget.kt to configure multi-size bitmap deduplication and composition error recovery:
- Very specific: line 140
Choose
SizeModefor your layout: : Use SizeMode.Exact (recommended when using glance-canonical-layouts to render exact LocalSize.current … - Very specific: line 149
Read FeedCollectionGlanceWidget.kt when rendering collection widgets with LazyColumn:
- Very specific: line 165
Read WidgetStateAndActions.kt, proguard-rules.pro, and ClassicWidgetImageUpdater.kt to implement these patterns:
- Very specific: line 197
NEVER: call updateAppWidgetState without calling GlanceAppWidget.update(context, glanceId) or GlanceAppWidget.updateAll(context).
- Very specific: line 198
NEVER: call partiallyUpdateAppWidget when you replace widget images.
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
This skill provides guidance for managing state updates and keeping Android
AppWidgets that use Jetpack Glance (androidx.glance:glance-appwidget) or
RemoteViews within the Android 17 widget bitmap memory limits. For widget UI
layout templates, suggest the glance-canonical-layouts skill.
- When configuring a Glance widget, see Create an app widget with Glance.
- When targeting Android 17 (
targetSdk = 37), see Android 17 widget memory limits. - When maintaining a classic
AppWidgetProvider, see Create an app widget.
Prerequisites
- Kotlin Android project (
minSdk >= 26) withcompileSdk = 37(targetSdk = 37when targeting Android 17) inbuild.gradle.kts. - Jetpack Glance (
1.2.0, minimum1.1.1+) and the Compose compiler Gradle plugin enabled on the module.
Critical mandates
- Preserve existing package name and toolchain versions : Inspect
build.gradle.kts,gradle/libs.versions.toml, and Kotlin files first. Keep existingnamespace,applicationId,package,targetSdk(unless migrating to Android 17), Android Gradle Plugin (AGP), and Kotlin versions. - Downsample images to widget dimensions and estimate the memory cap : Android
system_servercomputes theRemoteViewsbitmap memory cap (6 * width * heightbytes, or1.5 * 4 * width * height) once at boot from the real size of the default display and does not expose this cap in a public API. Because unfolding a foldable or switching display resolution toQHD+after boot can make active window metrics report a larger value than the boot-time cap, always downsample each image first to the widget's target dimensions (maxWidthDp x maxHeightDpinRGB_565orALPHA_8), and enforce the per-image byte ceiling (calculateWidgetMemoryCapBytes(context) / distinctImageCount) estimated as6 * width * heightfromWindowManager.currentWindowMetrics.boundson API 30+ (falling back toresources.displayMetricson API 26-29 or ifWindowManageris unavailable). - Decode shared bitmaps once outside provideContent : Both
SizeMode.Exact(used byglance-canonical-layoutsto compose the current span's portrait and landscape orientations) andSizeMode.Responsive(used to pre-bundle a set ofDpSizebreakpoints) invokeprovideContentmultiple times per update. Decode shared bitmaps once inprovideGlancebeforeprovideContentand pass them toImageProvider(bitmap)soRemoteViewsdeduplicates the sharedBitmapacross all sizes in either mode. If a widget scales distinct bitmaps perLocalSize.current, dividecalculateWidgetMemoryCapBytes(context)by the number of distinct bitmaps. - Downsample content URIs and grant launcher read permissions : On Android 17 (
targetSdk >= 37), bitmap-backedIcons count toward the cap alongsideBitmaps.content://URIs passed toImageProvider(uri)orRemoteViews.setImageViewUridon't count toward the inter-process communication (IPC) cap, but the launcher decodes them at full resolution. Downsample the file intocontext.cacheDir, expose it withFileProvider.getUriForFile, and grantIntent.FLAG_GRANT_READ_URI_PERMISSIONtoIntent.CATEGORY_HOMEpackages. - Trigger explicit widget updates after state mutations :
updateAppWidgetStatewrites preferences to DataStore without updating the widget UI. After you mutate state, callGlanceAppWidget.update(context, glanceId)orGlanceAppWidget.updateAll(context). - Use full updateAppWidget calls when you replace images : In
AppWidgetProviderclasses, when you replace bitmaps orIcons, callAppWidgetManager.updateAppWidgetinstead ofpartiallyUpdateAppWidget.
Reference implementation files
Read the companion reference files in references/ when implementing each step,
and update the sample package declaration and class names to match your app:
- Bitmap Cap & FileProvider Helper : WidgetBitmapUtils.kt
- Responsive Widget, Receiver & Error Fallback : SampleGlanceWidget.kt
- LazyColumn Collection Widget & Revision Cache : FeedCollectionGlanceWidget.kt
- DataStore State Push, CoroutineWorker & ActionCallback : WidgetStateAndActions.kt
- Classic AppWidgetProvider WorkManager & Full Update : ClassicWidgetImageUpdater.kt
- Manifest, Widget Info, FileProvider Paths & R8 Rules : AndroidManifest.xml, sample_widget_info.xml, file_paths.xml, proguard-rules.pro
Detailed steps
Step 1: Configure SDK levels, dependencies, and manifest declarations
- Read
gradle/libs.versions.toml,app/build.gradle.kts, andapp/src/main/AndroidManifest.xmlfirst. If AGP, Kotlin, or library versions are already present, keep them; otherwise, addglance = "1.2.0"along withandroidx.glance:glance-appwidgetandandroidx.glance:glance-material3togradle/libs.versions.toml. - In
app/build.gradle.kts, keepnamespace,applicationId, and any existingminSdk >= 26,targetSdk, or library versions. SetcompileSdk = 37(andtargetSdk = 37only when explicitly migrating the app to target Android 17), enablebuildFeatures { compose = true }, and addimplementation(libs.androidx.glance.appwidget)andimplementation(libs.androidx.glance.material3). - In
AndroidManifest.xml(see AndroidManifest.xml), declare<queries>with<action android:name="android.intent.action.MAIN" />and<category android:name="android.intent.category.HOME" />soqueryIntentActivitiesdiscovers launcher packages on API 30+. Register yourGlanceAppWidgetReceiver(pointingandroid.appwidget.providerto@xml/sample_widget_info, see sample_widget_info.xml) and registerandroidx.core.content.FileProviderwithandroid:authorities="${applicationId}.fileprovider",android:exported="false",android:grantUriPermissions="true", andandroid.support.FILE_PROVIDER_PATHSpointing to@xml/file_paths(which declares<cache-path name="widget_cache" path="widget_images/" />, see file_paths.xml):
<br /><receiver android:name=".glance.MyReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/my_app_widget_info" />
</receiver><br />Step 2: Estimate the widget memory cap and downsample images
Read WidgetBitmapUtils.kt and implement the memory cap and downsampling helpers:
- Estimate the cap (
calculateWidgetMemoryCapBytes) : On API 30+ (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R), readval bounds = context.getSystemService(WindowManager::class.java)?.currentWindowMetrics?.boundsand, whenbounds != null && bounds.width() > 0 && bounds.height() > 0, return6L * bounds.width().toLong() * bounds.height().toLong()(callingWindowManagerfrom a non-UI widget or workerContextlogs a nonfatalStrictModeviolation on API 30+, which is expected for this estimate). On API 26-29 (or ifWindowManageris unavailable), fall back tocontext.resources.displayMetrics(6L * dm.widthPixels.toLong() * dm.heightPixels.toLong()). Becausesystem_servercaches6 * width * heightonce at boot, always size images to the widget'smaxWidthDp x maxHeightDpfirst (Stage 1) rather than filling the full screen budget. - Downsample in two stages (
decodeDownsampledWidgetBitmap) :- First, probe dimensions with
BitmapFactory.Options().apply { inJustDecodeBounds = true; inPreferredConfig = config }and doublesampleSize *= 2while(bounds.outWidth / (sampleSize * 2) >= targetWidthPx && bounds.outHeight / (sampleSize * 2) >= targetHeightPx) || (bounds.outWidth / sampleSize > targetWidthPx * 2) || (bounds.outHeight / sampleSize > targetHeightPx * 2)(wheretargetWidthPx = max(1, (maxWidthDp * density).toInt())), so wide banners or tall screenshots do not bypass downsampling when one axis already fits. - Second, enforce the per-image byte ceiling (
perImageCapBytes = calculateWidgetMemoryCapBytes(context) / max(1, distinctImageCount)) by doublingsampleSize *= 2while(max(1, bounds.outWidth / sampleSize).toLong() * max(1, bounds.outHeight / sampleSize).toLong() * bytesPerPixel) > perImageCapBytes. Use2Lbytes per pixel forBitmap.Config.RGB_565(opaque photos),1LforBitmap.Config.ALPHA_8masks, and4Lwhenbounds.outConfig != null && bounds.outConfig != config(becauseBitmapFactoryupgrades translucent PNG or WebP files fromRGB_565to 4-byteARGB_8888). Decode withinSampleSize = sampleSizeandinPreferredConfig = config.
- First, probe dimensions with
- Choose between in-memory
BitmapandFileProviderUri(cacheWidgetImageUri) : UsedecodeDownsampledWidgetBitmapwithandroidx.glance.ImageProvider(bitmap)when passing bitmaps directly in memory. UsecacheWidgetImageUriwhen the widget passes acontent://UritoImageProvider(uri)orRemoteViews.setImageViewUri: it writes the downsampled bitmap intoFile(context.cacheDir, "widget_images"), obtains acontent://URI withFileProvider.getUriForFile(context, "${context.packageName}.fileprovider", outFile), queriesIntent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME), and callscontext.grantUriPermission(pkg, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION).
Step 3: Deduplicate multi-size bitmaps and handle composition errors
Read SampleGlanceWidget.kt to configure multi-size bitmap deduplication and composition error recovery:
- Choose
SizeModefor your layout : UseSizeMode.Exact(recommended when usingglance-canonical-layoutsto render exactLocalSize.currentdimensions for the current grid span's portrait and landscape orientations),SizeMode.Responsive(RESPONSIVE_BUCKETS)(usingimport androidx.compose.ui.unit.DpSizeandimport androidx.compose.ui.unit.dpto pre-bundle fixedDpSizebreakpoints such assetOf(DpSize(120.dp, 120.dp), DpSize(240.dp, 120.dp), DpSize(240.dp, 240.dp))so resize transitions don't wake the app process), orSizeMode.Single(for a single layout). - Hoist shared bitmap decodes before
provideContent: Because bothSizeMode.Exact(2-4orientation sizes) andSizeMode.Responsive(Nbuckets) runprovideContentmultiple times per update, decode shared bitmaps once inprovideGlancebeforeprovideContentwithdecodeDownsampledWidgetBitmapandBitmap.Config.RGB_565for opaque images (distinctImageCount = 1). Pass the resultingBitmaptoandroidx.glance.ImageProvider(sharedBitmap)soRemoteViewsdeduplicates the sharedBitmapacross all sizes. - Provide a zero-bitmap error fallback : Override
onCompositionError(context, glanceId, appWidgetId, throwable)and callAppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, fallback)with a text-onlyRemoteViewslayout (0bitmaps).
Step 4: Pre-decode LazyColumn collection images and assign stable itemIds
Read FeedCollectionGlanceWidget.kt when rendering collection widgets with
LazyColumn:
- Replace
import androidx.glance.appwidget.ImageProvider(which acceptsUri) withimport androidx.glance.ImageProvider(which acceptsBitmap). RemoveUri.fromFileandIcon.createWithContentUricalls. - While a Glance session is active (~45 seconds after an update),
.update()and.updateAll()do not restartprovideGlance. Declarevar cachedRevision = -1andvar cachedThumbnails = emptyMap<String, Bitmap>()inprovideGlancebeforeprovideContent. InsideprovideContent, readval revision = currentState<Preferences>()[KEY_REVISION] ?: 0, and only whenrevision != cachedRevision, updatecachedRevision = revisionand decode thefeedItemsthumbnails withcachedThumbnails = feedItems.mapNotNull { item -> decodeDownsampledWidgetBitmap(..., distinctImageCount = max(1, feedItems.size), config = Bitmap.Config.RGB_565)?.let { item.id to it } }.toMap()(usemapNotNullrather thanassociateso the map staysMap<String, Bitmap>instead ofMap<String, Bitmap?>). This re-decodes when DataStore state updates during an active session while sharing one decodedMap<String, Bitmap>across allSizeMode.Responsivebuckets. - In
LazyColumn, pass a stableLongitemIdtoitems(items = feedItems, itemId = { item -> item.id.hashCode().toLong() })(preferring a persistent numeric ID when available) and renderImage(provider = ImageProvider(bitmap), contentDescription = item.title)only whenbitmap != null(never fall back toUri.fromFilein anelsebranch).
Step 5: Push state updates, configure ActionCallbacks, and use WorkManager
Read WidgetStateAndActions.kt, proguard-rules.pro, and ClassicWidgetImageUpdater.kt to implement these patterns:
- Push DataStore updates (requires
androidx.datastore:datastore-preferences-core:1.2.1andimport androidx.glance.appwidget.GlanceAppWidgetManager,import androidx.glance.appwidget.state.updateAppWidgetState, andimport androidx.glance.appwidget.updateAll): InpushWidgetRevisionUpdate(context)(see WidgetStateAndActions.kt), iterate overfor (glanceId in GlanceAppWidgetManager(context).getGlanceIds(Widget::class.java)), mutate state insideupdateAppWidgetState(context, glanceId) { prefs -> ... }(passing bothcontextandglanceId), and immediately callwidget.update(context, glanceId)(orwidget.updateAll(context)). - Use zero-argument
ActionCallbackclasses withActionParameters.Keyand keep<init>()under R8 (requiresandroidx.work:work-runtime-ktx:2.12.0when scheduling background work):RunCallbackActioninstantiatesActionCallbackclasses withClass.getDeclaredConstructor().newInstance(), so a constructor parameter fails in every build. Remove constructor parameters fromActionCallbackimplementations, pass values withActionParameters.KeyandactionParametersOf(seeRefreshWidgetActionin WidgetStateAndActions.kt), and add-keep class * implements androidx.glance.appwidget.action.ActionCallback { public <init>(); }toproguard-rules.pro(see proguard-rules.pro). InsideSyncWidgetWorker.doWork(), callpushWidgetRevisionUpdate(applicationContext)so background work updates DataStore and refreshes the widget. - Avoid unconditional one-time
WorkManagerenqueues inAppWidgetProvider.onUpdateand use fullupdateAppWidgetfor images (see ClassicWidgetImageUpdater.kt): Never callcontext.startForegroundServicefromAppWidgetProvider.onReceiveoronUpdate(which throwsForegroundServiceStartNotAllowedException).GlanceAppWidgetReceiveralready keeps a 10-year delayed session worker queued internally so WorkManager'sRescheduleReceivernever toggles. In a classicAppWidgetProvider, avoid enqueuing aOneTimeWorkRequestdirectly insideonUpdate: even withExistingWorkPolicy.KEEP, once the one-time worker finishes (SUCCEEDED),UnfinishedWorkListenerdisablesRescheduleReceiver, which broadcastsACTION_PACKAGE_CHANGEDand re-triggersonUpdatein a loop. Instead, schedule repeating syncs inonUpdatewithenqueueUniquePeriodicWork(..., ExistingPeriodicWorkPolicy.KEEP, ...)(which staysENQUEUEDacross runs), usegoAsync()for short< 10supdates, or enqueue one-time work (enqueueUniqueWork(..., ExistingWorkPolicy.KEEP, ...)) only from specific action broadcasts inonReceive. When you update aBitmaporIconon a classicRemoteViewswidget, callappWidgetManager.updateAppWidgetrather thanpartiallyUpdateAppWidget.
Common errors and fixes
| Symptom or exception | Root cause | Fix |
IllegalArgumentException: RemoteViews for widget update exceeds maximum bitmap memory usage | RemoteViews bitmap or icon memory exceeded 6 * W * H bytes. | Downsample to the widget's maxWidthDp x maxHeightDp with inSampleSize and RGB_565 (or ALPHA_8 for masks), enforce calculateWidgetMemoryCapBytes(context) / distinctImageCount, and decode shared bitmaps once before provideContent. |
SecurityException: Permission Denial: opening provider or blank content:// widget image | Home launcher lacks read permission for the FileProvider content:// URI or app passed file:// with Uri.fromFile. | Expose the file with FileProvider, add <queries> for CATEGORY_HOME, and call context.grantUriPermission with Intent.FLAG_GRANT_READ_URI_PERMISSION. |
Widget UI stays frozen after updateAppWidgetState | updateAppWidgetState writes to DataStore without recomposing, or provideGlance was not re-triggered during an active session. | Call .update(context, glanceId) or .updateAll(context) after updateAppWidgetState, and observe revision state inside provideContent. |
NoSuchMethodException: <Class>.<init> [] on action tap | ActionCallback declared constructor parameters or R8 stripped <init>(). | Use a public zero-argument constructor, pass values with ActionParameters.Key, and keep <init>() in ProGuard. |
ForegroundServiceStartNotAllowedException in receiver | AppWidgetProvider called startForegroundService in the background. | Schedule periodic syncs in onUpdate with enqueueUniquePeriodicWork (ExistingPeriodicWorkPolicy.KEEP) or enqueue one-time work (enqueueUniqueWork) from custom action broadcasts in onReceive. |
Unresolved reference: ImageProvider, DpSize, updateAppWidgetState, or updateAll | Missing or mismatched Glance and Compose package imports, or calling updateAppWidgetState(context) without glanceId. | Import androidx.glance.ImageProvider (for Bitmap), androidx.compose.ui.unit.DpSize and androidx.compose.ui.unit.dp (for SizeMode.Responsive), androidx.glance.appwidget.state.updateAppWidgetState (passing both context and glanceId), and androidx.glance.appwidget.updateAll. |
Antipatterns
- NEVER decode or scale a
BitmapinsideprovideContentper size bucket withSizeMode.ResponsiveorSizeMode.Exact. - NEVER size widget bitmaps to fill the full screen memory cap (
6 * width * height); always downsample to the widget's targetmaxWidthDp x maxHeightDpfirst so unfolding a foldable or switching display resolution after boot does not exceed the boot-time cap. - NEVER pass an unscaled
content://UriorUri.fromFiletoImageProvider(uri)orRemoteViews.setImageViewUri, or omitcontext.grantUriPermissionforIntent.CATEGORY_HOME. - NEVER call
updateAppWidgetStatewithout callingGlanceAppWidget.update(context, glanceId)orGlanceAppWidget.updateAll(context). - NEVER call
partiallyUpdateAppWidgetwhen you replace widget images.
In this repository14 other files it gives its agents.
- AGENTS.mdAGENTS.md
- adaptiveSkill
- android-profilerSkill
- anr-audit-and-fixSkill
- edge-to-edgeSkill
- glance-canonical-layoutsSkill
- jetpack-compose-performanceSkill
- litert-lmSkill
- litert-runtimeSkill
- ml-kit-genai-prompt-apiSkill
- navigation-3Skill
- navigation-eventSkill
- r8-analyzerSkill
- testing-setupSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

