情境
最近以前寫的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
不過這樣一來就會花費比較多的記憶體在存取同樣的資料。