Android Widget ListView闲置后点击按钮变为空列表问题求助
Android Widget闲置后点击按钮列表变空问题修复
我正在开发一款Android Widget,用于展示可增减计数的条目。原本运行正常,但闲置数分钟后(可能是进程被系统回收时),点击按钮会导致列表变为空并显示空列表视图。


现有代码
Remote View Factory代码
class GoalWidgetDataProvider( private val context: Context, private val intent: Intent? ) : RemoteViewsFactory { override fun onCreate() { } override fun onDataSetChanged() { if (GoalsRepository.goals.isNotEmpty()) { getViewAt(0) } } override fun onDestroy() { } override fun getCount(): Int { return GoalsRepository.goals.size } override fun getViewAt(position: Int): RemoteViews { val fillIntentAdd = Intent() fillIntentAdd.putExtra(GoalWidget.UPDATE_TYPE,GoalWidget.INCREMENT) fillIntentAdd.putExtra(GoalWidget.NAME_KEY, GoalsRepository.goals[position].name) fillIntentAdd.putExtra(GoalWidget.MULTIPLIER, 1) val fillIntentSub = Intent() fillIntentSub.putExtra(GoalWidget.UPDATE_TYPE,GoalWidget.DECREMENT) fillIntentSub.putExtra(GoalWidget.NAME_KEY, GoalsRepository.goals[position].name) fillIntentSub.putExtra(GoalWidget.MULTIPLIER, -1) val remoteViews = RemoteViews(context.packageName, R.layout.goal_item_widget) remoteViews.setTextViewText(R.id.name, GoalsRepository.goals[position].name) remoteViews.setTextViewText(R.id.count_text, GoalsRepository.goals[position].count.toString()) remoteViews.setTextColor(R.id.count_text, GoalsRepository.goals[position].fgColor) remoteViews.setTextColor(R.id.name, GoalsRepository.goals[position].fgColor) remoteViews.setInt(R.id.btn_increment, "setColorFilter", GoalsRepository.goals[position].fgColor) remoteViews.setInt(R.id.btn_decrement, "setColorFilter", GoalsRepository.goals[position].fgColor) remoteViews.setInt(R.id.icon, "setColorFilter", GoalsRepository.goals[position].fgColor) remoteViews.setInt(R.id.image_bg, "setColorFilter", GoalsRepository.goals[position].bgColor) remoteViews.setOnClickFillInIntent(R.id.btn_increment, fillIntentAdd) remoteViews.setOnClickFillInIntent(R.id.btn_decrement, fillIntentSub) return remoteViews } override fun getLoadingView(): RemoteViews? = null override fun getViewTypeCount(): Int = 1 override fun getItemId(i: Int): Long = i.toLong() override fun hasStableIds(): Boolean = true }
AppWidgetProvider代码
class GoalWidget : AppWidgetProvider() { companion object { private const val ACTION_INCREMENT = "INTENT_INCREMENT" const val UPDATE_TYPE = "UPDATE_TYPE" const val NAME_KEY = "NAME_KEY" const val INCREMENT = "INCREMENT" const val DECREMENT = "DECREMENT" const val MULTIPLIER = "MULTIPLIER" } override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { val widgetManager = AppWidgetManager.getInstance(context.applicationContext) widgetManager.notifyAppWidgetViewDataChanged(widgetManager.getAppWidgetIds(ComponentName(context.applicationContext.packageName, GoalWidget::class.java.name)), R.id.widget_listView) for (appWidgetId in appWidgetIds) { val addIntent = Intent(context, javaClass) addIntent.action = ACTION_INCREMENT val addPendingIntent = PendingIntent.getBroadcast(context, appWidgetId, addIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) val serviceIntent = Intent(context, GoalWidgetService::class.java) serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) serviceIntent.data = Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME)) val intent = Intent(context, GoalsActivity::class.java) val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) val views = RemoteViews(context.packageName, R.layout.goal_widget) views.setOnClickPendingIntent(R.id.layout_root, pendingIntent) views.setRemoteAdapter(R.id.widget_listView, serviceIntent) views.setEmptyView(R.id.widget_listView, R.id.goal_widget_empty) views.setPendingIntentTemplate(R.id.widget_listView, addPendingIntent) appWidgetManager.updateAppWidget(appWidgetId, views) } super.onUpdate(context, appWidgetManager, appWidgetIds) } override fun onReceive(context: Context?, intent: Intent?) { if (intent != null) { when (intent.action) { ACTION_INCREMENT -> { val curName = intent.getStringExtra(NAME_KEY) ?: "" when (intent.extras?.getString(UPDATE_TYPE,"")) { INCREMENT, DECREMENT -> { val multi = intent.getIntExtra(MULTIPLIER, 1) CoroutineScope(Dispatchers.Main).launch { GoalsRepository.incrementWidget(curName, multi) val widgetManager = AppWidgetManager.getInstance(context!!.applicationContext) widgetManager.notifyAppWidgetViewDataChanged( widgetManager.getAppWidgetIds( ComponentName( context.applicationContext.packageName, GoalWidget::class.java.name ) ), R.id.widget_listView ) } } } } } } super.onReceive(context, intent) } override fun onDeleted(context: Context, appWidgetIds: IntArray) { } override fun onEnabled(context: Context) { } override fun onDisabled(context: Context) { } }
Repository代码
object GoalsRepository { var goalsDao : GoalDao? = null set(value) { field = value loadGoals() } private var goalsList : ArrayList<GoalItem> = ArrayList() val goals : List<GoalItem> = goalsList fun setGoal(goal : GoalItem, old : GoalItem?) { if (old == null) { goalsList.add(goal) CoroutineScope(Dispatchers.IO).launch { goalsDao?.insert(goal)?.let { goal.id = it[0] } } } else { val idx = goalsList.indexOf(old) goalsList[idx] = goal CoroutineScope(Dispatchers.IO).launch { goalsDao?.update(goal) } } } fun update(idx : Int, goal : GoalItem) { goalsList[idx] = goal CoroutineScope(Dispatchers.IO).launch { goalsDao?.update(goal) } } fun incrementWidget(name : String, inc : Int) { goalsList.find { it.name == name }?.let { goal -> val idx = goalsList.indexOf(goal) update( idx, goal.copy(count = goal.count.plus(goal.increment * inc)) ) } } fun deleteGoal(goal : GoalItem) { goalsList.remove(goal) CoroutineScope(Dispatchers.IO).launch { goalsDao?.delete(goal) } } private fun loadGoals() { goalsDao?.getAllGoals()?.let { goalsList.addAll(it) } } }
问题根源
- 内存缓存丢失:
goalsList是内存中的ArrayList,当App进程被系统回收后,内存数据会清空,而loadGoals()仅在goalsDao赋值时执行一次,进程重启后若未重新初始化goalsDao,数据无法恢复。 - Factory数据加载逻辑缺失:
RemoteViewsFactory的onDataSetChanged()方法仅无效调用getViewAt(0),未实现从数据库重新加载数据的逻辑,导致进程重启后Factory初始化时goalsList为空,getCount()返回0,显示空视图。 - 点击更新时无数据兜底:点击按钮触发更新时,若进程已重启,内存中无数据,直接操作空列表后通知更新,依然显示空视图。
修复方案
1. 修改Repository,支持主动加载数据
将loadGoals()改为公开的挂起方法,允许外部主动触发数据加载,同时清空旧数据避免重复添加:
object GoalsRepository { var goalsDao : GoalDao? = null set(value) { field = value CoroutineScope(Dispatchers.IO).launch { loadGoals() } } private var goalsList : ArrayList<GoalItem> = ArrayList() val goals : List<GoalItem> = goalsList // 改为公开挂起方法,支持外部主动加载 suspend fun loadGoals() { goalsDao?.getAllGoals()?.let { goalsList.clear() goalsList.addAll(it) } } // 其他原有方法保持不变... }
2. 修复RemoteViewsFactory的数据加载逻辑
在onCreate()和onDataSetChanged()中主动从数据库加载数据,确保每次Factory初始化或数据变更时都能获取最新数据:
class GoalWidgetDataProvider( private val context: Context, private val intent: Intent? ) : RemoteViewsFactory { // 定义协程Scope,在onDestroy时取消任务避免泄漏 private val scope = CoroutineScope(Dispatchers.IO + Job()) override fun onCreate() { // 初始化时加载数据 scope.launch { GoalsRepository.loadGoals() } } override fun onDataSetChanged() { // 数据变更通知时重新加载 scope.launch { GoalsRepository.loadGoals() } } override fun onDestroy() { // 销毁时取消协程任务 scope.cancel() } // 其他原有方法保持不变... }
3. 确保进程重启后Dao自动初始化
在自定义Application类中初始化数据库和Dao,保证进程启动时就能获取到持久化数据:
class MyApplication : Application() { override fun onCreate() { super.onCreate() // 初始化Room数据库(需替换为你的数据库实现) val db = Room.databaseBuilder( applicationContext, AppDatabase::class.java, "goals_database" ).build() GoalsRepository.goalsDao = db.goalDao() } }
记得在AndroidManifest.xml中注册这个Application:
<application android:name=".MyApplication" <!-- 其他配置 --> > </application>
4. 优化Widget更新逻辑
在onReceive中,确保数据更新完成后再通知Widget刷新(当前代码已在协程中执行,配合修改后的loadGoals挂起方法,数据会正确同步)。
额外建议
- Widget属于跨进程组件,系统随时可能回收宿主进程,绝对不能依赖内存缓存作为唯一数据来源,每次数据展示或更新都应从数据库等持久化存储获取。
- 使用协程时,尽量使用与生命周期绑定的Scope(如
viewModelScope),或自定义带有Job的Scope并在销毁时取消,避免内存泄漏。
内容的提问来源于stack exchange,提问作者Danilo Namitala
相关产品推荐
相关产品推荐

