Android(Kotlin) ViewPager2 のメモリリーク回避
February 04, 2021
はじめに
ビューバインディングを学習した際に、公式ドキュメントに下記の通り記述されているのを確かに読んだのですが、
注: フラグメントはビューよりも持続します。フラグメントの onDestroyView() メソッドでバインディング クラスのインスタンスへの参照をすべてクリーンアップしてください。
「ふーん」で通り過ぎてしまったせいで、メモリリークが身近な存在になってしまいました・・・。
今回はメモリリークをせずに ViewPager2 を使う方法をまとめます。
前提
- フラグメントから ViewPager2 を使う
- TabLayoutMediator を使ってタブ対応
準備
まずは、LeakCanary という素晴らしいメモリリーク検知ライブラリがあるので、追加します。公式ドキュメントの Getting Started の通りにすれば使えるようになりますが、効率化のために、retainedVisibleThreshold の値を小さくしても良いかもしれません。
LeakCanary.config = LeakCanary.config.copy(retainedVisibleThreshold = 1)
これで、何も考えずに実装すると ViewPager2 ではメモリリークしてしまうことが分かります。
メモリリーク回避方法
要点
基本は onDestroyView
メソッド内で、ビューを内包するオブジェクトへの参照は消しましょうってことなので、今回のケースでは、
- TabLayoutMediator の
detach
メソッドを呼ぶ - TabLayoutMediator への参照も消す
- 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 が素晴らしいです。