Edited at
AndroidDay 20

FragmentのViewにlateinitをつけてはいけないのでは?

この投稿はAndroid Advent Calendar 2018 20日めの代理です。


言いたいこと

FragmentのViewは動的に生成されて動的に破棄されるからNullable


経緯

AndroidとKotlinで開発していると FragmentのBindingやfindViewById()したViewをどうやってプロパティに保持するか困ってしまいます。

Viewの生成はonCreateView()で行われます。

そのため、クラスインスタンスの初期化時に初期化が必要なvalを使うことが出来ません。

Lazyを使ってはいけないというのはこちらのエントリにあるとおりで、lateinitを使うのがデファクトになりつつある気がします。

FragmentでKotlinのby lazyを使ってfindViewByIdするとレイアウト反映できない&リークする件

私もlateinitを使っていました。

これをどうにかカスタムLazyを使って再現できないかと考えていた時、突然、何だかやってはいけないことをやっている気持ちになったのです。


なぜか?

FragmentのViewはNullableだからです。

本来lateinitはDIのようにインスタンス初期化時には初期化されていないがその直後使用前には必ず初期化されることが保証されているものにたいして例外的に使用されることを想定した仕様ではないでしょうか?

FragmentのViewはその仕様にそぐわない気がします。

fragment#getView() にも明確に @Nullable と宣言されています。

onCreateView() が終了して onDestoryView() の間だけViewが存在し、その外ではNullになる可能性があります。

カスタムLazyを作っている時も、view#findViewById()を呼びたいが、当のviewがnullableである問題が発生し !! でviewのNull可能性を無視すればnon nullな値を返すことはできるけれど、うっかりViewが生成されていないタイミングで呼び出すと NullPointerException が発生します。

たしかに、 lateinit を使ってもライフサイクルを意識してコーディングを行えば、viewがnullのときにアクセスしてしまうことはないかもしれません。

でも、それって要するにNullableをプログラマが上手に回避しているだけですよね

せっかくのKotlinが作ったNull安全なしくみをあえて崩しているように思います。

そこまでして、Nullチェックをしないメリットも見当たりません。


ではどうするか?

素直にNullableを使って、アクセス前はNullチェックをするというのはどうでしょう?


DataBindingを使っている場合

もし、DataBindingを使っているなら、onCreateViewで作ったBindingをonDestoryViewで破棄すればBindingのNullチェックを行うことでView全体のNullチェックが可能です。

onCreateView()でViewの初期化とBindingプロパティに値をセットする。

private var binding: FragmentFooBinding? = null

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// 中略
return FragmentFooBinding.inflate(LayoutInflater.from(activity)).also{binding ->
this.binding = binding
// Viewの初期化処理

}.root
}

onDestoryで破棄

override fun onDestory(){

super.onDestory()
binding = null

Binding内の各Viewはnon nullなので 1 Bindingをnullチェックすれば一つ一つのViewの存在チェックは不要です。

binding?.let{binding ->

binding.textView.text = "foo"
}


DataBindingを使っていない場合

DataBindingを使っていないならViewを保持するViewHolderクラスをつくるのが便利かと思います。

class ViewHolder(val textView:TextView,val imageView:ImageView)

onCreateView()でViewHolderに値をセットする。


private var viewHolder:ViewHolder? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.main_fragment, container, false)
viewHolder = ViewHolder(textView = view.findViewById(R.id.textView), imageView = view.findViewById(R.id.imageView))
..

onDestoryで破棄

override fun onDestory(){

super.onDestory()
viewHodler = null

これなら、うっかりライフサイクルの外でViewを呼んでしまっても安心です。


追記

FragmentのViewをさらに考察してみる。





  1. 実はData Bindingにはにくい機能があって、画面サイズや向きによって存在しないViewがあった場合はしっかりnullableにしてくれます。