
【Android】【Kotlin】Fragmentのライフサイクルに合わせて自動に参照を消してくれる仕組みを作る
2023.02.22
はじめに
Fragment で View の参照を持っている場合、 onDestoryView() で参照を取り除いておくべきらしい。
恥ずかしながら、このことを最近知りました。
ViewBindingのガイドの例では
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | private var _binding: ResultProfileBinding? = null // This property is only valid between onCreateView and // onDestroyView. private val binding get() = _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { _binding = ResultProfileBinding.inflate(inflater, container, false) val view = binding.root return view } override fun onDestroyView() { super.onDestroyView() _binding = null } |
更に続けて
注: フラグメントはビューよりも持続します。フラグメントの
onDestroyView()
メソッドでバインディング クラスのインスタンスへの参照をすべてクリーンアップしてください。
と書かれています。
なるほど?
と思うと同時に、面倒くさいし実運用では onDestoryView() での = null を忘れそうな気がしました。
とりあえず、 DelegatedProperties を使えば解決できそうです。
車輪の再発明になるかもですが、自分で使いやすいように作っていこうと思います。
Fragmentのライフサイクルに合わせて自動に参照を消してくれる仕組みを作る
ライフサイクルに合わせて参照を消してくれる仕組みは、以前から個人的にKotterKnifeから一部拝借して使っていました。
https://gist.github.com/chrisbanes/fc4392dcbdc0aa5d99147dc551616676#file-kotterknife-kt-L123
ただ↑は ReadOnlyProperty なので、var の変数では使えないです。これをベースにしてReadWriteProperty
で作り直すことにしました。
ライフサイクルと紐付けるための LifecycleObserver が Deprecated になっているので代わりの DefaultLifecycleObserver を使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | private object EMPTY class LifecycleReadWriteProperty<T : LifecycleOwner, V> : ReadWriteProperty<T, V>, DefaultLifecycleObserver { private var value: Any? = EMPTY private var attachedToLifecycleOwner = false private var thisRef: T? = null override fun getValue(thisRef: T, property: KProperty<*>): V { this.thisRef = thisRef checkAddToLifecycleOwner(thisRef) if (value == EMPTY) { throw UninitializedPropertyAccessException() } @Suppress("UNCHECKED_CAST") return value as V } private fun checkAddToLifecycleOwner(thisRef: T) { if (!attachedToLifecycleOwner) { when (thisRef) { is Fragment -> thisRef.viewLifecycleOwner.lifecycle.addObserver(this) else -> thisRef.lifecycle.addObserver(this) } attachedToLifecycleOwner = true } } override fun setValue(thisRef: T, property: KProperty<*>, value: V) { checkAddToLifecycleOwner(thisRef) this.value = value } override fun onDestroy(owner: LifecycleOwner) { value = EMPTY when (val thisRef = this.thisRef) { is Fragment -> thisRef.viewLifecycleOwner.lifecycle.removeObserver(this) else -> thisRef?.lifecycle?.removeObserver(this) } attachedToLifecycleOwner = false this.thisRef = null super.onDestroy(owner) } } |
これで、 onDestory() がライフサイクルに合わせて呼び出されて、参照を消せるようになりました。
加えて↓
1 | inline fun <reified T : Fragment, V> bindViewLifecycle() = LifecycleReadWriteProperty<T, V>() |
↑拡張関数を作成して Fragment で使いやすくすることで
公式 ViewBinding ガイドの例を以下のように書き換えることができます。
1 2 3 4 5 6 7 8 9 10 11 12 | private val binding: ResultProfileBindnig by bindViewLifecycle() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { binding = ResultProfileBinding.inflate(inflater, container, false) val view = binding.root return view } } |
だいぶスッキリしましたね。
実装中に心配になったonDestroyの呼び出しタイミング
DefaultLifecycleObserver の override fun onDestroy() の呼び出しタイミングってどこなんだろう?
ということで DefaultLifecycleObserver.onDestory() の呼び出しを遡ってみることにしました。
FullLifecycleObserverAdapter.onStateChanged で Lifecycle.Event.ON_DESTROY のときに呼び出されているんですねー
そのまま更に辿ると、 Fragment.performDestroyView() にて Lifecycle.Event.ON_DESTROY を設定していることがわかりました。
1 2 3 4 5 6 7 8 9 10 11 | void performDestroyView() { mChildFragmentManager.dispatchDestroyView(); if (mView != null && mViewLifecycleOwner.getLifecycle().getCurrentState() .isAtLeast(Lifecycle.State.CREATED)) { mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); } mState = CREATED; mCalled = false; onDestroyView(); /* ...以下省略... */ } |
よく読んでみると、直後に Fragment.onDestroyView() を呼び出していますね。
つまり、
DefaultLifecycleObserverのoverride fun onDestroy() は、Fragment の onDestoryView() 直前で呼び出されている。
ということらしい。なるほど勉強になった。
おまけ
mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
の mViewLifecycleOwner は、 Fragment.getViewLifecycleOwner() で取得できる LifecycleOwner だ。
なるほど、やっぱり Fragment.getLifecycle() で取得できるライフサイクルとは別物で、イベントのタイミングが微妙に違うんですねー
改めて、Fragement でライフサイクルと View を絡めるときは Fragment.getViewLifecycleOwner() を使うべき、ということがなんとなくわかった。
書いた人はこんな人
