• トップ
  • ブログ一覧
  • 【Android】【Kotlin】Fragmentのライフサイクルに合わせて自動に参照を消してくれる仕組みを作る
  • 【Android】【Kotlin】Fragmentのライフサイクルに合わせて自動に参照を消してくれる仕組みを作る

    えばたん(エンジニア)えばたん(エンジニア)
    2022.08.31

    IT技術

    はじめに

    Fragment で View の参照を持っている場合、onDestoryView() で参照を取り除いておくべきらしい。

    恥ずかしながら、このことを最近知りました。

    ViewBindingのガイドの例では

    1    private var _binding: ResultProfileBinding? = null
    2    // This property is only valid between onCreateView and
    3    // onDestroyView.
    4    private val binding get() = _binding!!
    5
    6    override fun onCreateView(
    7        inflater: LayoutInflater,
    8        container: ViewGroup?,
    9        savedInstanceState: Bundle?
    10    ): View? {
    11        _binding = ResultProfileBinding.inflate(inflater, container, false)
    12        val view = binding.root
    13        return view
    14    }
    15
    16    override fun onDestroyView() {
    17        super.onDestroyView()
    18        _binding = null
    19    }

    更に続けて

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

    と書かれています。

    なるほど?

    と思うと同時に、面倒くさいし実運用ではonDestoryView() での= null を忘れそうな気がしました。

    とりあえず、DelegatedProperties を使えば解決できそうです。

    車輪の再発明になるかもですが、自分で使いやすいように作っていこうと思います。

    Fragmentのライフサイクルに合わせて自動に参照を消してくれる仕組みを作る

    ライフサイクルに合わせて参照を消してくれる仕組みは、以前から個人的にKotterKnifeから一部拝借して使っていました。

    https://gist.github.com/chrisbanes/fc4392dcbdc0aa5d99147dc551616676#file-kotterknife-kt-L123

    ただ↑はReadOnlyProperty なので、var の変数では使えないです。これをベースにしてReadWritePropertyで作り直すことにしました。

    ライフサイクルと紐付けるためのLifecycleObserver  が Deprecated になっているので代わりのDefaultLifecycleObserver を使います。

    1private object EMPTY
    2
    3class LifecycleReadWriteProperty<T : LifecycleOwner, V> : ReadWriteProperty<T, V>, DefaultLifecycleObserver {
    4    private var value: Any? = EMPTY
    5    private var attachedToLifecycleOwner = false
    6    private var thisRef: T? = null
    7
    8    override fun getValue(thisRef: T, property: KProperty<*>): V {
    9        this.thisRef = thisRef
    10        checkAddToLifecycleOwner(thisRef)
    11        if (value == EMPTY) {
    12            throw UninitializedPropertyAccessException()
    13        }
    14        @Suppress("UNCHECKED_CAST")
    15        return value as V
    16    }
    17
    18    private fun checkAddToLifecycleOwner(thisRef: T) {
    19        if (!attachedToLifecycleOwner) {
    20            when (thisRef) {
    21                is Fragment -> thisRef.viewLifecycleOwner.lifecycle.addObserver(this)
    22                else -> thisRef.lifecycle.addObserver(this)
    23            }
    24            attachedToLifecycleOwner = true
    25        }
    26    }
    27
    28    override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
    29        checkAddToLifecycleOwner(thisRef)
    30        this.value = value
    31    }
    32
    33    override fun onDestroy(owner: LifecycleOwner) {
    34        value = EMPTY
    35        when (val thisRef = this.thisRef) {
    36            is Fragment -> thisRef.viewLifecycleOwner.lifecycle.removeObserver(this)
    37            else -> thisRef?.lifecycle?.removeObserver(this)
    38        }
    39        attachedToLifecycleOwner = false
    40        this.thisRef = null
    41        super.onDestroy(owner)
    42    }
    43}

    これで、onDestory() がライフサイクルに合わせて呼び出されて、参照を消せるようになりました。

    加えて↓

    1inline fun <reified T : Fragment, V> bindViewLifecycle() = LifecycleReadWriteProperty<T, V>()

    ↑拡張関数を作成して Fragment で使いやすくすることで

    公式 ViewBinding ガイドの例を以下のように書き換えることができます。

    1    private val binding: ResultProfileBindnig by bindViewLifecycle()
    2
    3    override fun onCreateView(
    4        inflater: LayoutInflater,
    5        container: ViewGroup?,
    6        savedInstanceState: Bundle?
    7    ): View? {
    8        binding = ResultProfileBinding.inflate(inflater, container, false)
    9        val view = binding.root
    10        return view
    11    }
    12}

    だいぶスッキリしましたね。

    実装中に心配になったonDestroyの呼び出しタイミング

    DefaultLifecycleObserveroverride fun onDestroy() の呼び出しタイミングってどこなんだろう?

    ということでDefaultLifecycleObserver.onDestory() の呼び出しを遡ってみることにしました。

    FullLifecycleObserverAdapter.onStateChangedLifecycle.Event.ON_DESTROY のときに呼び出されているんですねー

    そのまま更に辿ると、Fragment.performDestroyView() にてLifecycle.Event.ON_DESTROY を設定していることがわかりました。

    1void performDestroyView() {
    2        mChildFragmentManager.dispatchDestroyView();
    3        if (mView != null && mViewLifecycleOwner.getLifecycle().getCurrentState()
    4                        .isAtLeast(Lifecycle.State.CREATED)) {
    5            mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
    6        }
    7        mState = CREATED;
    8        mCalled = false;
    9        onDestroyView();
    10        /* ...以下省略... */
    11    }

    よく読んでみると、直後にFragment.onDestroyView() を呼び出していますね。

    つまり、

    DefaultLifecycleObserverのoverride fun onDestroy() は、Fragment のonDestoryView() 直前で呼び出されている。

    ということらしい。なるほど勉強になった。

    おまけ

    mViewLifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);

    mViewLifecycleOwner は、Fragment.getViewLifecycleOwner() で取得できる LifecycleOwner だ。

    なるほど、やっぱりFragment.getLifecycle() で取得できるライフサイクルとは別物で、イベントのタイミングが微妙に違うんですねー

    改めて、Fragement でライフサイクルと View を絡めるときはFragment.getViewLifecycleOwner() を使うべき、ということがなんとなくわかった。

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    えばたん(エンジニア)

    えばたん(エンジニア)

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background