タイトル通りの罠にはまったので書きます
通常ViewBindingはActivityやFragmentの最初にinflateもしくはbindするだけなので問題は起こらないでしょうが、常時必要ではないモジュールにviewだけを渡して内部で再bindしている箇所があり、そこにViewStubを追加したことで問題が発生しました。
ViewStubを含むレイアウトを扱う際に、以下のようにinflate後にViewBindingへbindしようとすると
val binding = HogeBinding.bind(view)
...
binding.viewStub.inflate()
...
HogeBinding.bind(view)
NullPointerExceptionが発生してクラッシュしてしまいます。
Caused by: java.lang.NullPointerException: Missing required view with ID: com.example.myapplication:id/view_stub
ViewStubの仕組みを考えれば当然ではあります、
ViewStubのinflate()
の実装は以下のようになっていて、replaceSelfWithView()
で自分をViewParentから削除し、inflateしたViewに入れ替えます
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;
final View view = inflateViewNoAdd(parent);
replaceSelfWithView(view, parent);
mInflatedViewRef = new WeakReference<>(view);
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}
return view;
} else {
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
private void replaceSelfWithView(View view, ViewGroup parent) {
final int index = parent.indexOfChild(this);
parent.removeViewInLayout(this);
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
if (layoutParams != null) {
parent.addView(view, index, layoutParams);
} else {
parent.addView(view, index);
}
}
ViewBindingはbindしたときにそのレイアウト内に存在するすべてのViewIdを捜し、フィールドに保持します。そのとき、一つでも見つからないViewがあればNullPointerExceptionを発生させます。
inflate()
が実行された時点でこのViewStubがViewTreeから削除されてしまうため、再度bind仕様とすると、ViewStubに該当するViewが見つからず、NullPointerExceptionが発生してしまうのですね。
また、inflateされるレイアウトのRootViewにViewStubと同じIDをつけている場合、同じIDのViewに置き換わりはしますが、ViewStubのインスタンスではなくなっていますので、ClassCastExceptionが発生することになります。
これはViewStubに限ったことではなく、動的にViewを削除するようなことをしている場合、同様の問題が発生します。
ViewBindingはレイアウトのinflate以降、Viewの構造が変化した後にbindすることがないように注意しましょう。
以上です。