先日FragmentのViewにlateinitをつけてはいけないのでは?
でViewはnullableにして実行時にnullチェックをすべきでは?という話をしたのですが、
こういう意見が寄せられました。
Android よく知らないですが、null の時にアクセスされることがそもそも間違いであれば、間違った実装をしていること (想定してないフローに入っていること) がまずいのではと思いました。そのパターンが蔓延してしまうのではという気もします。
— みつせ (@mitsuse_t) December 22, 2018
たしかに、
Kotlinでnullについてもう一度考える
のときにも感じたNullにあまり説明性がない問題ですね
lateinitを使っていればViewがnullのときに処理が呼び出されると例外が発生してアプリが落ちますがNullチェックをいれてnon null時のみ処理をするようにしておくと単に処理が行われないだけで処理が継続されます。
ViewがNullのときに処理が呼び出される状態自体が想定外であれば下手に処理が継続するより例外で落ちてもらったほうが良いかもしれません。
一方でlateinitにすることでNullチェックを行わせず、例外を発生させる使い方はKotlin的には想定している使い方ではないと思いますし、非同期処理の結果を表示するときのようにViewがいないなら単に処理を無視して構わない時もあります。
ということで、Viewが存在していれば処理を行い、Viewが存在していなければログを出力するクラスを作ってみました。
ここではLogCatでログを出力していますがCrashlyticsなりFirebaseでエラーを通知するのが良いかと思います。
ライフサイクルがStopになるタイミングでViewにNullを明示的にセットすることで破棄されたViewにまちがえて保存するのを防ぐことが出来ます。
class ViewHolder<T>(lifecycle: Lifecycle) {
init {
lifecycle.addObserver(object : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
fun onStop() {
obj = null
}
})
}
var obj: T? = null
fun able(func: (T) -> Unit) {
obj?.let {
func.invoke(it) }
}
fun must(func: (T) -> Unit) {
obj?.also { func.invoke(it) }
?: run {
Log.e("ViewHolder", "Destroy")
}
}
}
使い方は次の通り
Fragmentの初期化時にViewHolderのインスタンスを初期化します。
このインスタンス自体はライフサイクルに関わらず存在するためnon nullにすることが出来ます。
型パラメータでライフサイクル内で保存したい型(DataBindingのViewDataBindingなど)を渡します。
引数としてlifecycleを渡します。
private val viewHolder = ViewHolder<MainFragmentBinding>(lifecycle)
onCreateViewで保存したいViewやBindingをviewHolderのobjにセットします。
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding: MainFragmentBinding = DataBindingUtil.inflate(inflater, R.layout.main_fragment, container, false)
viewHolder.obj = binding
// 中略
return binding.root
}
bindingを取得するときは次の通り
ViewHolder#mustを呼ぶと、Viewがあれば処理が行われ、Viewがなければエラー処理を実行します。
viewHolder.must{
// itにBindingがnon nullで渡されます。
}
ViewHolder#ableを呼ぶと、Viewがあれば処理が行われ、Viewがなければ処理を行いません。
ちょっと雑な感じがしないでもないですがどうでしょうかね