ネモラムネ

FragmentのViewとメモリリーク

#Android #Kotlin

2022-12-15

FragmentとViewのライフサイクルは違うよ。
ViewよりFragmentのほうが長生きだから、
FragmentにViewを保持してしまうと、
Androidのライフサイクル的に、Fragmentはまだ使うけどViewはもういらないって時に
いらなくなったViewを破棄できなくてメモリリークになっちゃうよ。

いろんなやり方で対策できるんだけど、
もしFragmentにViewを保持しなくてもいいなら保持しないようにしたほうがいいよ。

でもFragmentに保持しないと難しいってこともあると思うからいくつか対策を紹介するよ。

Nullable

Nullableで宣言してonDestroyViewでnullを代入する方法だよ。
結構よく見る方法で、シンプルで分かりやすいね。

class ExampleFragment: Fragment() {
    private var _binding: FragmentExampleBinding? = null
    private val binding get() = _binding!!

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

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

この方法はAndroidデベロッパーの
フラグメントでビュー バインディングを使用するにも書いてあるよ。
例ではbindingをNonNullにしてるけど、Nullableのまま使ってもよさそうだね

この方法の注意点としては、
onDestroyViewでbindingにnullを入れるの忘れちゃいがちなとこだよ

AutoClearedValue

Google公式のサンプルで使用されているAutoClearedValueというのもあるよ。
これはonCreateViewで値を代入するとonDestroyViewで自動的にnullを代入してくれるよ。
だからonDestroyViewでnullを入れ忘れることがないよ。

この方法の注意点は、値がNullの時に取得するとエラーするところだよ
だからonDestroyViewのあとにbindingを使っちゃうとエラーになっちゃうよ
そんなことあんまりないと思うけど非同期処理とかでたまにやっちゃうね

bind関数

bindingを使うたびにviewをbindingにする方法だよ
これも結構シンプルかも

class ExampleFragment: Fragment(R.layout.fragment_example) {
    private val binding get() = view?.let(FragmentExampleBinding::bind)
}

この方法の注意点はbindingを使うたびにbind関数が使われるから、
もしかしたらパフォーマンスに影響しちゃうところだよ

Delegateで保持する

AutoClearedValueを参考にして、Fragmentで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?>, DefaultLifecycleObserver {

    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(this)
            binding = bindingProvider()
            binding
        }

    override fun onDestroy(owner: LifecycleOwner) {
        super.onDestroy(owner)
        binding = null
    }
}

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

Fragmentで使うにはこんな感じ

class ExampleFragment: Fragment(R.layout.fragment_example) {
    private val binding by viewBinding(FragmentExampleBinding::bind)
}

僕はこれよく使ってるんだけど、
もしかしたら僕の知らないエラーとかあるかもしれないよ…

その他

そもそもxmlのレイアウトを使わずにJetPackComposeにしちゃえば全部解決だね!
といってもJetPackComposeも最近の技術だから
xmlでできたことが全部できるわけじゃないらしいよ

あとはこの問題を解決するライブラリとかもあるみたいだから調べてみてね!

自分のプロジェクトにあった解決方法を選択するのが重要だよ