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;
}
}
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:
- Your listview is called mListView.
- You only want to update the progress
- 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:
- Update your Data (when you receive new progress)
- 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);
}
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();
}
Although its not an answer to your question but one optimization that can be done in your getView()
method is this, Instead of creating and setting click listener every time like this:
holder.downloadStateBtn.setTag(position);
holder.downloadStateBtn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
int position = (Integer) v.getTag();
// your current normal click handling
}
});
You can just create it once as class variable and set it while creating row's View
:
final OnClickListener btnListener = new OnClickListener() {
@Override
public void onClick(View v) {
int position = (Integer) v.getTag();
// your normal click handling code goes here
}
}
and then in getView()
:
if (v == null) {
v = mLayoutInflater.inflate(R.layout.saved_show_list_item, parent, false);
// your ViewHolder stuff here
holder.downloadStateBtn.setOnClickListener(btnClickListener);//<<<<<
v.setTag(holder);
} else {
holder = (ViewHolder) v.getTag();
}
oh and don't forget to set tag on this button in getView()
as you are already doing:
holder.downloadStateBtn.setTag(position);
来源:https://stackoverflow.com/questions/19025841/how-to-update-some-data-in-a-listview-without-using-notifydatasetchanged