如何避免listview資料有變動程式掛掉

如何避免listview資料有變動程式掛掉

情境

最近以前寫的listview搭配adapter的專案常常爆這個雷

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. Make sure your adapter calls notifyDataSetChanged() when its content changes.

發現只要我把關鍵字丟到ListView內部去看, 會發現這一段

if (mItemCount == 0) {
     resetList();
     invokeOnItemScrollListener();
     return;
 } else if (mItemCount != mAdapter.getCount()) {
     throw new 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. Make sure your adapter calls notifyDataSetChanged() "
             + "when its content changes. [in ListView(" + getId() + ", " + getClass()
             + ") with Adapter(" + mAdapter.getClass() + ")]");
 }

從程式上來解讀就是當listview內的item數量跟adapter的數量不一致的時候,
就會丟出這個Exception,
為了證實我的想法沒錯,
先重現這個情況出來。

程式重現

public class MainActivity extends AppCompatActivity {
    private Context mContext;
    private ListView mListView;
    private MyAdapter adapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = getApplicationContext();
        initData();
        initView();
        execDataChange();
    }

    private void initData(){
        MyData.clear();
        for(int i = 0; i < 100; i++){
            MyData.addItem("a" + i);
        }
    }

    private void initView(){
        mListView = (ListView) findViewById(R.id.list_view);
        adapter = new MyAdapter(MyData.getData());
        mListView.setAdapter(adapter);
    }

    private void execDataChange(){
        ExecutorService thread1 = Executors.newSingleThreadExecutor();
        thread1.submit(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 100000; i++){
                    MyData.addItem("b" + i);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            adapter.notifyDataSetChanged();
                            mListView.setSelection(MyData.getData().size() - 1);
                        }
                    });
                }
            }
        });
        ExecutorService thread2 = Executors.newSingleThreadExecutor();
        thread2.submit(new Runnable() {
            @Override
            public void run() {
                for(int i = 0; i < 100000; i++){
                    MyData.addItem("c" + i);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            adapter.notifyDataSetChanged();
                            mListView.setSelection(MyData.getData().size() - 1);
                        }
                    });
                }
            }
        });
    }

    public class MyAdapter extends BaseAdapter {
        private List<String> list;
        public MyAdapter(List<String> data) {
            list = data;
        }
        public void setData(List<String> data){
            list = data;
        }
        @Override
        public int getCount() {
            return list.size();
        }

        @Override
        public Object getItem(int position) {
            return null;
        }

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

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            View view = convertView;
            Holder holder;
            if(view == null) {
                view = LayoutInflater.from(mContext).inflate(R.layout.view_item, parent, false);
                holder = new Holder();
                holder.textView = (TextView) view.findViewById(R.id.item_text);
                view.setTag(holder);
            } else{
                holder = (Holder) view.getTag();
            }
            holder.textView.setText(list.get(position));
            return view;
        }
        class Holder{
            TextView textView;
        }
    }
}

在initData方法先塞一些資料給陣列

private void initData(){
    MyData.clear();
    for(int i = 0; i < 100; i++){
        MyData.addItem("a" + i);
    }
}

接著開兩個Thread分別去塞MyData的值。

private void execDataChange(){
    ExecutorService thread1 = Executors.newSingleThreadExecutor();
    thread1.submit(new Runnable() {
        @Override
        public void run() {
            for(int i = 0; i < 100000; i++){
                MyData.addItem("b" + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        adapter.notifyDataSetChanged();
                        mListView.setSelection(MyData.getData().size() - 1);
                    }
                });
            }
        }
    });
    ExecutorService thread2 = Executors.newSingleThreadExecutor();
    thread2.submit(new Runnable() {
        @Override
        public void run() {
            for(int i = 0; i < 100000; i++){
                MyData.addItem("c" + i);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        adapter.notifyDataSetChanged();
                        mListView.setSelection(MyData.getData().size() - 1);
                    }
                });
            }
        }
    });
}

結果如預期的出現Exception


結果如下

修正程式

怎麼改呢?
只要在MyData這邊修改取得Data的時候,
複製一份陣列給他, 就不會參照到原本的陣列,
如此一來, 就不會造成資料不同步的問題。

public static List<String> getData(){
//        return ourInstance.mData;
    List<String> list = new ArrayList<>();
    list.addAll(ourInstance.mData);
    return list;
}

但是這邊變成每次只要修改一次資料,
就要去MyData再取一次。

我們新增MyAdapter一個方法。

public void setData(List<String> data){
    list = data;
}

接著在Thread內部修正為

runOnUiThread(new Runnable() {
    @Override
    public void run() {
        adapter.setData(MyData.getData());
        adapter.notifyDataSetChanged();
        mListView.setSelection(MyData.getData().size() - 1);
    }
});

這樣一來不管怎麼滑動都不會跳出Exception

不過這樣一來就會花費比較多的記憶體在存取同樣的資料。

程式碼

github