見出し画像

[ViewLifecycleProperty] Fragment でプロパティを Not Null で省メモリで onDestroyView でアクセスしても安全な AutoClearedValue にする方法

Fragment でプロパティの生存期間を View の生存期間と一致させる方法はいくつかあります。しかし、満たしたい要件に応じて適切な方法を選択するためには Fragment のことを良く知っている必要があります。

ここでは、そのようなプロパティが満たしたい要件と定義する方法を整理し、すべての要件を満たす ViewLifecycleProperty を紹介します。

TL;DR

それぞれの方法の比較表は以下の通りです。
画像2

ViewLifecycleProperty がすべての要件を満たしていてオススメです。

# プロパティが満たしたい要件

  ## 再初期化: View のライフサイクルに応じてプロパティを再初期化できるか

Fragment のライフサイクルは View のライフサイクルより長いです。Fragment のライフサイクル全体で immutable なプロパティではなく、View のライフサイクル全体で immutable なプロパティとして定義したいため、View の再生成に合わせてプロパティを再初期化できる必要があります。

  ## Not Null: プロパティを Not Null で宣言できるか

プロパティを Nullable で宣言すると、毎回 Null のハンドリングが必要になってしまうため、可能な限り Not Null で宣言したいです。

  ## 省メモリ: onDestroyView 以降にプロパティを null クリアできるか

プロパティのライフサイクルは View と一致しているため、onDestroyView 以降はプロパティを使用しません。使用しないプロパティは null でクリアしたほうが(わずかでしょうが)省メモリです。

Backstack や Navigation を使用している場合、null クリアせずに画面遷移を重ねると、それまでの Fragment は生存しているので保持しているプロパティの分だけメモリを消費します。チリツモでメモリ消費が大きくなり OOM が発生するかもしれません。

  ## onDestroyView: onDestroyView でプロパティにアクセスできるか

MediaPlayer など onDestroyView で何らかの処理を行いたい場合があるため、onDestroyView でアクセス可能である必要があります。

# プロパティを定義する方法

  ## val by lazy で宣言する

以下のように宣言します。

val property by lazy { SampleProperty() }

  ## var で null 許容型で宣言する

以下のように宣言し、onViewCreated などで代入します。

val property: SampleProperty? = null

  ## lateinit var で宣言する

以下のように宣言し、onViewCreated などで代入します。

lateinit val property: SampleProperty

  ## AutoClearedValue を使う

android/architecture-components-samples の AutoClearedValue を使います。以下のように宣言し、onViewCreated などで代入します。

var property: SampleProperty by autoCleared()

  ## ViewLifecycleProperty を使う

wada811/ViewLifecycleProperty を使います。以下のように宣言します。

val property: SampleProperty by viewLifecycle { SampleProperty() }

あるいは、以下のように宣言し、onViewCreated などで代入します。

var property: SampleProperty by viewLifecycle()

# 各方法の比較

プロパティの宣言方法の比較は以下の通りです。

画像2

・val by lazy で宣言する方法では再初期化することができません。
・var で null 許容型で宣言する方法では Not Null にすることができません。
・lateinit var で宣言する方法では省メモリではありませんがお手軽です。
・AutoClearedValue を使う方法では onDestroyView でプロパティにアクセスすると IllegalStateException が発生します。
・ViewLifecycleProperty を使う方法はすべての要件を満たしています。

結論: ViewLifecycleProperty がすべての要件を満たしていてオススメです。

# ViewLifecycleProperty とは

ViewLifecycleProperty は AutoClearedValue を改良し、上記の要件をすべて満たすように作ったライブラリです。

実際のソースコードは50行程度のマイクロライブラリです。

/*
 * Copyright (C) 2020 wada811
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.wada811.viewlifecycleproperty

import androidx.fragment.app.Fragment
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.Observer
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

/**
 * This method is extension method returning delegated property `ViewLifecycleProperty`.
 *
 * ViewLifecycleProperty is
 *  - lazy initialization
 *  - accessible when Fragment's view isn't null
 *  - automatically cleared by null when Fragment's view released
 */
fun <T> Fragment.viewLifecycle(initialize: (() -> T)? = null) = ViewLifecycleProperty(this, initialize)

class ViewLifecycleProperty<T>(private val fragment: Fragment, private val initialize: (() -> T)?) : ReadWriteProperty<Fragment, T> {
    private var value: T? = null

    init {
        // viewLifecycleOwnerLiveData set value after onCreateView
        fragment.viewLifecycleOwnerLiveData.observe(fragment, object : Observer<LifecycleOwner> {
            override fun onChanged(viewLifecycleOwner: LifecycleOwner?) {
                // onChanged called when STARTED
                fragment.viewLifecycleOwnerLiveData.removeObserver(this)
                fragment.viewLifecycleOwnerLiveData.observeForever(object : Observer<LifecycleOwner> {
                    override fun onChanged(owner: LifecycleOwner?) {
                        if (owner == null) {
                            // after onDestroyView, viewLifecycleOwnerLiveData set null in FragmentManagerImpl
                            fragment.viewLifecycleOwnerLiveData.removeObserver(this)
                            value = null
                        }
                    }
                })
            }
        })
    }

    override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
        checkNotNull(fragment.viewLifecycleOwnerLiveData.value) {
            "Can't access the Fragment[${thisRef.tag}]'s property[${property.name}] when getView() is null i.e., before onCreateView() or after onDestroyView()"
        }
        return value ?: initialize?.invoke()?.also { value = it } ?: throw IllegalStateException("Fragment[${thisRef.tag}]'s property[${property.name}] is not initialized.")
    }

    override fun setValue(thisRef: Fragment, property: KProperty<*>, value: T) {
        this.value = value
    }
}

AutoClearedValue との違いは、init でのライフサイクルの監視方法です。

AutoClearedValue のように単純に Fragment の View の Lifecycle を監視すると ON_DESTROY は onDestroyView の直前でイベントが発生し、onDestroyView でアクセスすると IllegalStateException が発生します。

そのため、ViewLifecycleProperty では、onDestroyView でもアクセスできるように onDestroyView の直後に viewLifecycleOwnerLiveData の値が null になることを監視して value に null をセットしています。

AutoClearedValue では onDestroyView でプロパティにアクセスできなくて困っている方はご検討ください。



この記事が気に入ったらサポートをしてみませんか?