android ViewPager 에서 삽질한 내용 정리

김토끼·2022년 3월 8일
3

ViewPager 에서 삽질한 내용 정리


1.

안드로이드 뷰페이저를 사용할때는 보통 ViewPager2를 생성하고 거기에 FragmentStateAdapter를 연결하게 된다

// MainActivity.kt
class MainActivity: AppCompatActivity() {
    ...
    binding.viewPager.adapter = MainPagerAdapter(this@MainActivity)
    ...
}


// MainPagerAdapter.kt
class CustomPagerAdapter(activity: FragmentActivity): FragmentStateAdapter(activity) {
    override fun createFragment(position: Int): Fragment {
        return when (position) {
            0 -> HomeFragment()
            // 나머지 Fragment 생성
        }
    }
    ...
}

2.

Activity 에서 위 코드를 쓰면 잘 동작한다. 하지만 똑같은 코드를 Fragment에서 작성하게 되는 경우가 있다.
내 경우는 BottomNavigationView으로 탭을 넣고 탭의 Fragment 내에서 ViewPager를 사용하는 화면이었다.

//HomeFragment.kt
class HomeFragment: Fragment(R.layout.fragment_home) {
    ...
    binding.viewPager.adapter = HomePagerAdapter(requireActivity())
    ...
}


//HomePagerAdapter.kt
class HomePagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
    override fun createFragment(position: Int): Fragment {
        return when (position) {
            // 페이지별 Fragment 생성
            0 -> HomeInnerFragment()
            ...
        }
    }
    ...
}

겉보기엔 정상 동작하는 것처럼 보였지만 탭을 여러번 이동한 후 HomeInnerFragment에서 데이터를 리로드 하면 똑같은 API가 두 번 이상 호출되거나 LiveData에 전달한 값을 Fragment에서 받아오지 못하는 문제가 발생했다.

원인을 찾아보기 위해 생성되는 내부 Fragment의 lifecycle마다 로그로 해시코드를 찍어보니 같은 페이지의 같은 lifecycle이 서로 다른 해시코드로 두번씩 찍히는걸 발견할 수 있었다. 즉 HomeInnerFragment가 중복으로 생성된 것이었다.

3.

Fragment 중복 생성 문제를 해결하기 위해 처음 시도했던 방법은 Fragment를 생성할때 리스트에 저장하고 createFragment()에서 Fragment가 이미 생성되있으면 리스트에서 꺼내오는 것이었다.

// HomePagerAdapter.kt
class HomePagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
    private val fragments: SparseArray<Fragment> = SparseArray()

    override fun createFragment(position: Int): Fragment {
        return fragments[position] ?: run {
            when (position) {
                // 페이지별 Fragment 생성
                0 -> HomeInnerFragment().also {
                    fragments.append(position, it)
                }
                ...
            }
        }
    }
    ...
}

하지만 똑같이 내부 Fragment가 중복 생성, 호출되는 문제는 해결되지 않았다. HomeInnerFragment 뿐만 아니라 HomeFragment까지 중복으로 생성되고 있었다.

HomeFragment에서 로그를 찍어보니 onCreate(), onCreateView() 등은 찍히지만 onDestroyView(), onDestroy()가 호출되지 않고 있었다.

4.

android 공식문서를 보면 FragmentStateAdapter는 생성자가 3종류 있다.

FragmentStateAdapter(FragmentActivity fragmentActivity)
FragmentStateAdapter(Fragment fragment)
FragmentStateAdapter(FragmentManager fragmentManager, Lifecycle lifecycle)

이걸 보고 HomeFragment의 어댑터를 FragmentStateAdapter(fragment)로 변경하니 드디어 정상적으로 동작했다!

//HomeFragment.kt
class HomeFragment: Fragment(R.layout.fragment_home) {
    ...
    binding.viewPager.adapter = HomePagerAdapter(this@HomeFragment)
    ...
}

//HomePagerAdapter.kt
class HomePagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
    override fun createFragment(position: Int): Fragment {
        return when (position) {
            // 페이지별 Fragment 생성
            0 -> HomeInnerFragment()
            ...
        }
    }
    ...
}

5.

다음날 작업을 하다가 IllegalArgumentException이 발생했다.
HomeFragment의 뷰페이저 내에서 다른 Fragment를 전체화면(android.R.id.content)으로 add 하다가 발생한 에러였다.

java.lang.IllegalArgumentException: No view found for id 0x1020002 (android:id/content) for fragment

구글링을 해보니 fragment에서 parentFragmentManager 대신 childFragmentManager를 사용하라는 등의 답변이 있었지만 전부 동작하지 않았다.
에러로그를 보니 화면 전체에 Fragment를 add 하기 위해 썼던 android.R.id.content를 찾지 못한다는 거였다.

곰곰히 생각해보니 바뀐건 HomePagerAdapter의 생성자를 FragmentActivity에서 Fragment로 바꾼것 밖에 없었다.

추측이긴 하지만 생성자에 전달하는 값이 Activity 에서 Fragment로 바뀌면서 android.R.id.content를 참조할 수 없게 된 것 같았다.

6.

HomePagerAdapter 생성자에 activity를 넣으면 Fragment가 중복으로 생성되고 fragment를 넣으면 activity의 android.R.id.content를 참조할 수 없다.
그럼 마지막 생성자를 사용해보면 어떨까 싶어서 다시 바꿔봤다.

// HomeFragment.kt
class HomeFragment: Fragment(R.layout.fragment_home) {
    ...
    // HomeFragment의 parent(=MainActivity)의 FragmentManager와 자신의 Lifecycle을 전달
    binding.viewPager.adapter = HomePagerAdapter(parentFragmentManager, lifecycle)
    ...
}

// HomePagerAdapter.kt
class HomePagerAdapter(fm: Fragmentmanager, lifecycle: Lifecycle)
    : FragmentStateAdapter(fm, lifecycle) {

    override fun createFragment(position: Int): Fragment {
        return when (position) {
            // 페이지별 Fragment 생성
            0 -> TestFragment()
            ...
        }
    }
    ...
}

드디어 생각한대로 동작한다.
parentFragmentManager를 전달해서 Activity의 FragmentManager를 참조하도록 하고 대신 어댑터가 Fragment의 lifecycle을 따라가도록 했다.

MainActivity에서 탭이 변경되면 HomeFragment가 제대로 onDestroy 되고 다시 홈탭으로 가면 내부 Fragment와 ViewModel까지 새로 생성된다.

profile
방구석 김토끼🐰

0개의 댓글