2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ViewStubを含むレイアウトは、ViewStubをinflateしたあとにViewBindingへのbindができないので注意しよう

Posted at

タイトル通りの罠にはまったので書きます

通常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することがないように注意しましょう。

以上です。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?