Jetpack Glance迁移Widget时添加StackView(基于RemoteViewsService)遇崩溃问题求助
兄弟,我之前把旧的AppWidgetProvider widget迁移到Glance的时候也踩过一模一样的坑!你遇到的这个崩溃提示“setRemoteAdapter can only be used for AppWidgets”,本质原因是你把带StackView的RemoteViews当成子Composable嵌套了,而setRemoteAdapter要求这个RemoteViews必须是AppWidget的根视图,系统需要它直接关联到对应的WidgetId,嵌套的子视图根本没这个权限。
我给你梳理几个可行的解决方案,你可以根据自己的需求选:
方案一:沿用RemoteViewsService,把StackView布局作为Widget根内容
如果你想继续用旧的RemoteViewsService逻辑,那得调整代码,让带StackView的RemoteViews成为整个Glance Widget的根,不能嵌套在其他Glance布局(比如Column、Box)里。
修改后的代码大概是这样:
@Composable private fun CardsStackView(glanceId: GlanceId) { val context = LocalContext.current val packageName = context.packageName val glanceManager = GlanceAppWidgetManager(context) // 获取对应的WidgetId,这里runBlocking没问题,因为Glance本身就是协程环境 val widgetId = runBlocking { glanceManager.getAppWidgetId(glanceId) } // 直接创建根RemoteViews,设置StackView的RemoteAdapter val remoteViews = RemoteViews(packageName, R.layout.cards_stack).apply { val intent = Intent(context, CardItemService::class.java).apply { putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME)) } setRemoteAdapter(R.id.cards, intent) // 记得设置空视图(如果需要的话) setEmptyView(R.id.cards, R.id.empty_view) } // 直接返回这个AndroidRemoteViews,不要套其他Composable AndroidRemoteViews(remoteViews) } // 你的GlanceWidget类里直接用这个根布局 class MyGlanceWidget : GlanceAppWidget() { override suspend fun provideGlance(context: Context, id: GlanceId) { provideContent { CardsStackView(id) } } }
另外还要确保你的CardItemService和Manifest注册是对的:
class CardItemService : RemoteViewsService() { override fun onGetViewFactory(intent: Intent): RemoteViewsFactory { val widgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) return CardItemViewFactory(applicationContext, widgetId) } class CardItemViewFactory(private val context: Context, private val widgetId: Int) : RemoteViewsFactory { override fun onCreate() {} override fun onDataSetChanged() {} override fun onDestroy() {} override fun getCount(): Int = 5 // 替换成你的实际数据数量 override fun getViewAt(position: Int): RemoteViews { val itemView = RemoteViews(context.packageName, R.layout.card_item) itemView.setTextViewText(R.id.card_title, "卡片 $position") // 设置item点击的填充意图 val fillInIntent = Intent().apply { putExtra("POSITION", position) } itemView.setOnClickFillInIntent(R.id.card_root, fillInIntent) return itemView } override fun getLoadingView(): RemoteViews? = null override fun getViewTypeCount(): Int = 1 override fun getItemId(position: Int): Long = position.toLong() override fun hasStableIds(): Boolean = true } }
Manifest里注册Service:
<service android:name=".CardItemService" android:permission="android.permission.BIND_REMOTEVIEWS" />
方案二:改用Glance原生的LazyColumn(更推荐)
其实Glance本身提供了LazyColumn组件,完全可以替代StackView,而且不需要依赖RemoteViewsService,代码更简洁,也更符合Glance的Compose开发模式。
示例代码:
@Composable private fun CardsList(glanceId: GlanceId) { LazyColumn( modifier = GlanceModifier.fillMaxSize().padding(8.dp) ) { // 替换成你的实际数据源 items(5) { position -> CardItem(position) } } } @Composable private fun CardItem(position: Int) { Box( modifier = GlanceModifier .fillMaxWidth() .padding(vertical = 4.dp) .background(Color.LightGray) .clickable { // 点击跳转到你的Activity actionStartActivity<MainActivity>() } ) { Text( text = "卡片 $position", modifier = GlanceModifier.padding(16.dp) ) } }
这种方式不需要写复杂的RemoteViewsService,数据更新也可以通过Glance的状态管理来处理,维护起来方便很多。
补充说明
如果你的Widget需要混合其他UI(比如顶部标题栏+StackView),那建议全部用RemoteViews来构建整个布局,不要混合Glance的Composable和AndroidRemoteViews,否则还是会遇到嵌套的问题。
内容来源于stack exchange

