Android:我的带有 ListView 的 App Widget 未通过按钮或更新周期进行更新

Posted

技术标签:

【中文标题】Android:我的带有 ListView 的 App Widget 未通过按钮或更新周期进行更新【英文标题】:Android: My App Widget with ListView is not updating via button or update period 【发布时间】:2014-02-08 20:07:45 【问题描述】:

我正在使用小部件来显示 RSS 提要。提要已显示,但我无法通过单击按钮对其进行更新。在小部件配置 xml 中设置的更新周期之后,列表也不会更新。

你能帮帮我吗?

WidgetProvider

public class WidgetProvider extends AppWidgetProvider 

    //Tag for Logging
    private static final String TAG = "Widget";

    // String to be sent on Broadcast as soon as Data is Fetched
    // should be included on WidgetProvider manifest intent action
    // to be recognized by this WidgetProvider to receive broadcast
    public static final String DATA_FETCHED = "mypackage.DATA_FETCHED";
    public static final String EXTRA_LIST_VIEW_ROW_NUMBER = "mypackage.EXTRA_LIST_VIEW_ROW_NUMBER";
    public static final String WIDGET_BUTTON = "mypackage.WIDGET_BUTTON";

    /*
     * this method is called every 30 mins (30 min =30x60x1000) as specified on widgetinfo.xml this
     * method is also called on every phone reboot from this method nothing is
     * updated right now but instead RetmoteFetchService class is called this
     * service will fetch data,and send broadcast to WidgetProvider this
     * broadcast will be received by WidgetProvider onReceive which in turn
     * updates the widget
     */
    @Override
    public void onUpdate(Context ctxt, AppWidgetManager appWidgetManager,
            int[] appWidgetIds) 

        Log.d(TAG, "Hello WidgetProvider onUpdate");

        /*
         * int[] appWidgetIds holds ids of multiple instance of your widget
         * meaning you are placing more than one widgets on your homescreen
         */

        for (int i = 0; i < appWidgetIds.length; ++i) 

            // Create an Intent for on click refresh
            Intent intent = new Intent(WIDGET_BUTTON);
            PendingIntent pendingIntent = PendingIntent.getBroadcast(ctxt, 0,
                    intent, PendingIntent.FLAG_UPDATE_CURRENT);

            // Get the layout for the App Widget and attach an on-click listener
            // to the button
            RemoteViews remoteViews = new RemoteViews(ctxt.getPackageName(),
                    R.layout.widget_layout);
            remoteViews.setOnClickPendingIntent(R.id.refresh_widget,
                    pendingIntent);

            // Tell the AppWidgetManager to perform an update on the current app widget
            appWidgetManager.updateAppWidget(appWidgetIds[i], remoteViews);

            //start RemoteFetchService to parse XML in AsyncTask
            Intent serviceIntent = new Intent(ctxt, RemoteFetchService.class);
            serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    appWidgetIds[i]);
            ctxt.startService(serviceIntent);
        
        super.onUpdate(ctxt, appWidgetManager, appWidgetIds);
    

    protected PendingIntent getPendingSelfIntent(Context context, String action) 
        Intent intent = new Intent(context, getClass());
        intent.setAction(action);
        return PendingIntent.getBroadcast(context, 0, intent, 0);
    

    /*
     * It receives the broadcast as per the action set on intent filters on
     * Manifest.xml once data is fetched from RemoteFetchService,it sends
     * broadcast and WidgetProvider notifies to change the data the data change
     * right now happens on ListProvider as it takes RemoteFetchService
     * listItemList as data
     */
    @SuppressWarnings("deprecation")
    @Override
    public void onReceive(Context context, Intent intent) 
        super.onReceive(context, intent);

        Log.d(TAG, "Hello WidgetProvider onReceive");

        // if RSS Feed was parsed in RemoteFetchService.java
        if (intent.getAction().equals(DATA_FETCHED)) 

            Log.d(TAG, "Data fetched in Widget Provider in OnReceive");

            int appWidgetId = intent.getIntExtra(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
            AppWidgetManager appWidgetManager = AppWidgetManager
                    .getInstance(context);

            // which layout to show on widget
            RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
                    R.layout.widget_layout);

            // RemoteViews Service needed to provide adapter for ListView
            Intent svcIntent = new Intent(context, WidgetService.class);
            // passing app widget id to that RemoteViews Service
            svcIntent
                    .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
            // setting a unique Uri to the intent
            svcIntent.setData(Uri.parse(svcIntent
                    .toUri(Intent.URI_INTENT_SCHEME)));
            // setting adapter to listview of the widget
            remoteViews.setRemoteAdapter(appWidgetId, R.id.listViewWidget,
                    svcIntent);
            // setting an empty view in case of no data
            remoteViews.setEmptyView(R.id.listViewWidget, R.id.empty_view);

            // onclick item listview

            // This section makes it possible for items to have individualized
            // behavior.
            // It does this by setting up a pending intent template. Individuals
            // items of a collection
            // cannot set up their own pending intents. Instead, the collection
            // as a whole sets
            // up a pending intent template, and the individual items set a
            // fillInIntent
            // to create unique behavior on an item-by-item basis.
            Intent toastIntent = new Intent(context, WidgetProvider.class);
            // Set the action for the intent.
            // When the user touches a particular view, it will have the effect
            // of
            // broadcasting TOAST_ACTION.
            toastIntent.setAction(WidgetProvider.EXTRA_LIST_VIEW_ROW_NUMBER);
            toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    appWidgetId);
            intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME)));
            PendingIntent toastPendingIntent = PendingIntent.getBroadcast(
                    context, 0, toastIntent, PendingIntent.FLAG_UPDATE_CURRENT);
            remoteViews.setPendingIntentTemplate(R.id.listViewWidget,
                    toastPendingIntent);

            appWidgetManager.updateAppWidget(appWidgetId, remoteViews);

            //appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.listViewWidget);
        
        // if item on list was clicked
        if (intent.getAction().equals(EXTRA_LIST_VIEW_ROW_NUMBER)) 

            Log.d(TAG, "List Item Clicked in OnReceive in Widget Provider");

            int appWidgetId = intent.getIntExtra(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);

            AppWidgetManager appWidgetManager = AppWidgetManager
                    .getInstance(context);

            // get position on listview which was clicked
            int position = intent.getIntExtra(EXTRA_LIST_VIEW_ROW_NUMBER, 0);

            // get RSSFeed
            RSSFeed feed = ListItem.Feed;

            Intent toastIntent = new Intent(context, ItemDetailActivity.class);
            toastIntent.setAction(WidgetProvider.EXTRA_LIST_VIEW_ROW_NUMBER);
            toastIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    appWidgetId);

            /*
             * Toast.makeText(context, "Clicked on position :" + viewIndex,
             * Toast.LENGTH_SHORT).show();
             */

            // start ItemDetailActivity
            Intent detailIntent = new Intent(context, ItemDetailActivity.class);
            detailIntent.putExtra("pos", position);
            detailIntent.putExtra("feed", feed);
            detailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(detailIntent);

        
        // if refresh button was pressed
        if (WIDGET_BUTTON.equals(intent.getAction())) 

            Log.d(TAG,
                    "Refresh Button Clicked in OnReceive in Widget Provider");

            AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
            int appWidgetIds[] = appWidgetManager.getAppWidgetIds(
                                       new ComponentName(context, WidgetProvider.class));
            appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.listViewWidget);

         else 
            Log.d(TAG, "No Data fetched in Widget Provider");
        

    

