ListView not getting updated in Android Widget

1.3k views Asked by At

I have a widget that has a ListView of items. When the widget is new and empty, user clicks on it to load an activity with list of objects (each object contain the list of items) to select an object and then the widget should receive it and update its content to display it. I managed to reach the stage where the object (containing the list) is received by the AppWidgetProvider and update is called. What I'm failing to do is to make the provider call the RemoteViewService and further steps. I'll include the classes and XMLs for review.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="io.litebit.ilfornodellacasa">

    <uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".ui.activities.MainActivity"
            android:launchMode="singleTop">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity
            android:name=".ui.activities.RecipeActivity"
            android:launchMode="singleTop">
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value=".ui.activities.MainActivity"/>
        </activity>

        <receiver android:name=".ui.widgets.IngredientsWidgetProvider">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/ingredients_app_widget_info"/>
        </receiver>

        <service
            android:name=".ui.widgets.WidgetService"
            android:exported="false"
            android:permission="android.permission.BIND_REMOTEVIEWS"/>
    </application>

</manifest>

IngredientsWidgetProvider.java

package io.litebit.ilfornodellacasa.ui.widgets;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.view.View;
import android.widget.RemoteViews;

import io.litebit.ilfornodellacasa.R;
import io.litebit.ilfornodellacasa.model.Recipe;
import io.litebit.ilfornodellacasa.ui.activities.MainActivity;
import io.litebit.ilfornodellacasa.ui.activities.RecipeActivity;

import static android.appwidget.AppWidgetManager.EXTRA_APPWIDGET_ID;

/**
 * Implementation of App Widget functionality.
 */
public class IngredientsWidgetProvider extends AppWidgetProvider {

    private static final String TAG = IngredientsWidgetProvider.class.getSimpleName();
    private static Recipe recipe;

    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {

        // Construct the RemoteViews object
        RemoteViews widget = new RemoteViews(context.getPackageName(),
                R.layout.ingredients_app_widget);

        // Create pending intent to open the MainActivity
        Intent mainActivityIntent = new Intent(context, MainActivity.class);
        mainActivityIntent.putExtra(EXTRA_APPWIDGET_ID, appWidgetId);
        mainActivityIntent.setAction(MainActivity.ACTION_UPDATE_WIDGET);

        PendingIntent pendingIntent = PendingIntent.getActivity(context,
                0, mainActivityIntent, 0);

        // Launch pending intent on click
        widget.setOnClickPendingIntent(R.id.widget_layout, pendingIntent);


        if (recipe != null) {
            Log.i(TAG, "Recipe: " + recipe.getName() + " to be visualized");
            widget.setViewVisibility(R.id.tv_widget_empty, View.GONE);
            widget.setViewVisibility(R.id.lv_widget, View.VISIBLE);

            Intent listIntent = new Intent(context, WidgetService.class);
            listIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            listIntent.putExtra(RecipeActivity.KEY_RECIPE, recipe);
            Uri uri = Uri.parse(listIntent.toUri(Intent.URI_INTENT_SCHEME));
            listIntent.setData(uri);

            widget.setRemoteAdapter(R.id.lv_widget, listIntent);
            widget.setEmptyView(R.id.lv_widget, R.id.tv_widget_empty);
        } else {
            widget.setViewVisibility(R.id.tv_widget_empty, View.VISIBLE);
            widget.setViewVisibility(R.id.lv_widget, View.GONE);
        }

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, widget);
    }

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There may be multiple widgets active, so update all of them
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    @Override
    public void onEnabled(Context context) {
        // Enter relevant functionality for when the first widget is created
    }

    @Override
    public void onDisabled(Context context) {
        // Enter relevant functionality for when the last widget is disabled
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
        recipe = intent.getParcelableExtra(RecipeActivity.KEY_RECIPE);
        if (recipe != null) {
            Log.i(TAG, "Recipe: " + recipe.getName() + " selected");
            updateAppWidget(context, AppWidgetManager.getInstance(context),
                    intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                            AppWidgetManager.INVALID_APPWIDGET_ID));
        }
    }
}

IngredientViewHolderFactory.java

package io.litebit.ilfornodellacasa.ui.widgets;

import android.content.Context;
import android.util.Log;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;

import java.util.ArrayList;
import java.util.List;

import io.litebit.ilfornodellacasa.R;
import io.litebit.ilfornodellacasa.model.Ingredient;
import io.litebit.ilfornodellacasa.ui.utils.Utils;

