Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to update some data in a Listview without using notifyDataSetChanged()?

Tags:

I'm trying to create a ListView with a list of downloading tasks.

The downloading tasks are managed in a Service (DownloadService). Everytime a chunk of data is received, the task sends the progress via a Broadcast, received by the Fragment containing the ListView (SavedShowListFragment). On receiving the Broadcast message, the SavedShowListFragment updates the progress of the downloading tasks in the adapter and triggers notifyDataSetChanged().

Every row in the list contains a ProgressBar, a TextView for the title of the file being downloaded and one for the numeric value of the progress, and a Button to pause/resume the download or play the saved show when the download is finished.

The problem is that the pause/resume/play Button is often not responsive (onClick() is not called), and I think that's because the whole list is updated very frequently with notifyDataSetChanged() (every time a chunk of data, i.e. 1024 bytes is received, which can be many times per second, especially when there are several downloading tasks running).

I guess I could increase the size of the data chunk in the downloading tasks, but I really think my method is not optimal at all!

Could calling very frequently notifyDataSetChanged() make the ListView UI unresponsive?

Is there any way to update only some Views in the ListView rows, i.e. in my case the ProgressBar and the TextView with the numeric value of the progress, without calling notifyDataSetChanged(), which updates the whole list?

To update the progress of the downloading tasks in the ListView, is there any better option than "getChunk/sendBroadcast/updateData/notifyDataSetChanged"?

Below are the relevant parts of my code.

Download task in download service