RemoteFetchService

public class RemoteFetchService extends Service 

    //Tag for Logging
    private static final String TAG = "Widget";

    private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
    RSSFeed feed;
    public Date pDate;
    public static ArrayList<ListItem> listItemList;

    @Override
    public IBinder onBind(Intent arg0) 
        return null;
    

    /*
     * Retrieve appwidget id from intent it is needed to update widget later
     * start Async Task
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) 

        Log.d(TAG, "Hello RemoteFetchService onStartCommand");

        if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID))
            appWidgetId = intent.getIntExtra(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);

        // fetchDataFromWeb();
        new AsyncLoadXMLFeed().execute();

        return super.onStartCommand(intent, flags, startId);
    

    /**
     * AsyncTask which parses the xml and post it
     **/
    public class AsyncLoadXMLFeed extends AsyncTask<Void, Void, RSSFeed> 


        protected RSSFeed doInBackground(Void... params) 
            try 

                Log.d(TAG, "Starte Parsing von URL in Widget");
                // Obtain feed
                DOMParser myParser = new DOMParser();
                feed = myParser.parseXml("http://www.test.de/feed");
                Log.d(TAG,
                        "ItemCount Parser in Widget: " + feed.getItemCount());

                return feed;

             catch (Exception e) 
                Log.d(TAG, "Exception beim Parsen: " + e.toString());
                return null;
            
        

        @Override
        protected void onPostExecute(RSSFeed parsed_feed) 
            // super.onPostExecute(result);
            Log.d(TAG, "Async Parse fertig");
            listItemList = new ArrayList<ListItem>();

            if (feed != null) 
                try 
                    int length = feed.getItemCount();
                    for (int i = 0; i < length; i++) 

                        String date = calc_date_difference(feed, i);

                        final ListItem listItem = new ListItem();
                        ListItem.Feed = feed;
                        listItem.heading = feed.getItem(i).getTitle();
                        listItem.pubDate = date;
                        Log.d(TAG+" Heading", feed.getItem(i).getTitle());
                        Log.d(TAG+" PubDate", date);
                        listItemList.add(listItem);
                    

                 catch (Exception e) 
                    Log.d(TAG, "Exception onPostExecute: " + e.toString());
                
             else 
                Log.d(TAG, "Feed in onPostExecute ist null");
            

            // start intent and broadcast WidgetProvider, that data is fetched
            Intent widgetUpdateIntent = new Intent();
            widgetUpdateIntent.setAction(WidgetProvider.DATA_FETCHED);
            widgetUpdateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                    appWidgetId);
            sendBroadcast(widgetUpdateIntent);

            // stop service
            stopSelf();
        
    

    /**
     * Method to calculate the time difference
     **/
    public String calc_date_difference(RSSFeed feed, int pos) 
        // calculate the time difference to the actual system time
        String pubDate = feed.getItem(pos).getDate();
        SimpleDateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z",
                Locale.ENGLISH);
        try 
            try 
                pDate = df.parse(pubDate);
             catch (java.text.ParseException e) 
                e.printStackTrace();
            
            pubDate = "Vor " + DateUtils.getDateDifference(pDate);
            return pubDate;
         catch (ParseException e) 
            Log.e(TAG, "Error parsing date..");
            return null;
        
    

