ViewPager의 PagerAdapter POSITION_NONE의 비밀

요즘 안드로이드 UI는 페이지단위가 대세인듯 합니다. 마켓뿐안 아니라 내놓으라는 인기앱들을 보면 모두 페이징 개념의 UI로 구성되어 있습니다. 안드로이드에서 이런 페이징을 쉽게 구현하기 위해 ViewPager을 Support해주고 있습니다.

ViewPage는 View또는 Fragment를 페이지 단위로 관리 할수 있는 커스텀 뷰입니다. PageAdapter와 ViewPager을 같이 쓰면서 자원낭비에 대한 문제점을 알아보고자 합니다.

PageAdapter의 notifyDataSetChanged는 데이터가 바꼈으니 Notify를 하는 메소드 입니다.

PageAdapter에서 Fragment를 새로 생성하여 View를 만들때 이런 데이터 작업을 하게 됩니다, 그래서 notifyDataSetChanged를 하게 되더라도 Fragment내부의 View들이 새로고침이 되지 않는 문제점이 발생합니다. 강제로 OnCreateView를 태워 View를 그리거나 View를 refresh하도록 구현 해야 합니다.

 

그래서 대안으로 PageAdapter에서 getItemPosition을 오버라이드 하여 포지션값을 POSITION_NONE으로 주는 방법입니다.

POSITION_NONE으로 인해 ViewPager는 destoryItem()이 호출되어 Fragment가 삭제 된것으로 판단하게 되어 onCreateView가 호출되어 다시 그리는 방식입니다.

        public int getItemPosition(Object object) {
            return POSITION_NONE;
        }

 

데이터를 바꾸기 위해 notifyDataSetChanged를 호출 하면 Fragment를 새로 생성한다? 

단지 간단한 View일 경우 그렇게 문제 될점은 없을지 모르지만 ListView같은 복잡한 View들로 구성되어 있을때 자원이 낭비되는 문제점을 발생 할 수 있습니다.

 

따라서 이런 불필요한 낭비를 막기위해서는 instantiateItem에서 View들에 대한 정보를 저장해놓은 다음 데이터업데이트시 View정보를 불러와 refresh하도록 구현하길 권합니다.

SparseArray< View > views = new SparseArray< View >();

@Override
public Object instantiateItem(View container, int position) {
    View root = //refresh할 뷰
    ((ViewPager) container).addView(root);
    views.put(position, root);
    return root;
}

@Override
public void destroyItem(View collection, int position, Object o) {
    View view = (View)o;
    ((ViewPager) collection).removeView(view);
    views.remove(position);
    view = null;
}

@Override
public void notifyDataSetChanged() {
    int key = 0;
    for(int i = 0; i < views.size(); i++) {
       key = views.keyAt(i);
       View view = views.get(key);
       //refresh할 작업들 
    }
    super.notifyDataSetChanged();
}

이 처럼 작업하여 자원의 낭비를 막을 수 있습니다.

POSITION_NONE은 장점도 있다. 예를 들어 가로세로 전환시 ViewPager의 View들이 새로운레이아웃을불러 들이는 경우에는 이 방법을 써야 합니다.

어떻게 하라고 권고 할 말한 사항은 아닌만큼 앱의 특성에 맞게 사용하시길 바랍니다.

