关于Adapter的notifyDataSetChanged()方法数据不更新问题解析

概述

做安卓开发的同学应该大多都经历过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的布局文件,很简单,去掉第三个按钮

然后为我们的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()方法刷新数据视图当然会改变啦!

听上去很复杂的样子,其实理解一下就是个内存指向的问题,自己特此记录一下

你可能感兴趣的:(安卓开发)