列表提供者

/**
 * If you are familiar with Adapter of ListView,this is the same as adapter with
 * few changes
 * 
 */
public class ListProvider implements RemoteViewsFactory 

    // Tag for Logging
    private static final String TAG = "Widget";

    private RemoteViews views;
    private Context ctxt = null;
    private int appWidgetId;
    private ArrayList<ListItem> listItemList = new ArrayList<ListItem>();
    public ImageLoader imageLoader;
    private Bitmap bmp;

    public ListProvider(Context ctxt, Intent intent) 
        this.ctxt = ctxt;
        appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
                AppWidgetManager.INVALID_APPWIDGET_ID);

        if (RemoteFetchService.listItemList != null)
            listItemList = (ArrayList<ListItem>) RemoteFetchService.listItemList
                    .clone();
        else
            listItemList = new ArrayList<ListItem>();
    

    @Override
    public void onCreate() 
        // no-op
        Log.d(TAG, "Hello ListProvider onCreate");
    

    @Override
    public void onDestroy() 
        // no-op
    

    @Override
    public int getCount() 
        return listItemList.size();
    

    /*
     * Similar to getView of Adapter where instead of Viewwe return RemoteViews
     */
    @Override
    public RemoteViews getViewAt(int position) 

        final RemoteViews remoteView = new RemoteViews(ctxt.getPackageName(),
                R.layout.widget_row);
        ListItem listItem = listItemList.get(position);
        remoteView.setTextViewText(R.id.heading, listItem.heading);
        remoteView.setTextViewText(R.id.pubDate, listItem.pubDate);

        // onclick item listview
        Intent fillInIntent = new Intent();
        fillInIntent.putExtra(WidgetProvider.EXTRA_LIST_VIEW_ROW_NUMBER,
                position);
        remoteView.setOnClickFillInIntent(R.id.heading, fillInIntent);

        return remoteView;
    

    @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 (true);
    

    @Override
    public void onDataSetChanged() 
        // This code is executed if the refresh button is pressed or after the
        // update period

        // start RemoteFetchService to parse XML in AsyncTask
        Intent serviceIntent = new Intent(ctxt, RemoteFetchService.class);
        serviceIntent
                .putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
        ctxt.startService(serviceIntent);
    

