FragmentのViewとメモリリーク

2022-02-13
AndroidKotlin

FragmentとViewのライフサイクルは違います。
Viewの生存期間よりFragmentの生存期間のほうが長いため、
FragmentにViewを保持してしまうとメモリリークとなります。

この対策方法は複数ありますが、
一番シンプルなのはそもそもFragmentにViewを保持しないという選択です。
といってもFragmentに保持したほうがいい場面もあるため対策をいくつか紹介します。

次点でシンプルなのは、
Nullableで宣言してonDestroyViewでnullを代入する方法です。

こんな感じです。

class HomeFragment: Fragment() {
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

この方法はAndroidデベロッパーのフラグメントでビュー バインディングを使用するにも書いてあります。
ただこれを100個のFragmentがあるアプリで書くのは少し面倒です。
特にonDestroyViewの記述は忘れてしまいそうですね。

もう一つの解決策としては、
Google公式のサンプルで使用されているAutoClearedValueというものです。
これはonCreateViewで値を代入するとonDestroyViewで自動的にnullを代入してくれます。
かなりシンプルで便利な物ですが、もう少しアレンジして使います。

AutoClearedValueでは値がNullの時に取得するとエラーを投げるようになっています。
実装としてはエラーを投げるのは正しいと思いますが、
非同期処理でも気軽に使えるようにNullableにしてみましょう。
また、ViewLifecycleで取得したいのはViewBindingだけでいいので専用にしてみます。

fun <T: ViewBinding>Fragment.viewBinding(
viewBinder: (View) -> T
): ReadOnlyProperty<Fragment, T?> = FragmentViewBindingDelegate { view?.let(viewBinder) }
private class FragmentViewBindingDelegate<T: ViewBinding>(
private val bindingProvider: () -> T?
): ReadOnlyProperty<Fragment, T?> {
private var binding: T? = null
override fun getValue(thisRef: Fragment, property: KProperty<*>) =
binding ?: thisRef.viewLifecycleOwnerLiveData.value?.lifecycle?.let{
if(!it.currentState.isAtLeast(Lifecycle.State.INITIALIZED)) return null
it.addObserver(object: DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
super.onDestroy(owner)
binding = null
}
})
binding = bindingProvider()
binding
}
}

書いてあることを簡単に説明すると
最初にbindingを取得した時にonCreateView以降だったらbindingがnullableで取得できます。
そしてonDestroyViewになるとbindingにnullが代入されて解放されます。

これをFragmentで使用するにはこのような形です。

class ExempleFragment: Fragment(R.layout.fragment_example) {
private val binding by viewBinding(FragmentExempleBinding::bind)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding?.textView?.text = "text"
}
}

一応、これがすべてではなく、
この問題を解決するライブラリがあるなど解決方法は多いのでプロジェクトにあったものを使うとよいと思います。

© 2022 シロボタン - Context Composition