개발을 파헤치다/Android

[Android ViewPager2] Pager Adapter의 아이템 변경시 발생하는 동시성 문제 해결하기

개발자_H 2023. 3. 16. 10:00
반응형

ViewPager2는 안드로이드 앱 개발을 할 때 대표적으로 쓰이는 레이아웃 컴포넌트입니다. 옆으로 쓸어서 탭 이동을 하거나, 이미지 슬라이드 등 활용하는 곳이 상당히 많습니다. ViewPager2로 화면을 구성할 때 Pager Adapter를 필수적으로 구현을 하는데요. 여러 Fragment들을 여기에 붙였다 없앴다하는 경우 의도와는 다르게 동작하는 상황이 발생합니다. 이번에는 Pager Adapter에 특정 Fragment를 지우고 새로운 Fragment를 추가했을 때 기존 Fragment가 나오는 문제의 원인과 해결 방법을 알아보도록 하겠습니다.

 

 

 

문제상황

ViewPager2를 사용하다보면 내부 Fragment에 이동이 있거나 삭제하고 새로운 Fragment를 추가하는 등의 작업이 생길 수 있습니다.

좀 더 구체적인 예시를 들어보겠습니다. 개발하는 서비스에서 게스트 모드와 멤버 모드가 있다고 가정해 보죠. 그럼 로그인하기 전에는 마이페이지 탭 메뉴에서 로그인 화면을 보여줘야 하고 로그인 후에는 다른 화면을 보여줘야 합니다. 물론 레이아웃 구성값을 변경해서 다르게 보여주는 방법도 있습니다. 하지만 저는 서로 다른 Fragment에 각각 화면을 구성한 뒤 로그인 상황에 따라 다른 Fragment를 보여주는 방법을 선택했습니다.

 

val viewPager = binding.mainViewPager
val adapter = viewPager.adapter as MainBottomTabAdapter
// 모든 메뉴가 로드됐을 때에만 실행
if(adapter.itemCount >= 4){
    val myPageFragment = adapter.getFragment(MY_PAGE_TAB)
    // 로그인이 된 경우
    if(Permission.hasMemberPermission()){
        // 멤버 페이지가 아닌 경우 페이지를 교체해준다
        if( myPageFragment !is MyPageMemberFragment){
            adapter.remove(MY_PAGE_TAB)
            adapter.add(MY_PAGE_TAB, MyPageMemberFragment())
            changeTabFragment(HOME_PAGE_TAB) // viewpager의  currentItem 변경
        }
    }

Pager Adapter에 모든 탭 메뉴가 Fragment 형태로 보관되는데요. 이때 Permission 체크를 통해 로그인 한 경우에는 기존의 Fragment를 지우고 로그인된 레이아웃을 보여주는 새로운 Fragment를 넣어주게 됩니다. 이렇게 하면 마이페이지 탭을 선택했을 때 로그인된 화면이 나타나야 맞죠? 하지만 예전 Fragment가 나오는 상황이 발생합니다. 대체 왜 이런 문제가 발생할까요?

문제원인

디버깅을 통해 문제 원인을 분석해봤더니 동시성 문제였습니다. 원인을 파악할 수 있었던 이유는 한 줄 한 줄 디버깅을 했을 때에는 원래 의도대로 로그인된 Fragment 화면이 나타났기 때문이었습니다. 즉, Adapter를 통해 기존 Fragment를 지우고 새로운 Fragment를 넣은 뒤 UI를 새로 그려주기 전에 currentItem을 변경하는 메서드가 실행돼서 문제가 발생하게 된 것입니다.

 

# ViewPager Adapter

// fragment를 추가
    fun add(index : Int, view: Fragment){
        fragmentList.add(index, view)
        notifyItemInserted(index)
    }
    // fragment를 삭제
    fun remove(index : Int){
        fragmentList.removeAt(index)
        notifyItemRemoved(index)
    }

 

위의 Pager Adapter 메서드를 살펴봅시다. 새롭게 Fragment를 삭제 및 추가할 때 notify메서드를 통해 아이템 변경이 있으니 UI를 다시 그리라고 알려주게 됩니다. 이 과정이 끝나기 전에 currentItem을 바꾸는 메서드가 실행되어 이전 Fragment를 리턴할 수밖에 없었던 것이죠. 그럼 어떻게 이 문제를 해결해야 할까요?

 

 

 

 

 

해결방법

val viewPager = binding.mainViewPager
val adapter = viewPager.adapter as MainBottomTabAdapter
// 모든 메뉴가 로드됐을 때에만 실행
if(adapter.itemCount >= 4){
    val myPageFragment = adapter.getFragment(MY_PAGE_TAB)
    // 로그인이 된 경우
    if(Permission.hasMemberPermission()){
        // 멤버 페이지가 아닌 경우 페이지를 교체해준다
        if( myPageFragment !is MyPageMemberFragment){
            adapter.remove(MY_PAGE_TAB)
            adapter.add(MY_PAGE_TAB, MyPageMemberFragment())
            viewPager.post{
                changeTabFragment(HOME_PAGE_TAB)
            }
        }
    }

이 문제의 해결방법은 View 클래스에서 제공하는 post 메서드를 사용하는 것입니다. 이렇게하면 문제가 해결되죠. 왜 그럴까요?

 

 

post 메서드는 View에서 제공하는 메서드입니다. 설명을 보면 Runnable을 생성하여 Message Queue에 넣는다고 나와있습니다. 그리고 이 Runnable은 UI Thread에서 처리가 된다고도 나와있죠. 안드로이드는 여러 Thread가 UI 자원에 동시 접근해서 동기화 문제가 발생하지 않도록 하나의 Thread에서만 UI 작업이 가능합니다.

 

ViewPager의 setCurrentItem을 Fragment 교체 로직 직후에 바로 호출하게 되면 기존의 Fragment 교체 작업이 무시된 채 전의 Fragment를 보여주게 됩니다. 하지만 Post 메서드를 사용하면 UI Thread의 Message Queue에 작업이 들어가고 기존의 교체 작업이 모두 완료된 뒤 setCurrentItem을 실행하기 때문에 정상적으로 교체된 Fragment가 나오게 되는 것이죠.

 

 

반응형