widget_provider.xml

<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
  android:minHeight="200dp"
  android:minWidth="200dp"
  android:updatePeriodMillis="30000"
  android:initialLayout="@layout/widget_layout"
  android:autoAdvanceViewId="@+id/words"
  android:previewImage="@drawable/widget_preview"
  android:resizeMode="vertical|horizontal"
/>

我希望这一切都可以帮助我。

【问题讨论】:

我不确定,但不应该在 onUpdate 中返回吗? 在其他示例中我从未在 onUpdate 中看到 return 语句 我的意思是,如果我没记错的话,remoteViews 应该有回报 我不这么认为,看官方描述:developer.android.com/guide/topics/appwidgets/… 您是否在onReceive() 中进行了任何调试以查看您是否按预期获得了DATA_FETCHED 广播?我猜这还没有完全正确地设置广播。 【参考方案1】:

在我为您的问题提供潜在答案之前,这里有一些如何改进代码的想法(注意它们与答案相关,因为它们有助于防止错误发生)。

onReceive

当您实现 onReceive() 时,您必须记住,appWidgetIds 可以作为带有 AppWidgetManager.EXTRA_APPWIDGET_ID 的 int 或作为带有 AppWidgetManager.EXTRA_APPWIDGET_IDS 的 int 数组传递(您自己使用这两种方法)。 为了解决这个问题,我通常会这样做:

@Override
public void onReceive(Context context, Intent intent) 
    Bundle extras = intent.getExtras();
    int invalidId = AppWidgetManager.INVALID_APPWIDGET_ID;
    int appWidgetId = extras != null ? extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, invalidId) : invalidId ;
    int[] appWidgetIds = extras != null ? extras.getIntArray(AppWidgetManager.EXTRA_APPWIDGET_IDS) : null;

    // single appWidgetId as parameter
    if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) 
        appWidgetIds = new int[] appWidgetId;
    

    // multiple appWidgetIds as parameter
    if (appWidgetIds != null) 
        for (int id:appWidgetIds) 
            // process a single appWidgetId
            onReceiveInternal(context, intent, id)
        
    

    super.onReceive(context, intent);


private boolean onReceiveInternal(Context context, Intent intent, int appWidgetId) 
    // do your stuff for one appWidgetId

这会阻止你这样做:

if (WIDGET_BUTTON.equals(intent.getAction())) 
    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
    int appWidgetIds[] = appWidgetManager.getAppWidgetIds(new ComponentName(context, WidgetProvider.class));
    appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.listViewWidget);

如果您收到针对单个 appWidgetId 的广播,则不应更新所有小部件实例。为什么不?因为您的所有小部件实例都将收到相同的广播,并且每个实例都会为其他实例调用 notifyAppWidgetViewDataChanged。如果用户将 4 个小部件实例放在主屏幕上,按下刷新按钮将触发 16 次(原文如此!)更新。

onUpdate

当您处理 DATA_FETCHED 广播时,您希望更新小部件。现在,与 onUpdate() 相比,为什么要为此使用不同的代码?他们真的应该这样做,所以我建议使用以下模式:

