You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)
        }
    }
}

问题根源

  1. 内存缓存丢失:goalsList是内存中的ArrayList,当App进程被系统回收后,内存数据会清空,而loadGoals()仅在goalsDao赋值时执行一次,进程重启后若未重新初始化goalsDao,数据无法恢复。
  2. Factory数据加载逻辑缺失:RemoteViewsFactory的onDataSetChanged()方法仅无效调用getViewAt(0),未实现从数据库重新加载数据的逻辑,导致进程重启后Factory初始化时goalsList为空,getCount()返回0,显示空视图。
  3. 点击更新时无数据兜底:点击按钮触发更新时,若进程已重启,内存中无数据,直接操作空列表后通知更新,依然显示空视图。

修复方案

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 20:09:53