0

I reduced a bigger portion of code to this easy to test AppWidgetProvider class and I still have this problem.

Basically, I have a simple navigation part with a prev and next button. If the index is 0 , the prev button should be gone, if index is 10, the next button is gone. When it's in between, both buttons are displayed. Pressing the buttons increases or decreases the index, which is saved in SharedPreferences

The problem I have is that, for example, when the widget is updated for the first time (and the index is 0), the prev button is not gone. As I click the next button, I can see the index being increased and the TextView in the center shows it. When the index is 10, at least the first time I tested it, the next button is gone, but then when I go back by pressing prev and the index is lower than 10, it doesn't show up again. And then, when I get to index 0, the prev button is not gone.

So, it's all this sort of weird behaviour that has to do with updating the widget that I have been going nuts with.

class DemoWidget : AppWidgetProvider() {
    override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
        super.onUpdate(context, appWidgetManager, appWidgetIds)
        for (appWidgetId in appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId)
        }
    }

    override fun onReceive(context: Context, intent: Intent) {
        super.onReceive(context, intent)

        val action = intent.action ?: ""

        if (action == "prev") {
            setIndex(context, getIndex(context) - 1)

            updateAppWidgets(context)
        } else if (action == "next") {
            setIndex(context, getIndex(context) + 1)

            updateAppWidgets(context)
        }
    }

    private fun updateAppWidgets(context: Context) {
        val manager = AppWidgetManager.getInstance(context)
        val ids = manager.getAppWidgetIds(ComponentName(context, javaClass))
        ids.forEach {
            updateAppWidget(context, manager, it)
        }
    }

    private fun getIndex(context: Context): Int {
        val prefs = context.getSharedPreferences(context.packageName, 0)
        return prefs.getInt("index", 0)
    }

    private fun setIndex(context: Context, prefValue: Int) {
        val prefs = context.getSharedPreferences(context.packageName, 0).edit()
        prefs.putInt("index", prefValue)
        prefs.apply()
    }

    private fun pendingIntent(context: Context?, action: String? = null): PendingIntent? {
        val intent = Intent(context, javaClass)
        intent.action = action

        return PendingIntent.getBroadcast(
            context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
    }

    private fun updateAppWidget(context: Context, manager: AppWidgetManager, widgetId: Int) {
        val views = RemoteViews(context.packageName, R.layout.demo_widget)

        val index = getIndex(context)

        views.setTextViewText(R.id.textViewIndex, index.toString())

        if (index > 0) {
            views.setOnClickPendingIntent(
                R.id.buttonPrev,
                pendingIntent(context, "prev")
            )
        } else {
            views.setViewVisibility(R.id.buttonBack, View.GONE)
        }

        if (index < 10) {
            views.setOnClickPendingIntent(
                R.id.buttonNext,
                pendingIntent(context, "next")
            )
        } else {
            views.setViewVisibility(R.id.buttonNext, View.GONE)
        }

        manager.updateAppWidget(widgetId, views)
    }
}

and my layout

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    style="@style/Widget.Demo.AppWidget.Container"
    android:id="@+id/layoutWidget"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:theme="@style/Theme.Demo.AppWidgetContainer">

    <ImageButton
        android:id="@+id/buttonPrev"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="start|center_vertical"
        android:background="@android:color/transparent"
        android:contentDescription="@string/back"
        android:src="@drawable/app_widget_back_background" />

    <TextView
        android:id="@+id/textViewIndex"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:padding="@dimen/widget_menu_padding"
        android:textColor="@color/white"
        android:textAlignment="center" />

    <ImageButton
        android:id="@+id/buttonNext"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="end|center_vertical"
        android:background="@android:color/transparent"
        android:contentDescription="@string/forward"
        android:src="@drawable/app_widget_forward_background" />

</FrameLayout>

The Manifest file, if you need it

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.Demo">

        <receiver
            android:name=".DemoWidget"
            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/demo_widget_info" />
        </receiver>
    </application>
</manifest>

Provider info (v31)

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/app_description"
    android:initialLayout="@layout/demo_widget"
    android:minWidth="@dimen/widget_min_width"
    android:minHeight="@dimen/widget_min_height"
    android:previewImage="@drawable/app_widget_preview"
    android:preAviewLayout="@layout/demo_widget"
    android:resizeMode="horizontal|vertical"
    android:targetCellWidth="1"
    android:targetCellHeight="1"
    android:updatePeriodMillis="86400000"
    android:widgetCategory="home_screen" />
1
  • 2
    views.setViewVisibility(R.id.buttonBack, View.GONE) – In updateAppWidget(), you're setting the visibility for the "previous" button on a different ID. Not sure if that's just a typo here, or how that reconciles with the observed behavior, as I'm not sure if I'm understanding the description of that correctly.
    – Mike M.
    Commented Jun 12, 2022 at 13:09

1 Answer 1

0

Like Mike.M says in the comments, you're not setting the visibility on your prev button, because it has a different ID to the view you're changing in code:

<ImageButton android:id="@+id/buttonPrev"
else views.setViewVisibility(R.id.buttonBack, View.GONE)

So that's why nothing's happening to that button.


Your next button disappears forever because you're not actually making it visible again. You hide it when index >= 10, but there's no code that shows it when index < 10

You need to think of the button as having states, let's call them ACTIVE and INACTIVE. Each state describes stuff about a button (in your case, whether it's visible or not), and your condition check works out which state a button should be in.

Then you need to apply that state to the button, so it always reflects the current state:

if (index < 10) {
    views.setOnClickPendingIntent(
        R.id.buttonNext,
        pendingIntent(context, "next")
    )
    // it's in the ACTIVE state, so it MUST BE VISIBLE
    views.setViewVisibility(R.id.buttonNext, View.VISIBLE)
} else {
    // it's in the ACTIVE state, so it MUST NOT BE VISIBLE
    views.setViewVisibility(R.id.buttonNext, View.GONE)
}

Doing things this way means when you call the update function, it always displays as it should. You don't need to know anything about its previous state, if it's currently visible, if it's been set up, or anything like that. You just declare how it should be, in every case. It makes it way easier to read and reason about, and you avoid bugs like stuff showing parts of their old state (if you get into RecyclerViews, setting the state of the whole item display is real important since the displays get reused)

You'll need to do the same for the other button of course ;)

Not the answer you're looking for? Browse other questions tagged or ask your own question.