/**
 * Copyright 2017 Ramy Bittar
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

public class IngredientViewHolderFactory implements RemoteViewsService.RemoteViewsFactory {

    private static final String TAG = IngredientViewHolderFactory.class.getSimpleName();
    private List<Ingredient> ingredients = new ArrayList<>();
    private Context context;
    private int appWidgetId;
    private Utils utils;

    IngredientViewHolderFactory(Context context, List<Ingredient> ingredients, int appWidgetId) {

        this.context = context;
        this.appWidgetId = appWidgetId;
        this.ingredients = ingredients;
        utils = new Utils(context);
        Log.i(TAG, "Public constructor");
    }

    @Override
    public void onCreate() {
        Log.i(TAG, "appWidgetId = " + this.appWidgetId);
    }

    @Override
    public void onDataSetChanged() {

    }

    @Override
    public void onDestroy() {

    }

    @Override
    public int getCount() {
        if (ingredients != null) {
            return ingredients.size();
        }
        return 0;
    }

    @Override
    public RemoteViews getViewAt(int i) {
        final RemoteViews viewHolder = new RemoteViews(context.getPackageName(),
                R.layout.viewholder_ingredient);
        Ingredient ingredient = ingredients.get(i);
        viewHolder.setTextViewText(R.id.tv_ingredient, ingredient.getIngredient());
        String quantity = utils.getQuantity(
                ingredient.getQuantity(),
                ingredient.getMeasure(),
                Utils.UNIT_SYS_IMPERIAL);
        viewHolder.setTextViewText(R.id.tv_quantity, quantity);

        return viewHolder;
    }

    @Override
    public RemoteViews getLoadingView() {
        return null;
    }

    @Override
    public int getViewTypeCount() {
        return 1;
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public boolean hasStableIds() {
        return false;
    }
}

WidgetService.java

package io.litebit.ilfornodellacasa.ui.widgets;

import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.util.Log;
import android.widget.RemoteViewsService;

import io.litebit.ilfornodellacasa.model.Recipe;
import io.litebit.ilfornodellacasa.ui.activities.RecipeActivity;

/**
 * Copyright 2017 Ramy Bittar
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

public class WidgetService extends RemoteViewsService {
    private static final String TAG = WidgetService.class.getSimpleName();

    /**
     * Invokes the remote view factory
     * @param intent passed from the calling widget provider to the remote view factory
     * @return RemoteViewsFactory object
     */
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        Log.i(TAG, "onGetViewFactory called");
        Recipe recipe = intent.getParcelableExtra(RecipeActivity.KEY_RECIPE);
        int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);
        return (new IngredientViewHolderFactory(this.getApplicationContext(),
                recipe.getIngredients(),
                appWidgetId));
    }
}

MainActivity.java

package io.litebit.ilfornodellacasa.ui.activities;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import java.util.List;

import io.litebit.ilfornodellacasa.R;
import io.litebit.ilfornodellacasa.model.Recipe;
import io.litebit.ilfornodellacasa.model.RecipeListSerializer;
import io.litebit.ilfornodellacasa.sync.RecipeSyncTask;
import io.litebit.ilfornodellacasa.ui.adapters.RecipeAdapter;
import io.litebit.ilfornodellacasa.ui.widgets.IngredientsWidgetProvider;
import pocketknife.BundleSerializer;
import pocketknife.PocketKnife;
import pocketknife.SaveState;

