做安卓开发的同学应该大多都经历过adapter中在调用了notifyDataSetChanged()方法之后数据不更新的问题,作为菜鸟的我也同样踩过坑,现在写这篇文章作为总结。
话不多说,上代码!
首先是Activity的布局,两个按钮,代表两种加载数据的方式,然后一个ListView。
"http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.example.indicatedemo.MainActivity">
接下来是模拟数据实体类Person。
public class Person {
private String name;// 姓名
private int age;// 年龄
private boolean isMan;// 是男生吗
public Person(String name, int age, boolean isMan) {
this.name = name;
this.age = age;
this.isMan = isMan;
}
public boolean isMan() {
return isMan;
}
public void setMan(boolean man) {
isMan = man;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
然后一个再正常不过的Adapter。
public class MyAdapter extends BaseAdapter {
private static final String TAG = "MyAdapter";
private List data;
private LayoutInflater inflater;
public MyAdapter(Context context) {
inflater = LayoutInflater.from(context);
}
public void setData(List data) {
this.data = data;
}
@Override
public int getCount() {
return data == null ? 0 : data.size();
}
@Override
public Object getItem(int position) {
return null;
}
@Override
public long getItemId(int position) {
return 0;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder holder = null;
if (null == convertView) {
holder = new ViewHolder();
convertView = inflater.inflate(R.layout.item, null);
holder.isMan = (TextView) convertView.findViewById(R.id.isMan);
holder.name = (TextView) convertView.findViewById(R.id.name);
holder.age = (TextView) convertView.findViewById(R.id.age);
convertView.setTag(holder);
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.name.setText(data.get(position).getName() + "");
holder.age.setText(data.get(position).getAge() + "");
holder.isMan.setText(data.get(position).isMan() + "");
Log.i(TAG, data.get(position).getName() + "|" + data.get(position).getAge() + "|" + data.get(position).isMan());
return convertView;
}
private final class ViewHolder {
private TextView isMan;
private TextView name;
private TextView age;
}
}
然后是ListView里面的子布局。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/age"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/isMan"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
LinearLayout>
值得一提的是,ListView中子布局最外层的宽度和高度是不受控制的,无论将这两个值设为多少都是无效的,原因是这样的:要设置一个View的大小,要求这个View必须存在于一个布局中,那么大家可能会问,我平时用的Activity中的布局可以怎么随意设置大小呢,因为在Activity加载的时候就已经设置好了一个FramLayout,我们的setContentView方法只是把一个布局加载到这个FramLayout中,有兴趣的同学可以去研究一下LayoutInflat加载布局的流程。
接下来是我们的Activity的代码
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button firstWayBt;
private Button secondWayBt;
private Button thirdWayBt;
private ListView listView;
private MyAdapter adapter;
private List data;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initUI();
bindData();
}
private void initUI() {
firstWayBt = (Button) findViewById(R.id.FirstWay);
secondWayBt = (Button) findViewById(R.id.SecondWay);
thirdWayBt = (Button) findViewById(R.id.ThirdWay);
listView = (ListView) findViewById(R.id.listView);
}
private void bindData() {
data = initData();
adapter = new MyAdapter(this);
adapter.setData(data);
listView.setAdapter(adapter);
firstWayBt.setOnClickListener(this);
secondWayBt.setOnClickListener(this);
thirdWayBt.setOnClickListener(this);
}
private List initData() {
List personList = new ArrayList<>();
Person p1 = new Person("1号", 10, true);
Person p2 = new Person("2号", 20, true);
Person p3 = new Person("3号", 30, true);
Person p4 = new Person("4号", 40, true);
personList.add(p1);
personList.add(p2);
personList.add(p3);
personList.add(p4);
return personList;
}
private List getNewData() {
List personList = new ArrayList<>();
Person p1 = new Person("5号", 50, false);
Person p2 = new Person("6号", 60, false);
Person p3 = new Person("7号", 70, false);
Person p4 = new Person("8号", 80, false);
personList.add(p1);
personList.add(p2);
personList.add(p3);
personList.add(p4);
return personList;
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.FirstWay:
adapter.setData(getNewData());
break;
case R.id.SecondWay:
data = getNewData();
break;
case R.id.ThirdWay:
adapter.setData(data = getNewData());
break;
}
adapter.notifyDataSetChanged();
}
}
也是很简单的加载UI然后设置数据到ListView的流程。这里我写了三种更换数据的方式:
1. 第一种:FirstWay设置数据的方式就类似于,在网络请求拿到数据之后直接设置到Adapter中然后调用notifyDataSetChanged()方法。
2. 第二种:SecondWay设置数据的方法就是先给Adapter一个默认的数据data,然后网络请求拿到新的数据之后先赋值给原先的老数据data,再调用notifyDataSetChanged()方法刷新数据。
3. 第三种:ThirdWay设置数据的方法和第一种其实本质上是相同的,但是读起来逻辑是有点不同的,先给Adapter设置一个默认的data,然后通过网络请求获取到数据后替换到原来data的数据在设置给Adapter,在调用notifyDataSetChanged()方法。
默认打印的日志是这样的:
1号|10|true
2号|20|true
3号|30|true
4号|40|true
分别运行三种加载数据的方式,打印出来的日志是这样的:
第一种:
5号|50|false
6号|60|false
7号|70|false
8号|80|false
第二种:
1号|10|true
2号|20|true
3号|30|true
4号|40|true
第三种:
5号|50|false
6号|60|false
7号|70|false
8号|80|false
从日志可以看出来第二种方法的数据是没有更新的,既然数据没更新当然就不会刷新啦。所以使用Adapter设置数据时要注意数据源data的使用,防止出现使用notifyDataSetChange()方法数据不更新的错误。
然后我们再来看一下另一种情况!!!
让我们修改一下Activity的布局文件,很简单,去掉第三个按钮
id="@+id/FirstWay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="方法一" />
id="@+id/SecondWay"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="方法二" />
id="@+id/listView"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
然后为我们的Person类添加一个print()方法用来打印数据,这里直接用toString()方法,因为。。。直接用toString()方法的话会影响后面代码打印内存地址。。。
public String print() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", isMan=" + isMan +
'}';
}
修改一下Activity中的代码,如下:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button firstWayBt;
private Button secondWayBt;
private ListView listView;
private MyAdapter adapter;
private List data;
private static final String TAG = "MyMainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initUI();
bindData();
}
private void initUI() {
firstWayBt = (Button) findViewById(R.id.FirstWay);
secondWayBt = (Button) findViewById(R.id.SecondWay);
listView = (ListView) findViewById(R.id.listView);
}
private void bindData() {
data = initData();
Log.i(TAG, "第一次内存地址:" + data);
adapter = new MyAdapter(this);
adapter.setData(data);
listView.setAdapter(adapter);
firstWayBt.setOnClickListener(this);
secondWayBt.setOnClickListener(this);
}
private List initData() {
List personList = new ArrayList<>();
Person p1 = new Person("1号", 10, true);
Person p2 = new Person("2号", 20, true);
Person p3 = new Person("3号", 30, true);
Person p4 = new Person("4号", 40, true);
personList.add(p1);
personList.add(p2);
personList.add(p3);
personList.add(p4);
return personList;
}
private void getNewData1() {
List personList = new ArrayList<>();
Person p1 = new Person("100号", 100, false);
Person p2 = new Person("100号", 100, false);
Person p3 = new Person("100号", 100, false);
Person p4 = new Person("100号", 100, false);
personList.add(p1);
personList.add(p2);
personList.add(p3);
personList.add(p4);
data = personList;
}
private void getNewData2() {
for (Person person : data) {
person.setName("100号");
person.setAge(100);
person.setMan(false);
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.FirstWay:
getNewData1();
break;
case R.id.SecondWay:
getNewData2();
break;
}
printAddress();
adapter.notifyDataSetChanged();
}
private void printAddress() {
Log.i(TAG, "更新后的data内存地址:" + data);
for (Person person : data) {
Log.i(TAG, person.print());
}
}
}
修改了加载数据的方法,为了方便大家理解,在几个关键的地方输出一下日志。
private void bindData() {
data = initData();
Log.i(TAG, "第一次内存地址:" + data);
// 。。。省略代码
}
在第一次加载数据的地方打印内存地址。
private void getNewData1() {
List personList = new ArrayList<>();
Person p1 = new Person("100号", 100, false);
Person p2 = new Person("100号", 100, false);
Person p3 = new Person("100号", 100, false);
Person p4 = new Person("100号", 100, false);
personList.add(p1);
personList.add(p2);
personList.add(p3);
personList.add(p4);
data = personList;
}
然后第一种加载数据的方式,注意这里是new了一个新的ArrayList然后赋值给data,相当于data换了一个对象。
private void getNewData2() {
for (Person person : data) {
person.setName("100号");
person.setAge(100);
person.setMan(false);
}
}
第二种加载数据的方式,这里并没有像第一种方式那样new一个新的ArrayList然后赋值,而是直接修改原data中的数据。
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.FirstWay:
getNewData1();
break;
case R.id.SecondWay:
getNewData2();
break;
}
printAddress();
adapter.notifyDataSetChanged();
}
private void printAddress() {
Log.i(TAG, "更新后的data内存地址:" + data);
for (Person person : data) {
Log.i(TAG, person.print());
}
}
在点击按钮后增加了一个data内存地址和数据的方法。
我们首次加载Activity输出日志如下:
第一次内存地址:
[com.example.indicatedemo.Person@1d07ca95,
com.example.indicatedemo.Person@48d7aa,
com.example.indicatedemo.Person@835cf9b,
com.example.indicatedemo.Person@110ccf38]
点击方法一按钮之后输出日志如下:
更新后的data内存地址:
[com.example.indicatedemo.Person@1b3ceeac,
com.example.indicatedemo.Person@2eb23775,
com.example.indicatedemo.Person@845730a,
com.example.indicatedemo.Person@325b377b
Person{name='100号', age=100, isMan=false}
Person{name='100号', age=100, isMan=false}
Person{name='100号', age=100, isMan=false}
Person{name='100号', age=100, isMan=false}]
可以很明显发现:data的内存地址已经更改,已经不是原来的对象了,虽然看上去数据是变成了加载后的数据,但是已经不是原来的data了,然后可以看到界面并没有更新。
然后我们再试一下第二种方式(注意要重新进入软件才能保证地址是对的),输出日志如下:
第一次内存地址:
[com.example.indicatedemo.Person@1d07ca95, com.example.indicatedemo.Person@48d7aa, com.example.indicatedemo.Person@835cf9b, com.example.indicatedemo.Person@110ccf38]
更新后的data内存地址:
[com.example.indicatedemo.Person@1d07ca95, com.example.indicatedemo.Person@48d7aa, com.example.indicatedemo.Person@835cf9b, com.example.indicatedemo.Person@110ccf38]
Person{name='100号', age=100, isMan=false}
Person{name='100号', age=100, isMan=false}
Person{name='100号', age=100, isMan=false}
Person{name='100号', age=100, isMan=false}
可以看到内存地址是相同的,说明还是同一个对象,所以!界面改变了!
首次进入activity,我们adapter的数据源data指向一个内存地址,可以理解为adapte中的数据直接指向的是这个内存地址,然后通过getNewData1()方法加载数据,此时的data已经不是原来的data了,而是另外一个新的对象,指向了一个新的内存地址!然而adapter还是指向原来的内存地址,所以,用notifyDataSetChanged()方法刷新的是原来内存地址中的数据,发现压根就没变啊,所以视图根本就不会更新。然而我们通过getNewData2()方法,只是修改了数据,并不是新的对象,所有在用notifyDataSetChanged()方法刷新数据视图当然会改变啦!
听上去很复杂的样子,其实理解一下就是个内存指向的问题,自己特此记录一下