ViewPager、notifyDataSetChanged和观察者模式

本文探讨了在Android中使用ViewPager时为何在数据更改后需要调用notifyDataSetChanged(),通过源码分析揭示了这与观察者模式的关系。在不调用notifyDataSetChanged()时,由于ViewPager和PagerAdapter之间的数据同步问题会导致程序崩溃。通过观察者模式的实例解释了如何避免这种情况,并总结了观察者模式在数据更新通知中的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

最近用ViewPager做一个用户引导界面,对于为什么在setAdapter后,再对数据更改时要加notifyDataSetChanged()产生了疑惑。在不加notifyDataSetChanged()时,程序就会崩溃,并提醒你要加上notifyDataSetChanged。通过查找源码找到了答案,并且学习了一下设计模式中的观察者模式,不对的地方,欢迎指教和艾特我出来挨打。

二、源码解析

初学android的时候,学到ListView的时候,会先将要展示的数据准备好放到adapter中,再调用ListView中的setAdapter,并将adapter传入,从而实现一个简单的ListView。同理ViewPager也可以这么实现。但最近看到一些代码是将顺序颠倒,先setAdapter将adapter传,再设置adapter中设置数据,并加一个notifyDataSetChanged()。

  vp.setAdapter(adapter);
  ArrayList<Integer> datas = new ArrayList<>();
  datas.add(R.drawable.guide1);
  datas.add(R.drawable.guide2);
  datas.add(R.drawable.guide3);
  adapter.setDatas(datas); 
    
//adapter中setDatas代码
public void setDatas(List<T> data){
        if(data!=null&&data.size()>0){
            datas.clear();
            datas.addAll(data);
            notifyDataSetChanged();
        }
    }

在不加notifyDataSetChanged(),程序直接崩溃。在崩溃点找到如下源码,是在populate函数里,关于这个函数,网上说的是跟View页面绘制有关,这里我也不太懂,但可以知道是在页面绘制时出的问题。

 void populate(int newCurrentItem) {
///
 final int pageLimit = mOffscreenPageLimit;
 final int startPos = Math.max(0, mCurItem - pageLimit);
 final int N = mAdapter.getCount();   
 final int endPos = Math.min(N-1, mCurItem + pageLimit);

 if (N != mExpectedAdapterCount) {
        String resName;
        try {
              resName = getResources().getResourceName(getId());
        } catch (Resources.NotFoundException e) {
                resName = Integer.toHexString(getId());
        }
        throw new IllegalStateException("The application's PagerAdapter changed the adapter's" +
                    " contents without calling PagerAdapter#notifyDataSetChanged!" +
                    " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N +
                    " Pager id: " + resName +
                    " Pager class: " + getClass() +
                    " Problematic adapter: " + mAdapter.getClass());
        }
}

可以看到final int N = mAdapter.getCount();getCount()是一个抽象函数,在使用adapter时,需要我们自己实现,用来返回数据的个数。在获取到数据个数后会与一个成员变量mExpectedAdapterCount做比较,不相等的话,就会抛出异常,异常里的信息就是要我们加上notifyDataSetChanged。看来问题就是出在这个mExpectedAdapterCount上,接下来要看看这个成员变量是什么。

     /**
     * Set a PagerAdapter that will supply views for this pager as needed.
     *
     * @param adapter Adapter to use
     */
    public void setAdapter(PagerAdapter adapter) {
       

        final PagerAdapter oldAdapter = mAdapter;
        mAdapter = adapter;
        mExpectedAdapterCount = 0;

        if (mAdapter != null) {
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            mAdapter.registerDataSetObserver(mObserver);
            mPopulatePending = false;
            final boolean wasFirstLayout = mFirstLayout;
            mFirstLayout = true;
            mExpectedAdapterCount = mAdapter.getCount();
       
        }
    }

在setAdapter中可以看到首先将mExpectedAdapterCount赋值为0,接着判断适配器是否为空,在最后我们又一次看到了熟悉的代码,mExpectedAdapterCount = mAdapter.getCount(),又是获取adapter数据的个数。这时候,我们就知道为什么不加notifyDataSetChanged会崩溃,因为在先使用setAdapter的时候,会对mExpectedAdapterCount赋一次值,值为数据的个数,这时候,我们并没有在adapter中传入数据,所以数组的大小为0,我们接着往adapter里入传数据,然后View开始绘制页面时,执行到populate时再次获取适配器里的数据,这时候数据已经发生变化了,所以发生了崩溃。知道了崩溃的原因后,我们就可以知道加notifyDataSetChanged肯定是将mExpectedAdapterCount的值修正了,但是我们还是要看一下源码,看它是在哪里将值修正的,毕竟眼见为实。


    /**
     * This method should be called by the application if the data backing this adapter has changed
     * and associated views should update.
     */
    public void notifyDataSetChanged() {
        mObservable.notifyChanged();
    }