private void onReceiveInternal(Context context, Intent intent, int appWidgetId) 
    AppWidgetManager appWidgetMgr = AppWidgetManager.getInstance(context);
    String action = intent.getAction();

    if (DATA_FETCHED.equals(action)) 
        onUpdateInternal(context, appWidgetMgr, appWidgetId);
    
    // more code here


@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) 
    for (int appWidgetId : appWidgetIds) 
        onUpdateInternal(context, appWidgetManager, appWidgetId);
        startFetchService(context, appWidgetId); // here we start the service to fetch the feed
    
    super.onUpdate(context, appWidgetManager, appWidgetIds);



private void onUpdateInternal(Context context, AppWidgetManager appWidgetManager, int appWidgetId) 
    // here goes the update code

这样做可以保证更新小部件的效果相同,无论是 Android 触发的更新还是在您的提要数据进入时触发的更新。我了解您希望在第一种情况下启动 fetch 服务(这很容易实现正如我上面的代码示例所示),但实际的 ui 更新应该是一样的。

现在您创建的不是相同的 RemoteView。如果它是 Android 触发的更新,那么您基本上会膨胀布局并为刷新按钮添加一个 Intent,而提要后的更新使用 RemoteViewsService 为每个列表项设置单独的 Intent。这种方法的问题在于,Android 更新会破坏各个 Intent,直到新的提要进来,并且从用户的角度来看,小部件的行为会变得不稳定。使用我的方法,显示和行为将是一致的。

清单

请确保在您的 WidgetProvider 接收器的定义中包含具有正确操作的标签,否则您将不会收到任何更新(我猜您已经拥有该标签,或者您的小部件根本不会显示任何数据):

<receiver android:name="mypackage.WidgetProvider">
    <intent-filter>
        <action android:name="mypackage.DATA_FETCHED" />
        <action android:name="mypackage.EXTRA_LIST_VIEW_ROW_NUMBER" />
        <action android:name="mypackage.WIDGET_BUTTON" />
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/widget_provider" />
</receiver>

一个答案

您没有发布完整的代码,所以我无法运行它。没有它,我只能确定哪个部分肯定会失败,但我可能会错过其他可能无法正常工作的部分。因此,我将其声明为“一个答案”。

在收到 DATA_FETCHED 广播后创建 RemoteViews 时,您没有将 Intent 设置为刷新按钮(您在 onUpdate 中执行此操作,但在处理 DATA_FETCHED 时未调用该按钮 -> 因此我建议使用相同的更新代码以防止此类错误发生)。如果没有这个 Intent,按下刷新按钮将什么也做不了。

当然,当涉及到定期更新时,其他答案是正确的,根据这个:http://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo.html#updatePeriodMillis

如果您需要更频繁的更新,请使用 AlarmManager(在 SO 上搜索代码示例)。

【讨论】:

同时我重写了我的整个小部件代码,但你给出了如此详细的答案,你值得赏金!我在你指出的代码中有很多错误!【参考方案2】:

虽然这不能解决您通过按钮更新的问题,但您的updatePeriodMillis 肯定不会按预期工作。

根据AppWidgetProviderInfo 文档,最短更新周期为 30 分钟(1800000 毫秒)。您在 XML 中指定的更新周期仅为 30 秒。如果您在检查小部件之前没有等待整整 30 分钟,您将看不到任何新数据。

【讨论】:

【参考方案3】:
The issue is:

您已在 xml 中指定了仅 30 秒的更新周期

android:updatePeriodMillis="30000"

您需要等待 30 分钟才能使其正常工作。 详情见链接

http://developer.android.com/reference/android/appwidget/AppWidgetProviderInfo.html#updatePeriodMillis

【讨论】:

以上是关于Android:我的带有 ListView 的 App Widget 未通过按钮或更新周期进行更新的主要内容,如果未能解决你的问题,请参考以下文章

带有自定义适配器的 Android ListView 有视觉故障

带有 ListView 的 Android 可点击小部件在 ListItems 上不可点击

在android studio中删除一个带有按钮的listview项目

带有过滤器 Android 的自定义 Listview 适配器

带有 ListView 的 Android 小部件无法正确加载项目

Android ListView 和带有 ViewHolder 的自定义适配器