/**
 * Copyright 2017 Ramy Bittar
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

public class MainActivity extends AppCompatActivity implements RecipeSyncTask.SyncRecipesCallback,
        RecipeAdapter.OnRecipeClicked {

//    private static final String TAG = MainActivity.class.getSimpleName();

    private static final String TAG = MainActivity.class.getSimpleName();
    public static final String ACTION_UPDATE_WIDGET = TAG + ".action.update_widget";

    private boolean updateWidget = false;
    private int appWidgetId;
    private RecipeAdapter adapter;

    @SaveState
    @BundleSerializer(RecipeListSerializer.class)
    List<Recipe> recipes;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        PocketKnife.bindExtras(this);
        PocketKnife.restoreInstanceState(this, savedInstanceState);

        RecyclerView recyclerView = findViewById(R.id.recycler_view);
        RecyclerView.LayoutManager layoutManager;
        if (isTabletAndLandscape()) {
            layoutManager = new GridLayoutManager(this, 3);
        } else {
            layoutManager = new LinearLayoutManager(this);
        }
        recyclerView.setLayoutManager(layoutManager);

        adapter = new RecipeAdapter(null, this);
        recyclerView.setAdapter(adapter);

        if (recipes == null || recipes.size() == 0) {
            RecipeSyncTask syncTask = new RecipeSyncTask(this);
            syncTask.syncRecipes();
        } else {
            refreshRecyclerView(recipes);
        }

        if (!getIntent().getAction().equals("")) {
            updateWidget = getIntent().getAction().equals(ACTION_UPDATE_WIDGET);
            appWidgetId = getIntent().getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0);
        }

        Log.i(TAG, "updateWidget = " + updateWidget);
    }

    private boolean isTabletAndLandscape() {
        return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE
                && getResources().getConfiguration().screenWidthDp >= 900;
    }

    private void refreshRecyclerView(List<Recipe> recipes) {
        this.adapter.switchData(recipes);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_settings:
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        PocketKnife.saveInstanceState(this, outState);
        super.onSaveInstanceState(outState);
    }

    @Override
    public void onSyncResponse(List<Recipe> newRecipes) {
        recipes = newRecipes;
        refreshRecyclerView(this.recipes);
        Toast.makeText(this, recipes.size() + " recipe(s) found.", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onSyncFailure(Throwable throwable) {
        Toast.makeText(this, "Something wrong happened. Check system log for details.",
                Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onClick(int recipeId) {
        Recipe currentRecipe = null;
        for (Recipe recipe : recipes) {
            if (recipe.getId() == recipeId) {
                currentRecipe = recipe;
            }
        }
        if (updateWidget) {
            sendIntentToWidget(currentRecipe);
        } else {
            sendIntentToRecipeActivity(currentRecipe);
        }
    }

    private void sendIntentToWidget(Recipe currentRecipe) {
        Intent recipeIntent = new Intent(this, IngredientsWidgetProvider.class);
        recipeIntent.putExtra(RecipeActivity.KEY_RECIPE, currentRecipe);
        recipeIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
        recipeIntent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
        sendBroadcast(recipeIntent);
        finish();
    }

    private void sendIntentToRecipeActivity(Recipe currentRecipe) {
        Intent recipeIntent = new Intent(this, RecipeActivity.class);
        recipeIntent.putExtra(RecipeActivity.KEY_RECIPE, currentRecipe);
        recipeIntent.putExtra(RecipeActivity.KEY_STEP_ID, RecipeActivity.NO_STEP_SELECTED);
        startActivity(recipeIntent);
    }
}

viewholder_ingredient.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:orientation="vertical"
              android:paddingTop="10dp"
              android:paddingBottom="0dp"
              android:paddingLeft="14dp"
              android:paddingRight="14dp">

    <TextView
        android:id="@+id/tv_ingredient"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textStyle="bold"/>

    <TextView
        android:id="@+id/tv_quantity"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

ingredients_app_widget.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              xmlns:tools="http://schemas.android.com/tools"
              android:id="@+id/widget_layout"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:background="#66ffffff"
              android:orientation="vertical"
              android:padding="@dimen/widget_margin">

    <ListView
        android:id="@+id/lv_widget"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:visibility="visible"/>

    <TextView
        android:id="@+id/tv_widget_empty"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:text="@string/click_to_select_a_recipe"
        android:visibility="gone"
        tools:text="Empty list text"/>
</LinearLayout>

ingredients_app_widget_info.xml

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
                    xmlns:tools="http://schemas.android.com/tools"
                    android:initialKeyguardLayout="@layout/ingredients_app_widget"
                    android:initialLayout="@layout/ingredients_app_widget"
                    android:minHeight="125dp"
                    android:minWidth="250dp"
                    android:previewImage="@mipmap/ic_launcher"
                    android:resizeMode="vertical"
                    android:updatePeriodMillis="86400000"
                    android:widgetCategory="home_screen"
                    android:configure="io.litebit.ilfornodellacasa.ui.activities.MainActivity"
                    tools:targetApi="jelly_bean_mr1">
</appwidget-provider>

Thanks for help in advance.

Ramy Bittar

2

There are 2 answers

2
Filipe Batista On

It seems to me that you are not calling notifyAppWidgetViewDataChanged. If the data has changed you need to notify the widget that the collection view needs to be updated.

AppWidgetManager.notifyAppWidgetViewDataChanged

Also, take a look in the collections sample of the widgets the topic Keeping Collection Data Fresh

One feature of app widgets that use collections is the ability to provide users with up-to-date content. For example, consider the Android 3.0 Gmail app widget, which provides users with a snapshot of their inbox. To make this possible, you need to be able to trigger your RemoteViewsFactory and collection view to fetch and display new data. You achieve this with the AppWidgetManager call notifyAppWidgetViewDataChanged()

1
HeyAlex On

You need to make notifyAppWidgetViewDataChanged() after updateAppWidget(). So make logic like this:

appWidgetManager.updateAppWidget(widgetId, views);
appWidgetManager.notifyAppWidgetViewDataChanged(widgetId, R.id.lessons); // R.id.lessons - it's your listview id