找到PagerAdapter里的notifyDataSetChanged函数,看到是调用了mObservable的notifyChanged函数,mObservable下面会介绍是什么,这里先看notifyChanged函数。

 /**
     * Invokes {@link DataSetObserver#onChanged} on each observer.
     * Called when the contents of the data set have changed.  The recipient
     * will obtain the new contents the next time it queries the data set.
     */
    public void notifyChanged() {
        synchronized(mObservers) {
            // since onChanged() is implemented by the app, it could do anything, including
            // removing itself from {@link mObservers} - and that could cause problems if
            // an iterator is used on the ArrayList {@link mObservers}.
            // to avoid such problems, just march thru the list in the reverse order.
            for (int i = mObservers.size() - 1; i >= 0; i--) {
                mObservers.get(i).onChanged();
            }
        }
    }

notifyChanged里的代码很少,重点是其中的for循环,遍历了mObservers中的数据,调用它们的onChanged函数。。

  private class PagerObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            dataSetChanged();
        }
        @Override
        public void onInvalidated() {
            dataSetChanged();
        }
    }

在ViewPager里看到 onChanged的实现,调用了dataSetChanged函数。

  void dataSetChanged() {
        // This method only gets called if our observer is attached, so mAdapter is non-null.

        final int adapterCount = mAdapter.getCount();
        mExpectedAdapterCount = adapterCount;
        boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 &&
                mItems.size() < adapterCount;
        int newCurrItem = mCurItem;

      
    }

在这个函数的开头我们就可以看到mExpectedAdapterCount被adapterCount更新,adapterCount 是mAdapter.getCount()里得到的,现在mExpectedAdapterCount已经被更新为最新的值,所以就不会产生崩溃。至此崩溃的原因和解决办法已经明了,但是在我初学的时候,对类之间的关系和函数调用看的云里雾里,但在看到设计模式中的观察者模式后,对整体的脉络更加清晰了,下面介绍一下观察者模式。

3、观察者模式

观察者模式,在生活中随处可见。比如你关注了一位博主,在有新的内容更新或者发布的时候,就会通知你。在这里订阅者就是一个观察者,博主就是一个被观察者。android里的广播是不是就有种似曾相识的感觉,下面我自己实现了个简单的观察者模式。

public interface MyObserver {
    public void myupdate(MyObservable myObservable);
}

首先是定义了一个观察者的接口,里面只有一个myupdate函数,参数为MyObservable,是一个自己定义的一个被观察者。

import java.util.ArrayList;
import java.util.List;

public class  MyObservable {
    private String sendInfo;
    private List<MyObserver> mMyObservers;

    public MyObservable(){
        mMyObservers = new ArrayList<>();
    }

    protected void MyNotificationDataChanged(){
        for(MyObserver myObserver : mMyObservers){
            myObserver.myupdate(this);
        }
    }

    public void AddObservrt(MyObserver myObserver){
        mMyObservers.add(myObserver);
    }
    protected void setInfo(String string){
        this.sendInfo = string;
    }

    public String getInfo(){
        return "接受广播:"+ this.sendInfo;
    }

}

这是一个被观察者类,其中定义了一个MyObserver数组,接着看AddObserver,代码很简单,将传进来的myObserver添加到数组里,最后看MyNotificationDataChanged函数,是不是和android源码中的notifyChanged很像?遍历mMyObservers数组,调用它们的myupdate函数,这里myupdate还没实现,下面来实现它。

import javafx.beans.Observable;



public class Coder implements MyObserver {
    public String name;

    public Coder(String aName){
        name = aName;
    }
    @Override
    public void myupdate(MyObservable myObservable) {
        System.out.println(this.name + myObservable.getInfo());
    }
}

接着实现了MyObserver接口,并重写了myupdate,这里打印了下从被观察者中得到的数据,接着我们再写一个继承被观察者的类。

import java.util.Observable;

public class MyBroadcast extends MyObservable {
    public  MyBroadcast(){}

    public void sendBroadcast(String content){
        setInfo(content);
        MyNotificationDataChanged();
    }
}

