views:

93

answers:

1

What I want to do: run a background thread which calculates ListView contents and update ListView partially, while results are calculated.

What I know I have to avoid: I cannot mess with ListAdapter contents from background thread, so I inherited AsyncTask and publish result (add entries to adapter) from onProgressUpdate. My Adapter uses ArrayList of result objects, all operations on those arraylists are synchronized.

Research of other people: there is very valuable data here - http://permalink.gmane.org/gmane.comp.handhelds.android.devel/82685 I also suffered from almost daily crashes for group of ~500 users, and when I added list.setVisibility(GONE)/trackList.setVisibility(VISIBLE) block in onProgressUpdate, crashes lowered by a factor of 10 but not disappeared. (it was suggested in answer http://www.mailinglistarchive.com/html/[email protected]/2010-04/msg01174.html )

What I got sometimes: please notice, it happens really rarely (once a week for one of 3.5k users). But I'd like to get rid of this bug completely. Here is partial stacktrace:

java.lang.IllegalStateException: The content of the adapter has changed but ListView  did not receive a notification. Make sure the content of your adapter is not modified from a background thread, but only from the UI thread. [in ListView(2131296334, class android.widget.ListView) with Adapter(class com.transportoid.Tracks.TrackListAdapter)]
at android.widget.ListView.layoutChildren(ListView.java:1432)
at android.widget.AbsListView.onTouchEvent(AbsListView.java:2062)
at android.widget.ListView.onTouchEvent(ListView.java:3234)
at android.view.View.dispatchTouchEvent(View.java:3709)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:852)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:884)
[...]

Help? Do you know any workaround for this? How can I avoid this exception? Quoting Jayesh: "is it possible that my code in onProgressUpdate() that appends to adapter can get executed between onMeasure() and layoutChildren() of the ListView? AFAIU, all three of these methods (onProgressUpdate, onMeasure, layoutChildren) run on the same GUI thread."

+1  A: 

I wrote this code and had it run in a 2.1 emulator image for ~12 hours and did not get the IllegalStateException. I'm going to give the android framework the benefit of the doubt on this one and say that it is most likely an error in your code. I hope this helps. Maybe you can adapt it to your list and data.

public class ListViewStressTest extends ListActivity {
    ArrayAdapter<String> adapter;
    ListView list;
    AsyncTask<Void, String, Void> task;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        this.adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);
        this.list = this.getListView();

        this.list.setAdapter(this.adapter);

        this.task = new AsyncTask<Void, String, Void>() {
            Random r = new Random();
            int[] delete;
            volatile boolean scroll = false;

            @Override
            protected void onProgressUpdate(String... values) {
                if(scroll) {
                    scroll = false;
                    doScroll();
                    return;
                }

                if(values == null) {
                    doDelete();
                    return;
                }

                doUpdate(values);

                if(ListViewStressTest.this.adapter.getCount() > 5000) {
                    ListViewStressTest.this.adapter.clear();
                }
            }

            private void doScroll() {
                if(ListViewStressTest.this.adapter.getCount() == 0) {
                    return;
                }

                int n = r.nextInt(ListViewStressTest.this.adapter.getCount());
                ListViewStressTest.this.list.setSelection(n);
            }

            private void doDelete() {
                int[] d;
                synchronized(this) {
                    d = this.delete;
                }
                if(d == null) {
                    return;
                }
                for(int i = 0 ; i < d.length ; i++) {
                    int index = d[i];
                    if(index >= 0 && index < ListViewStressTest.this.adapter.getCount()) {
                        ListViewStressTest.this.adapter.remove(ListViewStressTest.this.adapter.getItem(index));
                    }
                }
            }

            private void doUpdate(String... values) {
                for(int i = 0 ; i < values.length ; i++) {
                    ListViewStressTest.this.adapter.add(values[i]);
                }
            }

            private void updateList() {
                int number = r.nextInt(30) + 1;
                String[] strings = new String[number];

                for(int i = 0 ; i < number ; i++) {
                    strings[i] = Long.toString(r.nextLong());
                }

                this.publishProgress(strings);
            }

            private void deleteFromList() {
                int number = r.nextInt(20) + 1;
                int[] toDelete = new int[number];

                for(int i = 0 ; i < number ; i++) {
                    int num = ListViewStressTest.this.adapter.getCount();
                    if(num < 2) {
                        break;
                    }
                    toDelete[i] = r.nextInt(num);
                }

                synchronized(this) {
                    this.delete = toDelete;
                }

                this.publishProgress(null);
            }

            private void scrollSomewhere() {
                this.scroll = true;
                this.publishProgress(null);
            }

            @Override
            protected Void doInBackground(Void... params) {
                while(true) {
                    int what = r.nextInt(3);

                    switch(what) {
                        case 0:
                            updateList();
                            break;
                        case 1:
                            deleteFromList();
                            break;
                        case 2:
                            scrollSomewhere();
                            break;
                    }

                    try {
                        Thread.sleep(0);
                    } catch(InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }

        };

        this.task.execute(null);
    }
}
Qberticus
@QberticusThank you for your work! I noticed that for ArrayAdapter method hasStableIds()=false; my implementation returned true which was not quite right, as rows were sorted before every notifyDataSetChanged(). I'll give it a try - if crashes survives, I will continue investigation. Best regards!
tomash