fragmentが難しい理由の一つとして、viewの生存期間よりfragmentの生存期間のほうが長い、というところがあると思います。
そのため、以下のようなコードは問題があります。
(data binding / view bindingの登場でそもそもfindViewByIdするコードをあまり見かけなくなりましたが)
// 例1: by lazyを使う
class HogeFragment: Fragment() {
private val button: View by lazy {
requireView().findViewById(R.id.button)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
button.setOnClickListeenr {
...
}
}
}
// 例2: lateinitを使う
class HogeFragment: Fragment() {
private lateinit var button: View
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
button = requireView().findViewById(R.id.button)
button.setOnClickListeenr {
...
}
}
}
例1では、viewが再生成された際に以前のviewがそのまま使われ、新しいviewに正しくllistenerが設定されません。
(例えば、fragmentをdetach後、再度attachするとfragmentはそのままでviewが再生成される)
例2では、一見正しく動くように見えますが、onDestroyで破棄されたviewへの参照を持ち続けるので、メモリリークが発生することになります。
したがって、以下のようなコードが正しく動きます。
// 例3: onDestroyViewで参照を切る
class HogeFragment: Fragment() {
private var button: View? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
button = requireView().findViewById(R.id.button)
button?.setOnClickListeenr {
...
}
}
override fun onDestroyView() {
button = null
}
}
data binding / view binding / recycler viewのadapter等も本来このような形で書く必要があります。
しかし、このコードを毎回書くのは、以下の理由で大変です。
これらの解決策としてgoogleのAAC sampleではAutoClearedValueというもの使っています。
以下のように使うことができます。
// 例4: AutoClearedValue
class HogeFragment: Fragment() {
private var button: View by autoCleared()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
button = requireView().findViewById(R.id.button)
button.setOnClickListeenr {
...
}
}
}
これでbuttonをnonnullで扱え、なおかつonDestroyViewでnullを入れなくても勝手にnullになってくれます。
しかし、この方法でも以下のような不満を感じました。
そこで、by lazyのように扱えるReadOnlyAutoClearedValueを作ってみます。
早速ですが実際のコードです
class ReadOnlyAutoClearedValue<T : Any>(
private val fragment: Fragment,
private val initializer: () -> T
) : ReadOnlyProperty<Fragment, T> {
private var value: T? = null
private val lifecycleObserver = object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
value = null
}
}
init {
fragment.viewLifecycleOwnerLiveData.observe(fragment) {
it.lifecycle.addObserver(lifecycleObserver)
}
}
override fun getValue(thisRef: Fragment, property: KProperty<*>): T {
val isInitialized = getCurrentViewState()?.isAtLeast(Lifecycle.State.INITIALIZED) ?: false
if (!isInitialized) {
throw IllegalStateException(
"Can't access the ReadOnlyAutoClearedValue when getView() is null i.e., before onCreateView() or after onDestroyView()"
)
}
return getOrCreateValue()
}
private fun getCurrentViewState(): Lifecycle.State? {
return try {
fragment.viewLifecycleOwner.lifecycle.currentState
} catch (e: IllegalStateException) {
null
}
}
private fun getOrCreateValue(): T {
return value ?: initializer().also {
value = it
}
}
}
fun <T : Any> Fragment.autoCleared(initializer: () -> T) =
ReadOnlyAutoClearedValue(this, initializer)
ちょっと長いですが、以下のようになっています。
このように使います。
// 例4: ReadOnlyAutoClearedValue
class HogeFragment: Fragment() {
private val button: View by autoCleared {
requireView().findViewById(R.id.button)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
...
button.setOnClickListeenr {
...
}
}
}
かなりシンプルになったのではないかと思います。
今回はわかりやすいようにfindViewByIdで表記しましたが、data bindning / view bindingの登場でこのようなケースは減っていると思います。
しかし、recycler viewのadapterやepoxyのcontroller等はこのような形でメンバで保持するのがいいのかもしれません。
fragmentは複雑で色々なハマリポイントがあるので、注意深く扱ってあげる必要がありそうです。