public class DownloadService extends Service {      //...      private class DownloadTask extends AsyncTask<SavedShow, Void, Map<String, Object>> {          //...          @Override         protected Map<String, Object> doInBackground(SavedShow... params) {               //...              BufferedInputStream in = new BufferedInputStream(connection.getInputStream());              byte[] data = new byte[1024];             int x = 0;              while ((x = in.read(data, 0, 1024)) >= 0) {                  if(!this.isCancelled()){                     outputStream.write(data, 0, x);                     downloaded += x;                      MyApplication.dbHelper.updateSavedShowProgress(savedShow.getId(), downloaded);                      Intent intent_progress = new Intent(ACTION_UPDATE_PROGRESS);                     intent_progress.putExtra(KEY_SAVEDSHOW_ID, savedShow.getId());                     intent_progress.putExtra(KEY_PROGRESS, downloaded );                     LocalBroadcastManager.getInstance(DownloadService.this).sendBroadcast(intent_progress);                          }                 else{                     break;                 }             }              //...         }          //...     } } 

SavedShowListFragment

public class SavedShowListFragment extends Fragment {         //...      @Override     public void onResume() {                  super.onResume();          mAdapter = new SavedShowAdapter(getActivity(), MyApplication.dbHelper.getSavedShowList());          mListView.setAdapter(mAdapter);          //...     }       private ServiceConnection mDownloadServiceConnection = new ServiceConnection() {          @Override         public void onServiceConnected(ComponentName className, IBinder service) {              // Get service instance              DownloadServiceBinder binder = (DownloadServiceBinder) service;             mDownloadService = binder.getService();              // Set service to adapter, to 'bind' adapter to the service              mAdapter.setDownloadService(mDownloadService);              //...         }          @Override         public void onServiceDisconnected(ComponentName arg0) {              // Remove service from adapter, to 'unbind' adapter to the service              mAdapter.setDownloadService(null);         }     };       private BroadcastReceiver mMessageReceiver = new BroadcastReceiver() {         @Override         public void onReceive(Context context, Intent intent) {              String action = intent.getAction();              if(action.equals(DownloadService.ACTION_UPDATE_PROGRESS)){                   mAdapter.updateItemProgress(intent.getLongExtra(DownloadService.KEY_SAVEDSHOW_ID, -1),                         intent.getLongExtra(DownloadService.KEY_PROGRESS, -1));             }              //...         }     };      //...  } 

SavedShowAdapter

public class SavedShowAdapter extends ArrayAdapter<SavedShow> {       private LayoutInflater mLayoutInflater;      private List<Long> mSavedShowIdList; // list to find faster the position of the item in updateProgress      private DownloadService mDownloadService;      private Context mContext;      static class ViewHolder {         TextView title;         TextView status;         ProgressBar progressBar;         DownloadStateButton downloadStateBtn;     }      public static enum CancelReason{ PAUSE, DELETE };      public SavedShowAdapter(Context context, List<SavedShow> savedShowList) {         super(context, 0, savedShowList);                mLayoutInflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );           mContext = context;          mSavedShowIdList = new ArrayList<Long>();          for(SavedShow savedShow : savedShowList){             mSavedShowIdList.add(savedShow.getId());         }     }      public void updateItemProgress(long savedShowId, long progress){         getItem(mSavedShowIdList.indexOf(savedShowId)).setProgress(progress);         notifyDataSetChanged();     }      public void updateItemFileSize(long savedShowId, int fileSize){         getItem(mSavedShowIdList.indexOf(savedShowId)).setFileSize(fileSize);         notifyDataSetChanged();     }       public void updateItemState(long savedShowId, int state_ind, String msg){          SavedShow.State state = SavedShow.State.values()[state_ind];          getItem(mSavedShowIdList.indexOf(savedShowId)).setState(state);          if(state==State.ERROR){             getItem(mSavedShowIdList.indexOf(savedShowId)).setError(msg);         }          notifyDataSetChanged();     }      public void deleteItem(long savedShowId){         remove(getItem((mSavedShowIdList.indexOf(savedShowId))));                notifyDataSetChanged();     }      public void setDownloadService(DownloadService downloadService){         mDownloadService = downloadService;         notifyDataSetChanged();     }      @Override     public View getView(final int position, View convertView, ViewGroup parent) {          ViewHolder holder;         View v = convertView;          if (v == null) {              v = mLayoutInflater.inflate(R.layout.saved_show_list_item, parent, false);              holder = new ViewHolder();              holder.title = (TextView)v.findViewById(R.id.title);             holder.status = (TextView)v.findViewById(R.id.status);             holder.progressBar = (ProgressBar)v.findViewById(R.id.progress_bar);             holder.downloadStateBtn = (DownloadStateButton)v.findViewById(R.id.btn_download_state);              v.setTag(holder);         } else {             holder = (ViewHolder) v.getTag();         }          holder.title.setText(getItem(position).getTitle());          Integer fileSize = getItem(position).getFileSize();         Long progress = getItem(position).getProgress();         if(progress != null && fileSize != null){             holder.progressBar.setMax(fileSize);              holder.progressBar.setProgress(progress.intValue());              holder.status.setText(Utils.humanReadableByteCount(progress) + " / " +                     Utils.humanReadableByteCount(fileSize));         }          holder.downloadStateBtn.setTag(position);          SavedShow.State state = getItem(position).getState();          /* set the button state */          //...          /* set buton onclicklistener */          holder.downloadStateBtn.setOnClickListener(new OnClickListener() {              @Override             public void onClick(View v) {                  int position = (Integer) v.getTag();                  SavedShow.State state = getItem(position).getState();                  if(state==SavedShow.State.DOWNLOADING){                      getItem(position).setState(SavedShow.State.WAIT_PAUSE);                     notifyDataSetChanged();                      mDownloadService.cancelDownLoad(getItem(position).getId(), CancelReason.PAUSE);                  }                 else if(state==SavedShow.State.PAUSED || state==SavedShow.State.ERROR){                                       getItem(position).setState(SavedShow.State.WAIT_DOWNLOAD);                     notifyDataSetChanged();                      mDownloadService.downLoadFile(getItem(position).getId());                  }                 if(state==SavedShow.State.DOWNLOADED){                      /* play file */                 }              }         });          return v;     } }  
like image 898
jul Avatar asked Sep 26 '13 10:09

jul


People also ask

How can you update a ListView dynamically?

This example demonstrates how do I dynamically update a ListView in android. Step 1 − Create a new project in Android Studio, go to File ⇒ New Project and fill all required details to create a new project. Step 2 − Add the following code to res/layout/activity_main. xml.

What is notifyDataSetChanged in ListView?

It tells the ListView that the data has been modified; and to show the new data, the ListView must be redrawn.

What is notifyItemChanged in android?

notifyItemChanged. Notify any registered observers that the item at position has changed with an optional payload object. This is an item change event, not a structural change event. It indicates that any reflection of the data at position is out of date and should be updated.

What does notifyDataSetChanged do in RecyclerView?

notifyDataSetChanged. Notify any registered observers that the data set has changed. There are two different classes of data change events, item changes and structural changes. Item changes are when a single item has its data updated but no positional changes have occurred.


2 Answers

Of course, As pjco stated, do not update at that speed. I would recommend sending broadcasts at intervals. Better yet, have a container for the data such as progress and update every interval by polling.

However, I think it is a good thing as well to update the listview at times without notifyDataSetChanged. Actually this is most useful when the application has a higher update frequency. Remember: I am not saying that your update-triggering mechanism is correct.


Solution

Basically, you will want to update a particular position without notifyDataSetChanged. In the following example, I have assumed the following:

  1. Your listview is called mListView.
  2. You only want to update the progress
  3. Your progress bar in your convertView has the id R.id.progress

public boolean updateListView(int position, int newProgress) {     int first = mListView.getFirstVisiblePosition();     int last = mListView.getLastVisiblePosition();     if(position < first || position > last) {         //just update your DataSet         //the next time getView is called         //the ui is updated automatically         return false;     }     else {         View convertView = mListView.getChildAt(position - first);         //this is the convertView that you previously returned in getView         //just fix it (for example:)         ProgressBar bar = (ProgressBar) convertView.findViewById(R.id.progress);         bar.setProgress(newProgress);         return true;     } } 

Notes

This example of course is not complete. You can probably use the following sequence:

  1. Update your Data (when you receive new progress)
  2. Call updateListView(int position) that should use the same code but update using your dataset and without the parameter.

In addition, I just noticed that you have some code posted. Since you are using a Holder, you can simply get the holder inside the function. I will not update the code (I think it is self-explanatory).

Lastly, just to emphasize, change your whole code for triggering progress updates. A fast way would be to alter your Service: Wrap the code that sends the broadcast with an if statement which checks whether the last update had been more than a second or half second ago and whether the download has finished (no need to check finished but make sure to send update when finished):

In your download service

private static final long INTERVAL_BROADCAST = 800; private long lastUpdate = 0; 

Now in doInBackground, wrap the intent sending with an if statement

if(System.currentTimeMillis() - lastUpdate > INTERVAL_BROADCAST) {     lastUpdate = System.currentTimeMillis();     Intent intent_progress = new Intent(ACTION_UPDATE_PROGRESS);     intent_progress.putExtra(KEY_SAVEDSHOW_ID, savedShow.getId());     intent_progress.putExtra(KEY_PROGRESS, downloaded );     LocalBroadcastManager.getInstance(DownloadService.this).sendBroadcast(intent_progress); } 
like image 194
Sherif elKhatib Avatar answered Sep 30 '22 20:09

Sherif elKhatib


The short answer: dont update the UI based on data speeds

Unless you are writing a speed test style app, there is no user benefit to updating this way.

ListView is very well optimized, (as you seem to already know because you are using the ViewHolder pattern).

Have you tried calling notifyDataSetChanged() every 1 second?

Every 1024 bytes is ridiculously fast. If someone is downloading at 8Mbps that could be updating over 1000 times a second and this could definitely cause an ANR.

Rather than update progress based on amount downloaded, you should poll the amount at an interval that does not cause UI blocking.

Anyways, in order to help avoid blocking the UI thread you could post updates to a Handler.

Play around with the value for sleep to be sure you are not updating too often. You could try going as low as 200ms but I wouldn't go below 500ms to be sure. The exact value depends on the devices you are targeting and number of items that will need layout passes.

NOTE: this is just one way to do this, there are many ways to accomplish looping like this.

private static final int UPDATE_DOWNLOAD_PROGRESS = 666;  Handler myHandler = new Handler() {     @Override     handleMessage(Message msg)     {         switch (msg.what)         {             case UPDATE_DOWNLOAD_PROGRESS:                 myAdapter.notifyDataSetChanged();                 break;             default:                 break;         }     } }    private void runUpdateThread() {      new Thread(      new Runnable() {          @Override          public void run() {              while ( MyFragment.this.getIsDownloading() )              {                   try                    {                           Thread.sleep(1000); // Sleep for 1 second                        MyFragment.this.myHandler                           .obtainMessage(UPDATE_DOWNLOAD_PROGRESS)                           .sendToTarget();                   }                    catch (InterruptedException e)                    {                       Log.d(TAG, "sleep failure");                   }              }           }      } ).start();  } 
like image 40
pjco Avatar answered Sep 30 '22 20:09

pjco