定义了一个MyBroadcast继承MyObservable,定义了一个成员函数sendBroadcast,setInfo用来更新内容,然后调用函数MyNotificationDataChanged来通知订阅观察者。终于写好了,可以写demo测试下。

public class Test {
    public static void  main(String[] args){
        MyBroadcast myBroadcast = new MyBroadcast();
        Coder coder_1 = new Coder("coder_1");
        Coder coder_2 = new Coder("coder_2");
        Coder coder_3 = new Coder("coder_3");
        Coder coder_4 = new Coder("coder_4");

        myBroadcast.AddObserver(coder_1);
        myBroadcast.AddObserver(coder_2);
        myBroadcast.AddObserver(coder_3);
        myBroadcast.AddObserver(coder_4);

        myBroadcast.sendBroadcast("我是新消息\n");
    }
}


-----------------------------------------------------------
 Test
coder_1接受广播:我是新消息

coder_2接受广播:我是新消息

coder_3接受广播:我是新消息

coder_4接受广播:我是新消息

总结下,观察者加入到被观察者中的数组中,完成了注册,被观察者通过SendBroiadcast->MyNotificationDataChanged遍历数组中的所有数据,并调用它们的myupdate函数从而完成通知。到此我们就可拿这个demo和前面的adnroid源码中的代码作比较,会发现原理是一样的。,PageView是根据PagerAdapter中的数据来显示的,所以说PagerAdapter是一个被观察者,ViewPager是一个观察者。我们可以回头来看下源码中的ViewPager中的setAdapter函数。

    /**
     * Set a PagerAdapter that will supply views for this pager as needed.
     *
     * @param adapter Adapter to use
     */
    public void setAdapter(PagerAdapter adapter) {
       ///

        if (mAdapter != null) {
            if (mObserver == null) {
                mObserver = new PagerObserver();
            }
            mAdapter.registerDataSetObserver(mObserver);
            mPopulatePending = false;
            final boolean wasFirstLayout = mFirstLayout;
            mFirstLayout = true;
            mExpectedAdapterCount = mAdapter.getCount();
            if (mRestoredCurItem >= 0) {
                mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader);
                setCurrentItemInternal(mRestoredCurItem, false, true);
                mRestoredCurItem = -1;
                mRestoredAdapterState = null;
                mRestoredClassLoader = null;
            } else if (!wasFirstLayout) {
                populate();
            } else {
                requestLayout();
            }
        }
        
    }

可以看到mAdapter.registerDataSetObserver(mObserver),其中mAdapter是一个PagerAdapter,mObserver是一个观察者,可以看一下它的数据类型和实现。

private class PagerObserver extends DataSetObserver {
        @Override
        public void onChanged() {
            dataSetChanged();
        }
        @Override
        public void onInvalidated() {
            dataSetChanged();
        }
    }

再次翻到前面的解析,它重写了观察者的onChanged方法,跟我们demo里的myupdate类似。这时候观察者有了,被观察者也有了,demo里是用AddObserver函数联结它们,将观察者加入到数组,这里可以猜到registerDataSetObserver应该做了同样的事。

/**
     * Register an observer to receive callbacks related to the adapter's data changing.
     *
     * @param observer The {@link android.database.DataSetObserver} which will receive callbacks.
     */
    public void registerDataSetObserver(DataSetObserver observer) {
        mObservable.registerObserver(observer);
    }

这里调用了mObservable的registerObserver函数,mObservable是一个DataSetObservable类。


    /**
     * Adds an observer to the list. The observer cannot be null and it must not already
     * be registered.
     * @param observer the observer to register
     * @throws IllegalArgumentException the observer is null
     * @throws IllegalStateException the observer is already registered
     */
    public void registerObserver(T observer) {
        if (observer == null) {
            throw new IllegalArgumentException("The observer is null.");
        }
        synchronized(mObservers) {
            if (mObservers.contains(observer)) {
                throw new IllegalStateException("Observer " + observer + " is already registered.");
            }
            mObservers.add(observer);
        }
    }

最关键的是最后一句mObservers.add(observers),来看一下mObservers的数据类型。

protected final ArrayList<T> mObservers = new ArrayList<T>();

是一个数组!!是不是发现跟我们的demo相似度高的离谱。这说明了ViewPager和PageDapter之间使用了观察者模式。

四、总结

总结分析完整个流程后,可以直观的得到,在adapter中更改了数据,而不受用notifyDataSetChanged就相当于被观察者更新了内容,却不通知你,这样的订阅毫无意义。观察者模式应该做到被观察者更改了数据,就需要通知订阅者,至于订阅者怎么处理,这就不管它的事了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值