“ViewPager의 PagerAdapter POSITION_NONE의 비밀”에 대한 9개의 생각

  1. getItemPosition 메소드를 오버라이드하여 사용하는 경우에 대해 질문있습니다.

    데이터의 개수가 많은 경우, 데이터가 업데이트 될 때마다 모든 뷰가 재생성되는 것으로 이해됩니다.
    그런데, PagerAdapter의 경우 화면에 지금 보여지고 있는 뷰의 앞뒤 뷰들만 생성하고 가지고 있는 것으로 알고 있습니다. 즉, 페이지가 바뀔때마다 instantiateItem와 destroyItem 메소드를 이용하여 이후에 필요할 몇개의 뷰만 생성/삭제하는 것..아닌가요??

    그래서 데이터의 개수가 많더라도 뷰 재생성에 의한 로드는 크지 않을 것이라고 생각되는데,
    제가 이해한 바, 제가 알고있는 바가 맞는지 질문드립니다.

  2. 네 맞습니다. 데이터만큼 모든 뷰 클래스를 생성하는게 아닌 앞뒤만을 가지고 있습니다. 페이지를 넘길때 마가 뷰들이 삭제 재생성이 반복 되죠.. 즉, 재활용이 아닌 재생성을 한다는 것입니다. 리스트뷰의 경우 화면에 보이는 경우의 뷰만 최초시 생성해놓은 다음 재활용하는 방법이나 뷰페이저의 경우에는 다릅니다. 페이지가 넘어 갈때 마다 뷰를 삭제하고 재 생성한다는 것입니다.

    이 글의 요지는 뷰페이저에서 notifyDataSetChanged()를 하는 경우 뷰의 재생성이 아닌 재활용을 할 수 있는 방법에 대해 기술한 것입니다. 감사합니다.

  3. 안녕하세요
    이글로 해결을 할수있을것 같은데 잘못하겠습니다
    좀 도와주세요

    현상태 메인엑티비티 아래 프래그먼트3개로 탭메뉴를 구성했습니다.

    탭메뉴1 안에 프래그먼트로 페이저를 넣었는데
    탭메뉴2 의 데이터에 따라서 탭메뉴1 의 페이지의 갯수를 변형시키고 싶은데 잘안되네요
    아답타 겟카운트에서 페이지 갯수를 적어놓아도 제가 데이터를 갱신한 순간 반영되지안고 좀있다가 반영되네요 크리에이트 뷰에 대부분의 코드는 넣었습니다.

    탭메뉴간 데이터는 메인엑티비티에 어레이리스트를 만들고 그 데이터를 공유해서 하위프래그먼트간 데이터를 공유하고 있습니다.데이터가 잘공유되는것은 확인했습니다.

    겟카운트 리턴에 어래이사이즈를 넣으면
    이런에러가 발생합니다,
    “04-30 03:54:15.755 2044-2044/com.example.administrator.tab6 E/MessageQueue-JNI: java.lang.IllegalStateException: The application’s PagerAdapter changed the adapter’s contents without calling PagerAdapter#notifyDataSetChanged! Expected adapter item count: 1, found: 7 Pager id: com.example.administrator.tab6:id/pager Pager class: class android.support.v4.view.ViewPager Problematic adapter: class com.example.administrator.tab6.PlaceholderFragment2$PagerAdapter”

  4. mPagerAdapter = new PagerAdapter(getChildFragmentManager());
    mPagerAdapter.notifyDataSetChanged();
    pager.setAdapter(mPagerAdapter);
    이렇게 해봤습니다.

  5. 안녕하세요.
    일단 페이저 1번의 데이터에 따라 페이저 2번에 있는 뷰 페이저에 어떠한 일을 하고 싶으신것으로 질문을 받아 들이겠습니다.

    1. 페이저 1번을 관리하는 뷰페이저
    2. 페이저 2번내에 페이지를 관리하는 뷰페이저

    1번의 페이지 데이터 갯수의 상태를 보고 2번의 특정 행동을 하고 싶으신것 이기 때문에 1번의 뷰페이저의 Adapter내에 notifyDataSetChanged()를 오버라이드 하시면 데이터 갯수가 변경될때 마다 호출 됩니다.

    이때 2번의 뷰페이저에 하고 싶은 행동을 하시면 됩니다. 방법은 2번의 뷰페이저를 1번 뷰페이저의 Adapter에 넘겨 작업을 하시거나 1번 뷰페이저의 Adapter의 notifyDataSetChanged가 호출 될때 리스너를 만들어 사용하는 방법이 있습니다.

  6. setAdapter()를 하시면 Adapter 내부적으로 notifyDataSetChanged()가 호출 되기때문에 mPagerAdapter.notifyDataSetChanged();는 무의미한 코드입니다.

  7. 안녕하세요 올려주신 코드에서 View rootView에 refresh할 view를 넣으라고 되어있는데 구체적으로 어떻게 넣어야할지 감이 안잡히네요… 도아주세요

댓글 남기기