Android(Kotlin) ViewPager2 のメモリリーク回避

はじめに

ビューバインディングを学習した際に、公式ドキュメントに下記の通り記述されているのを確かに読んだのですが、

注: フラグメントはビューよりも持続します。フラグメントの onDestroyView() メソッドでバインディング クラスのインスタンスへの参照をすべてクリーンアップしてください。

「ふーん」で通り過ぎてしまったせいで、メモリリークが身近な存在になってしまいました・・・。

今回はメモリリークをせずに ViewPager2 を使う方法をまとめます。

前提

  • フラグメントから ViewPager2 を使う
  • TabLayoutMediator を使ってタブ対応

準備

まずは、LeakCanary という素晴らしいメモリリーク検知ライブラリがあるので、追加します。公式ドキュメントの Getting Started の通りにすれば使えるようになりますが、効率化のために、retainedVisibleThreshold の値を小さくしても良いかもしれません。

LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 1)

これで、何も考えずに実装すると ViewPager2 ではメモリリークしてしまうことが分かります。

メモリリーク回避方法

要点

基本は onDestroyView メソッド内で、ビューを内包するオブジェクトへの参照は消しましょうってことなので、今回のケースでは、

  1. TabLayoutMediator の detach メソッドを呼ぶ
  2. TabLayoutMediator への参照も消す
  3. ViewPager2 の Adapter への参照も消す

で良いみたいです。少なくても、これで LeakCanary でメモリリークが検知されなくなりました。

サンプルコード

    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!
    private var tabLayoutMediator: TabLayoutMediator? = null

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
    ): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        binding.lifecycleOwner = viewLifecycleOwner

        val pagerAdapter = ScreenSlidePagerAdapter(this)
        binding.viewPager.adapter = pagerAdapter
        tabLayoutMediator =
            TabLayoutMediator(binding.tabLayout, binding.viewPager) { tab, position ->
                tab.text = resources.getString(getTabResId(position))
            }
        tabLayoutMediator!!.attach()
    }

    @StringRes
    private fun getTabResId(position: Int): Int {
        return when (position) {
            0 -> R.string.home_tab_0
            1 -> R.string.home_tab_1
            else -> throw IllegalArgumentException("position: $position")
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        tabLayoutMediator?.detach()
        tabLayoutMediator = null
        binding.viewPager.adapter = null
        _binding = null
    }


    private inner class ScreenSlidePagerAdapter(fragment: Fragment) :
        FragmentStateAdapter(fragment) {
        override fun getItemCount(): Int = 2

        override fun createFragment(position: Int): Fragment = when (position) {
            0 -> Tab1Fragment()
            1 -> Tab2Fragment()
            else -> throw IllegalArgumentException("position: $position")
        }
    }

おわりに

ViewPager2 のメモリリークについて書いたわけですが、そんなことより、とにかく、LeakCanary が素晴らしいです。