androidkotlinandroid-widget

widget listview becomes empty after a time


I'm making an android widget to list some items that I can increment and decrement the count.

It worked fine, but after a few minutes (maybe when all services stop) if when I click on the buttons, the list becomes empty and displays the view of empty list.

enter image description here

enter image description here

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

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

Solution

  • I don't know exactly why, but using shared preferences instead of Rom Database worked fine.

    I think it's because the database initialization take a time and the widget does not support asyncronous tasks.

    i didn't find an explanation, but it worked.