glance-appwidget-updates-and-memory
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseThis skill provides guidance for managing state updates and keeping Android
AppWidgets that use Jetpack Glance () or
within the Android 17 widget bitmap memory limits. For widget UI
layout templates, suggest the skill.
androidx.glance:glance-appwidgetRemoteViewsglance-canonical-layouts- When configuring a Glance widget, see Create an app widget with Glance.
- When targeting Android 17 (), see Android 17 widget memory limits.
targetSdk = 37 - When maintaining a classic , see Create an app widget.
AppWidgetProvider
本 skill 提供指导,帮助管理状态更新,并确保使用 Jetpack Glance () 或
的 Android AppWidget 保持在 Android 17 小组件位图内存限制内。对于小组件 UI
布局模板,建议使用 skill。
androidx.glance:glance-appwidgetRemoteViewsglance-canonical-layouts- 配置 Glance 小组件时,请参阅 使用 Glance 创建应用小组件。
- 面向 Android 17 () 时,请参阅 Android 17 小组件内存限制。
targetSdk = 37 - 维护传统式 时,请参阅 创建应用小组件。
AppWidgetProvider
Prerequisites
前置条件
- Kotlin Android project () with
minSdk >= 26(compileSdk = 37when targeting Android 17) intargetSdk = 37.build.gradle.kts - Jetpack Glance (, minimum
1.2.0) and the Compose compiler Gradle plugin enabled on the module.1.1.1+
- Kotlin Android 项目(),其
minSdk >= 26中配置了build.gradle.kts(针对 Android 17 时为compileSdk = 37)。targetSdk = 37 - 模块已启用 Jetpack Glance (,最低
1.2.0) 和 Compose compiler Gradle plugin。1.1.1+
Critical mandates
关键要求
- Preserve existing package name and toolchain versions : Inspect ,
build.gradle.kts, and Kotlin files first. Keep existinggradle/libs.versions.toml,namespace,applicationId,package(unless migrating to Android 17), Android Gradle Plugin (AGP), and Kotlin versions.targetSdk - Downsample images to widget dimensions and estimate the memory cap : Android computes the
system_serverbitmap memory cap (RemoteViewsbytes, or6 * 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 to1.5 * 4 * width * heightafter 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 (QHD+inmaxWidthDp x maxHeightDporRGB_565), and enforce the per-image byte ceiling (ALPHA_8) estimated ascalculateWidgetMemoryCapBytes(context) / distinctImageCountfrom6 * width * heighton API 30+ (falling back toWindowManager.currentWindowMetrics.boundson API 26-29 or ifresources.displayMetricsis unavailable).WindowManager - Decode shared bitmaps once outside provideContent : Both (used by
SizeMode.Exactto compose the current span's portrait and landscape orientations) andglance-canonical-layouts(used to pre-bundle a set ofSizeMode.Responsivebreakpoints) invokeDpSizemultiple times per update. Decode shared bitmaps once inprovideContentbeforeprovideGlanceand pass them toprovideContentsoImageProvider(bitmap)deduplicates the sharedRemoteViewsacross all sizes in either mode. If a widget scales distinct bitmaps perBitmap, divideLocalSize.currentby the number of distinct bitmaps.calculateWidgetMemoryCapBytes(context) - Downsample content URIs and grant launcher read permissions : On Android 17 (), bitmap-backed
targetSdk >= 37s count toward the cap alongsideIcons.BitmapURIs passed tocontent://orImageProvider(uri)don't count toward the inter-process communication (IPC) cap, but the launcher decodes them at full resolution. Downsample the file intoRemoteViews.setImageViewUri, expose it withcontext.cacheDir, and grantFileProvider.getUriForFiletoIntent.FLAG_GRANT_READ_URI_PERMISSIONpackages.Intent.CATEGORY_HOME - Trigger explicit widget updates after state mutations : writes preferences to DataStore without updating the widget UI. After you mutate state, call
updateAppWidgetStateorGlanceAppWidget.update(context, glanceId).GlanceAppWidget.updateAll(context) - Use full updateAppWidget calls when you replace images : In classes, when you replace bitmaps or
AppWidgetProviders, callIconinstead ofAppWidgetManager.updateAppWidget.partiallyUpdateAppWidget
- 保留现有包名和工具链版本:先检查 、
build.gradle.kts和 Kotlin 文件。保留现有的gradle/libs.versions.toml、namespace、applicationId、package(除非迁移到 Android 17)、Android Gradle Plugin (AGP) 和 Kotlin 版本。targetSdk - 将图片降采样到小组件尺寸并估算内存上限:Android 会在启动时根据默认显示器的实际尺寸计算一次
system_server位图内存上限(RemoteViews字节,或6 * width * height),且不会通过公共 API 公开该上限。由于启动后展开可折叠设备或切换到1.5 * 4 * width * height分辨率,可能使活动窗口指标报告的值大于启动时计算的上限,因此始终先将每张图片降采样到小组件的目标尺寸(QHD+,使用maxWidthDp x maxHeightDp或RGB_565),并执行单张图片的字节上限(ALPHA_8);在 API 30+ 上,该上限通过calculateWidgetMemoryCapBytes(context) / distinctImageCount按WindowManager.currentWindowMetrics.bounds估算(API 26-29 或6 * width * height不可用时,回退到WindowManager)。resources.displayMetrics - 在 外仅解码一次共享位图:
provideContent(由SizeMode.Exact用于合成当前网格跨度对应的纵向和横向方向)与glance-canonical-layouts(用于预打包一组SizeMode.Responsive断点)都会在每次更新时多次调用DpSize。在provideContent之前,先在provideContent中解码共享位图一次,再将其传给provideGlance,这样ImageProvider(bitmap)会在任一模式的所有尺寸间去重共享的RemoteViews。如果小组件按Bitmap缩放不同的位图,请将LocalSize.current除以不同位图的数量。calculateWidgetMemoryCapBytes(context) - 降采样 URI 并授予启动器读取权限:在 Android 17 (
content://) 上,基于位图的targetSdk >= 37与Icon一并计入上限。传给Bitmap或ImageProvider(uri)的RemoteViews.setImageViewUriURI 不计入进程间通信 (IPC) 上限,但启动器会按完整分辨率解码它们。将文件降采样到content://,通过context.cacheDir公开,并针对FileProvider.getUriForFile包授予Intent.CATEGORY_HOME。Intent.FLAG_GRANT_READ_URI_PERMISSION - 状态变更后显式触发小组件更新:会将偏好写入 DataStore,但不会更新小组件 UI。变更状态后,调用
updateAppWidgetState或GlanceAppWidget.update(context, glanceId)。GlanceAppWidget.updateAll(context) - 替换图片时使用完整的 调用:在
updateAppWidget类中替换位图或AppWidgetProvider时,调用Icon,而不是AppWidgetManager.updateAppWidget。partiallyUpdateAppWidget
Reference implementation files
参考实现文件
Read the companion reference files in when implementing each step,
and update the sample declaration and class names to match your app:
references/package- 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
实现每个步骤时,请阅读 中的配套参考文件,
并将示例 声明和类名更新为与应用匹配:
references/package- Bitmap 上限 & FileProvider 辅助工具:WidgetBitmapUtils.kt
- 响应式小组件、接收器 & 错误回退方案:SampleGlanceWidget.kt
- LazyColumn 集合小组件 & 修订缓存:FeedCollectionGlanceWidget.kt
- DataStore 状态推送、CoroutineWorker & ActionCallback:WidgetStateAndActions.kt
- 传统式 AppWidgetProvider WorkManager & 完整更新:ClassicWidgetImageUpdater.kt
- Manifest、小组件信息、FileProvider Paths & R8 规则:AndroidManifest.xml、sample_widget_info.xml、file_paths.xml、proguard-rules.pro
Detailed steps
详细步骤
Step 1: Configure SDK levels, dependencies, and manifest declarations
第 1 步:配置 SDK 级别、依赖项和 Manifest 声明
- Read ,
gradle/libs.versions.toml, andapp/build.gradle.ktsfirst. If AGP, Kotlin, or library versions are already present, keep them; otherwise, addapp/src/main/AndroidManifest.xmlalong withglance = "1.2.0"andandroidx.glance:glance-appwidgettoandroidx.glance:glance-material3.gradle/libs.versions.toml - In , keep
app/build.gradle.kts,namespace, and any existingapplicationId,minSdk >= 26, or library versions. SettargetSdk(andcompileSdk = 37only when explicitly migrating the app to target Android 17), enabletargetSdk = 37, and addbuildFeatures { compose = true }andimplementation(libs.androidx.glance.appwidget).implementation(libs.androidx.glance.material3) - In (see AndroidManifest.xml), declare
AndroidManifest.xmlwith<queries>and<action android:name="android.intent.action.MAIN" />so<category android:name="android.intent.category.HOME" />discovers launcher packages on API 30+. Register yourqueryIntentActivities(pointingGlanceAppWidgetReceivertoandroid.appwidget.provider, see sample_widget_info.xml) and register@xml/sample_widget_infowithandroidx.core.content.FileProvider,android:authorities="${applicationId}.fileprovider",android:exported="false", andandroid:grantUriPermissions="true"pointing toandroid.support.FILE_PROVIDER_PATHS(which declares@xml/file_paths, see file_paths.xml):<cache-path name="widget_cache" path="widget_images/" />
xml
<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>- 先阅读 、
gradle/libs.versions.toml和app/build.gradle.kts。如果已有 AGP、Kotlin 或库版本,请保留;否则,将app/src/main/AndroidManifest.xml以及glance = "1.2.0"和androidx.glance:glance-appwidget添加到androidx.glance:glance-material3。gradle/libs.versions.toml - 在 中,保留
app/build.gradle.kts、namespace以及任何现有的applicationId、minSdk >= 26或库版本。设置targetSdk(仅当明确将应用迁移为面向 Android 17 时才设置compileSdk = 37),启用targetSdk = 37,并添加buildFeatures { compose = true }和implementation(libs.androidx.glance.appwidget)。implementation(libs.androidx.glance.material3) - 在 中(参见 AndroidManifest.xml),声明
AndroidManifest.xml,其中包含<queries>和<action android:name="android.intent.action.MAIN" />,以便<category android:name="android.intent.category.HOME" />在 API 30+ 上发现启动器包。注册queryIntentActivities(将GlanceAppWidgetReceiver指向android.appwidget.provider,参见 sample_widget_info.xml),并注册@xml/sample_widget_info,设置androidx.core.content.FileProvider、android:authorities="${applicationId}.fileprovider"、android:exported="false",同时让android:grantUriPermissions="true"指向android.support.FILE_PROVIDER_PATHS(其中声明@xml/file_paths,参见 file_paths.xml):<cache-path name="widget_cache" path="widget_images/" />
xml
<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>Step 2: Estimate the widget memory cap and downsample images
第 2 步:估算小组件内存上限并降采样图片
Read WidgetBitmapUtils.kt and implement the memory cap and downsampling
helpers:
- Estimate the cap () : On API 30+ (
calculateWidgetMemoryCapBytes), readBuild.VERSION.SDK_INT >= Build.VERSION_CODES.Rand, whenval bounds = context.getSystemService(WindowManager::class.java)?.currentWindowMetrics?.bounds, returnbounds != null && bounds.width() > 0 && bounds.height() > 0(calling6L * bounds.width().toLong() * bounds.height().toLong()from a non-UI widget or workerWindowManagerlogs a nonfatalContextviolation on API 30+, which is expected for this estimate). On API 26-29 (or ifStrictModeis unavailable), fall back toWindowManager(context.resources.displayMetrics). Because6L * dm.widthPixels.toLong() * dm.heightPixels.toLong()cachessystem_serveronce at boot, always size images to the widget's6 * width * heightfirst (Stage 1) rather than filling the full screen budget.maxWidthDp x maxHeightDp - Downsample in two stages () :
decodeDownsampledWidgetBitmap- First, probe dimensions with and double
BitmapFactory.Options().apply { inJustDecodeBounds = true; inPreferredConfig = config }whilesampleSize *= 2(where(bounds.outWidth / (sampleSize * 2) >= targetWidthPx && bounds.outHeight / (sampleSize * 2) >= targetHeightPx) || (bounds.outWidth / sampleSize > targetWidthPx * 2) || (bounds.outHeight / sampleSize > targetHeightPx * 2)), so wide banners or tall screenshots do not bypass downsampling when one axis already fits.targetWidthPx = max(1, (maxWidthDp * density).toInt()) - Second, enforce the per-image byte ceiling () by doubling
perImageCapBytes = calculateWidgetMemoryCapBytes(context) / max(1, distinctImageCount)whilesampleSize *= 2. Use(max(1, bounds.outWidth / sampleSize).toLong() * max(1, bounds.outHeight / sampleSize).toLong() * bytesPerPixel) > perImageCapBytesbytes per pixel for2L(opaque photos),Bitmap.Config.RGB_565for1Lmasks, andBitmap.Config.ALPHA_8when4L(becausebounds.outConfig != null && bounds.outConfig != configupgrades translucent PNG or WebP files fromBitmapFactoryto 4-byteRGB_565). Decode withARGB_8888andinSampleSize = sampleSize.inPreferredConfig = config
- First, probe dimensions with
- Choose between in-memory and
BitmapFileProvider(Uri) : UsecacheWidgetImageUriwithdecodeDownsampledWidgetBitmapwhen passing bitmaps directly in memory. Useandroidx.glance.ImageProvider(bitmap)when the widget passes acacheWidgetImageUricontent://toUriorImageProvider(uri): it writes the downsampled bitmap intoRemoteViews.setImageViewUri, obtains aFile(context.cacheDir, "widget_images")URI withcontent://, queriesFileProvider.getUriForFile(context, "${context.packageName}.fileprovider", outFile), and callsIntent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME).context.grantUriPermission(pkg, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
阅读 WidgetBitmapUtils.kt,并实现内存上限估算与
降采样辅助函数:
- 估算上限 ():在 API 30+ (
calculateWidgetMemoryCapBytes) 上,读取Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;当val bounds = context.getSystemService(WindowManager::class.java)?.currentWindowMetrics?.bounds时,返回bounds != null && bounds.width() > 0 && bounds.height() > 0(在 API 30+ 上,从非 UI 小组件或 worker 的6L * bounds.width().toLong() * bounds.height().toLong()调用Context会记录非致命的WindowManager违规,这在此估算中属于预期行为)。在 API 26-29 上(或StrictMode不可用时),回退到WindowManager(context.resources.displayMetrics)。由于6L * dm.widthPixels.toLong() * dm.heightPixels.toLong()会在启动时缓存system_server一次,因此始终应先将图片调整到小组件的6 * width * height(Stage 1),而不是使用整个屏幕的内存预算。maxWidthDp x maxHeightDp - 分两个阶段降采样 ():
decodeDownsampledWidgetBitmap- 首先,使用 探测尺寸,并按条件将
BitmapFactory.Options().apply { inJustDecodeBounds = true; inPreferredConfig = config }不断乘以 2:sampleSize *= 2(其中(bounds.outWidth / (sampleSize * 2) >= targetWidthPx && bounds.outHeight / (sampleSize * 2) >= targetHeightPx) || (bounds.outWidth / sampleSize > targetWidthPx * 2) || (bounds.outHeight / sampleSize > targetHeightPx * 2)),这样即使宽幅横幅或高长截图的一个轴已经符合要求,也不会绕过降采样。targetWidthPx = max(1, (maxWidthDp * density).toInt()) - 其次,执行单张图片的字节上限():当
perImageCapBytes = calculateWidgetMemoryCapBytes(context) / max(1, distinctImageCount)时,将(max(1, bounds.outWidth / sampleSize).toLong() * max(1, bounds.outHeight / sampleSize).toLong() * bytesPerPixel) > perImageCapBytes不断乘以 2。对不透明照片使用sampleSize *= 2字节/像素(2L),对遮罩使用Bitmap.Config.RGB_565字节/像素(1L);当Bitmap.Config.ALPHA_8时使用bounds.outConfig != null && bounds.outConfig != config,因为4L会将半透明 PNG 或 WebP 文件从BitmapFactory升级为 4 字节的RGB_565。使用ARGB_8888和inSampleSize = sampleSize进行解码。inPreferredConfig = config
- 首先,使用
- 在内存 与 FileProvider
Bitmap之间选择 (Uri):直接以内存形式传递位图时,使用cacheWidgetImageUri,并通过decodeDownsampledWidgetBitmap传递。当小组件将androidx.glance.ImageProvider(bitmap)content://传给Uri或ImageProvider(uri)时,使用RemoteViews.setImageViewUri:它会将降采样后的位图写入cacheWidgetImageUri,通过File(context.cacheDir, "widget_images")获取FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", outFile)URI,查询content://,并调用Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_HOME)。context.grantUriPermission(pkg, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
Step 3: Deduplicate multi-size bitmaps and handle composition errors
第 3 步:对多尺寸位图进行去重并处理合成错误
Read SampleGlanceWidget.kt to configure multi-size bitmap deduplication and
composition error recovery:
- Choose for your layout : Use
SizeMode(recommended when usingSizeMode.Exactto render exactglance-canonical-layoutsdimensions for the current grid span's portrait and landscape orientations),LocalSize.current(usingSizeMode.Responsive(RESPONSIVE_BUCKETS)andimport androidx.compose.ui.unit.DpSizeto pre-bundle fixedimport androidx.compose.ui.unit.dpbreakpoints such asDpSizeso resize transitions don't wake the app process), orsetOf(DpSize(120.dp, 120.dp), DpSize(240.dp, 120.dp), DpSize(240.dp, 240.dp))(for a single layout).SizeMode.Single - Hoist shared bitmap decodes before : Because both
provideContent(SizeMode.Exactorientation sizes) and2-4(SizeMode.Responsivebuckets) runNmultiple times per update, decode shared bitmaps once inprovideContentbeforeprovideGlancewithprovideContentanddecodeDownsampledWidgetBitmapfor opaque images (Bitmap.Config.RGB_565). Pass the resultingdistinctImageCount = 1toBitmapsoandroidx.glance.ImageProvider(sharedBitmap)deduplicates the sharedRemoteViewsacross all sizes.Bitmap - Provide a zero-bitmap error fallback : Override and call
onCompositionError(context, glanceId, appWidgetId, throwable)with a text-onlyAppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, fallback)layout (RemoteViewsbitmaps).0
阅读 SampleGlanceWidget.kt,用于配置多尺寸位图去重和
合成错误恢复:
- 为布局选择 :使用
SizeMode(推荐与SizeMode.Exact配合使用,按当前网格跨度的精确glance-canonical-layouts尺寸合成纵向和横向方向)、LocalSize.current(使用SizeMode.Responsive(RESPONSIVE_BUCKETS)和import androidx.compose.ui.unit.DpSize预打包固定的import androidx.compose.ui.unit.dp断点,例如DpSize,使尺寸调整过渡不会唤醒应用进程),或setOf(DpSize(120.dp, 120.dp), DpSize(240.dp, 120.dp), DpSize(240.dp, 240.dp))(用于单一布局)。SizeMode.Single - 在 前提取共享位图解码:由于
provideContent(2-4 个方向尺寸)和SizeMode.Exact(N 个桶)在每次更新时都会多次运行SizeMode.Responsive,请在provideContent之前,先在provideContent中使用provideGlance对共享位图解码一次;对于不透明图片,使用decodeDownsampledWidgetBitmap(Bitmap.Config.RGB_565)。将生成的distinctImageCount = 1传给Bitmap,使androidx.glance.ImageProvider(sharedBitmap)在所有尺寸间去重共享的RemoteViews。Bitmap - 提供零位图错误回退方案:覆盖 ,并使用仅含文本的
onCompositionError(context, glanceId, appWidgetId, throwable)布局(0 个位图)调用RemoteViews。AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, fallback)
Step 4: Pre-decode LazyColumn collection images and assign stable itemIds
第 4 步:预先解码 LazyColumn 集合图片并分配稳定的 itemId
Read FeedCollectionGlanceWidget.kt when rendering collection widgets with
:
LazyColumn- Replace (which accepts
import androidx.glance.appwidget.ImageProvider) withUri(which acceptsimport androidx.glance.ImageProvider). RemoveBitmapandUri.fromFilecalls.Icon.createWithContentUri - While a Glance session is active (~45 seconds after an update), and
.update()do not restart.updateAll(). DeclareprovideGlanceandvar cachedRevision = -1invar cachedThumbnails = emptyMap<String, Bitmap>()beforeprovideGlance. InsideprovideContent, readprovideContent, and only whenval revision = currentState<Preferences>()[KEY_REVISION] ?: 0, updaterevision != cachedRevisionand decode thecachedRevision = revisionthumbnails withfeedItems(usecachedThumbnails = feedItems.mapNotNull { item -> decodeDownsampledWidgetBitmap(..., distinctImageCount = max(1, feedItems.size), config = Bitmap.Config.RGB_565)?.let { item.id to it } }.toMap()rather thanmapNotNullso the map staysassociateinstead ofMap<String, Bitmap>). This re-decodes when DataStore state updates during an active session while sharing one decodedMap<String, Bitmap?>across allMap<String, Bitmap>buckets.SizeMode.Responsive - In , pass a stable
LazyColumnLongtoitemId(preferring a persistent numeric ID when available) and renderitems(items = feedItems, itemId = { item -> item.id.hashCode().toLong() })only whenImage(provider = ImageProvider(bitmap), contentDescription = item.title)(never fall back tobitmap != nullin anUri.fromFilebranch).else
使用 渲染集合小组件时,阅读
FeedCollectionGlanceWidget.kt:
LazyColumn- 将 (接受
import androidx.glance.appwidget.ImageProvider)替换为Uri(接受import androidx.glance.ImageProvider)。移除Bitmap和Uri.fromFile调用。Icon.createWithContentUri - 当 Glance 会话处于活跃状态时(更新后约 45 秒),和
.update()不会重新启动.updateAll()。在provideGlance之前的provideContent中声明provideGlance和var cachedRevision = -1。在var cachedThumbnails = emptyMap<String, Bitmap>()内,读取provideContent;仅当val revision = currentState<Preferences>()[KEY_REVISION] ?: 0时,更新revision != cachedRevision,并使用cachedRevision = revision解码cachedThumbnails = feedItems.mapNotNull { item -> decodeDownsampledWidgetBitmap(..., distinctImageCount = max(1, feedItems.size), config = Bitmap.Config.RGB_565)?.let { item.id to it } }.toMap()缩略图(使用feedItems而不是mapNotNull,以使映射保持为associate而不是Map<String, Bitmap>)。这样,当活跃会话期间 DataStore 状态更新时会重新解码,同时在所有Map<String, Bitmap?>桶之间共享同一份已解码的SizeMode.Responsive。Map<String, Bitmap> - 在 中,为
LazyColumn传入稳定的items(items = feedItems, itemId = { item -> item.id.hashCode().toLong() })Long(如果可用,优先使用持久化的数字 ID),并且仅在itemId时渲染bitmap != null(切勿在Image(provider = ImageProvider(bitmap), contentDescription = item.title)分支中回退到else)。Uri.fromFile
Step 5: Push state updates, configure ActionCallbacks, and use WorkManager
第 5 步:推送状态更新、配置 ActionCallbacks 并使用 WorkManager
Read WidgetStateAndActions.kt, proguard-rules.pro, and
ClassicWidgetImageUpdater.kt to implement these patterns:
- Push DataStore updates (requires and
androidx.datastore:datastore-preferences-core:1.2.1,import androidx.glance.appwidget.GlanceAppWidgetManager, andimport androidx.glance.appwidget.state.updateAppWidgetState): Inimport androidx.glance.appwidget.updateAll(see WidgetStateAndActions.kt), iterate overpushWidgetRevisionUpdate(context), mutate state insidefor (glanceId in GlanceAppWidgetManager(context).getGlanceIds(Widget::class.java))(passing bothupdateAppWidgetState(context, glanceId) { prefs -> ... }andcontext), and immediately callglanceId(orwidget.update(context, glanceId)).widget.updateAll(context) - Use zero-argument classes with
ActionCallbackand keepActionParameters.Keyunder R8 (requires<init>()when scheduling background work):androidx.work:work-runtime-ktx:2.12.0instantiatesRunCallbackActionclasses withActionCallback, so a constructor parameter fails in every build. Remove constructor parameters fromClass.getDeclaredConstructor().newInstance()implementations, pass values withActionCallbackandActionParameters.Key(seeactionParametersOfin WidgetStateAndActions.kt), and addRefreshWidgetActionto-keep class * implements androidx.glance.appwidget.action.ActionCallback { public <init>(); }(see proguard-rules.pro). Insideproguard-rules.pro, callSyncWidgetWorker.doWork()so background work updates DataStore and refreshes the widget.pushWidgetRevisionUpdate(applicationContext) - Avoid unconditional one-time enqueues in
WorkManagerand use fullAppWidgetProvider.onUpdatefor images (see ClassicWidgetImageUpdater.kt): Never callupdateAppWidgetfromcontext.startForegroundServiceorAppWidgetProvider.onReceive(which throwsonUpdate).ForegroundServiceStartNotAllowedExceptionalready keeps a 10-year delayed session worker queued internally so WorkManager'sGlanceAppWidgetReceivernever toggles. In a classicRescheduleReceiver, avoid enqueuing aAppWidgetProviderdirectly insideOneTimeWorkRequest: even withonUpdate, once the one-time worker finishes (ExistingWorkPolicy.KEEP),SUCCEEDEDdisablesUnfinishedWorkListener, which broadcastsRescheduleReceiverand re-triggersACTION_PACKAGE_CHANGEDin a loop. Instead, schedule repeating syncs inonUpdatewithonUpdate(which staysenqueueUniquePeriodicWork(..., ExistingPeriodicWorkPolicy.KEEP, ...)across runs), useENQUEUEDfor shortgoAsync()updates, or enqueue one-time work (< 10s) only from specific action broadcasts inenqueueUniqueWork(..., ExistingWorkPolicy.KEEP, ...). When you update aonReceiveorBitmapon a classicIconwidget, callRemoteViewsrather thanappWidgetManager.updateAppWidget.partiallyUpdateAppWidget
阅读 WidgetStateAndActions.kt、proguard-rules.pro 和 ClassicWidgetImageUpdater.kt,以实现以下模式:
- 推送 DataStore 更新(需要 ,并导入
androidx.datastore:datastore-preferences-core:1.2.1、androidx.glance.appwidget.GlanceAppWidgetManager和androidx.glance.appwidget.state.updateAppWidgetState):在androidx.glance.appwidget.updateAll中(参见 WidgetStateAndActions.kt),遍历pushWidgetRevisionUpdate(context),在for (glanceId in GlanceAppWidgetManager(context).getGlanceIds(Widget::class.java))中修改状态(同时传入updateAppWidgetState(context, glanceId) { prefs -> ... }和context),并立即调用glanceId(或widget.update(context, glanceId))。widget.updateAll(context) - 使用零参数 类,并让 R8 保留
ActionCallback(安排后台工作时需要<init>()):androidx.work:work-runtime-ktx:2.12.0使用RunCallbackAction实例化Class.getDeclaredConstructor().newInstance()类,因此带构造函数参数的类在每次构建中都会失败。从ActionCallback实现中移除构造函数参数,改用ActionCallback和ActionParameters.Key传值(参见 WidgetStateAndActions.kt 中的actionParametersOf),并将RefreshWidgetAction添加到-keep class * implements androidx.glance.appwidget.action.ActionCallback { public <init>(); }(参见 proguard-rules.pro)。在proguard-rules.pro内调用SyncWidgetWorker.doWork(),使后台工作更新 DataStore 并刷新小组件。pushWidgetRevisionUpdate(applicationContext) - 避免在 中无条件入队一次性 WorkManager 任务,并在处理图片时使用完整的
AppWidgetProvider.onUpdate(参见 ClassicWidgetImageUpdater.kt):绝不从updateAppWidget或AppWidgetProvider.onReceive调用onUpdate(这会抛出context.startForegroundService)。ForegroundServiceStartNotAllowedException已在内部让一个延迟 10 年的会话 worker 保持排队状态,因此 WorkManager 的GlanceAppWidgetReceiver不会切换状态。在传统式RescheduleReceiver中,不要直接在AppWidgetProvider内入队onUpdate:即使使用OneTimeWorkRequest,一次性 worker 完成 (ExistingWorkPolicy.KEEP) 后,SUCCEEDED也会禁用UnfinishedWorkListener,后者广播RescheduleReceiver并再次触发ACTION_PACKAGE_CHANGED,形成循环。应在onUpdate中使用onUpdate安排重复同步(它在各次运行之间保持enqueueUniquePeriodicWork(..., ExistingPeriodicWorkPolicy.KEEP, ...)状态),对短于ENQUEUED的更新使用10s,或者仅在goAsync()中特定的操作广播里使用onReceive入队一次性工作。在传统式enqueueUniqueWork(..., ExistingWorkPolicy.KEEP, ...)小组件中更新RemoteViews或Bitmap时,调用Icon,而不是appWidgetManager.updateAppWidget。partiallyUpdateAppWidget
Common errors and fixes
常见错误与修复
| Symptom or exception | Root cause | Fix |
|---|---|---|
| | Downsample to the widget's |
| Home launcher lacks read permission for the | Expose the file with |
Widget UI stays frozen after | | Call |
| | Use a public zero-argument constructor, pass values with |
| | Schedule periodic syncs in |
| Missing or mismatched Glance and Compose package imports, or calling | Import |
| 症状或异常 | 根本原因 | 修复方案 |
|---|---|---|
| | 通过 |
| 主屏幕启动器没有 FileProvider | 使用 |
调用 | | 调用 |
操作点击时出现 | | 使用公共零参数构造函数,通过 |
接收器中出现 | | 在 |
| 缺少或不匹配的 Glance 和 Compose 包导入,或者调用 | 导入 |
Antipatterns
反模式
- NEVER decode or scale a inside
Bitmapper size bucket withprovideContentorSizeMode.Responsive.SizeMode.Exact - NEVER size widget bitmaps to fill the full screen memory cap (); always downsample to the widget's target
6 * width * heightfirst so unfolding a foldable or switching display resolution after boot does not exceed the boot-time cap.maxWidthDp x maxHeightDp - NEVER pass an unscaled
content://orUritoUri.fromFileorImageProvider(uri), or omitRemoteViews.setImageViewUriforcontext.grantUriPermission.Intent.CATEGORY_HOME - NEVER call without calling
updateAppWidgetStateorGlanceAppWidget.update(context, glanceId).GlanceAppWidget.updateAll(context) - NEVER call when you replace widget images.
partiallyUpdateAppWidget
- 绝不在 或
SizeMode.Responsive下,针对每个尺寸桶在SizeMode.Exact内解码或缩放provideContent。Bitmap - 绝不将小组件位图调整到填满整个屏幕的内存上限();始终先将图片降采样到小组件的目标
6 * width * height,以避免在启动后展开可折叠设备或切换显示分辨率时超过启动时计算的上限。maxWidthDp x maxHeightDp - 绝不向 或
ImageProvider(uri)传入未缩放的RemoteViews.setImageViewUricontent://或Uri,也绝不省略针对Uri.fromFile的Intent.CATEGORY_HOME。context.grantUriPermission - 绝不在调用 后,不调用
updateAppWidgetState或GlanceAppWidget.update(context, glanceId)。GlanceAppWidget.updateAll(context) - 绝不在替换小组件图片时调用 。
partiallyUpdateAppWidget