Snackbar使用時に ScrollView can host only one direct child とエラーが起きる場合の対処

## 問題概要
この記事にたどり着いた人はScrollViewにaddViewした覚えがないのに
`ScrollView can host only one direct child ` のエラーに悩まされている人かと思います。

その場合、
Snackbarの[make](https://developer.android.com/reference/android/support/design/widget/Snackbar.html#make(android.view.View,%20java.lang.CharSequence,%20int))の第一引数に渡しているviewが、ScrollViewになっていないか確認してください。

ScrollViewになっていれば、原因はそれの可能性が高いです。

## 原因

MateralComponentの1.1.0-alpha-06までのSnackbarの実装を見てみると、

```java
 @NonNull
  public static Snackbar make(
      @NonNull View view, @NonNull CharSequence text, @Duration int duration) {
    final ViewGroup parent = findSuitableParent(view);
    if (parent == null) {
      throw new IllegalArgumentException(
          "No suitable parent found from the given view. Please provide a valid view.");
    }

    final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
    final SnackbarContentLayout content =
        (SnackbarContentLayout)
            inflater.inflate(
                hasSnackbarButtonStyleAttr(parent.getContext())
                    ? R.layout.mtrl_layout_snackbar_include
                    : R.layout.design_layout_snackbar_include,
                parent,
                false);
    final Snackbar snackbar = new Snackbar(parent, content, content);
    snackbar.setText(text);
    snackbar.setDuration(duration);
    return snackbar;
  }
```
makeの中で、ViewGroupを取得する際に、findSuitableParentを使用していることがわかります。

```java
final ViewGroup parent = findSuitableParent(view);
```
では、さらに見ていくと、

```java
private static ViewGroup findSuitableParent(View view) {
    ViewGroup fallback = null;
    do {
      if (view instanceof CoordinatorLayout) {
        // We've found a CoordinatorLayout, use it
        return (ViewGroup) view;
      } else if (view instanceof FrameLayout) {
        if (view.getId() == android.R.id.content) {
          // If we've hit the decor content view, then we didn't find a CoL in the
          // hierarchy, so use it.
          return (ViewGroup) view;
        } else {
          // It's not the content view but we'll use it as our fallback
          fallback = (ViewGroup) view;
        }
      }

      if (view != null) {
        // Else, we will loop and crawl up the view hierarchy and try to find a parent
        final ViewParent parent = view.getParent();
        view = parent instanceof View ? (View) parent : null;
      }
    } while (view != null);

    // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
    return fallback;
  }

```
まずCoordinatorLayoutであるか見て、その次にFrameLayoutであるかどうかを見ています。
その場合の細かい話は今回置いておいて、違った場合を見てみると、parent viewが見つかるまでview.getParent()しています。
最終的に、parent viewがScrollViewだった場合、ScrollViewがparent viewとして返ります。
そして、

```java
final Snackbar snackbar = new Snackbar(parent, content, content);
```
そのparentを自身のコンストラクタに渡しています。

```java
  private Snackbar(
      ViewGroup parent,
      View content,
      com.google.android.material.snackbar.ContentViewCallback contentViewCallback) {
    super(parent, content, contentViewCallback);
    accessibilityManager =
        (AccessibilityManager) parent.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
  }
```

次に、Snackbarの親であるBaseTransientBottomBarを見てみます。

最終的に実行されるshowView()を見てみると、

```java
  final void showView() {
    if (this.view.getParent() == null) {
      final ViewGroup.LayoutParams lp = this.view.getLayoutParams();

      if (lp instanceof CoordinatorLayout.LayoutParams) {
        // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
        final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;

        final SwipeDismissBehavior<? extends View> behavior =
            this.behavior == null ? getNewBehavior() : this.behavior;

        if (behavior instanceof BaseTransientBottomBar.Behavior) {
          ((BaseTransientBottomBar.Behavior) behavior).setBaseTransientBottomBar(this);
        }
        behavior.setListener(
            new SwipeDismissBehavior.OnDismissListener() {
              @Override
              public void onDismiss(View view) {
                view.setVisibility(View.GONE);
                dispatchDismiss(BaseCallback.DISMISS_EVENT_SWIPE);
              }

              @Override
              public void onDragStateChanged(int state) {
                switch (state) {
                  case SwipeDismissBehavior.STATE_DRAGGING:
                  case SwipeDismissBehavior.STATE_SETTLING:
                    // If the view is being dragged or settling, pause the timeout
                    SnackbarManager.getInstance().pauseTimeout(managerCallback);
                    break;
                  case SwipeDismissBehavior.STATE_IDLE:
                    // If the view has been released and is idle, restore the timeout
                    SnackbarManager.getInstance().restoreTimeoutIfPaused(managerCallback);
                    break;
                  default:
                    // Any other state is ignored
                }
              }
            });
        clp.setBehavior(behavior);
        // Also set the inset edge so that views can dodge the bar correctly, but only if there is
        // no anchor view.
        if (anchorView == null) {
          clp.insetEdge = Gravity.BOTTOM;
        }
      }

      extraBottomMarginAnchorView = calculateBottomMarginForAnchorView();
      updateBottomMargin();

      targetParent.addView(this.view);
    }
```

最後の行の

```java
targetParent.addView(this.view);
```

でaddViewしており、このtargetParentがSnakbarのコンストラクタで親に渡していたparentです。
ここがScrollViewだった場合、エラーが発生してランタイムで落ちてしまいます。

## DataBindingを使っている場合の落とし穴

ちなみに、rootがScrollViewになってしまう原因として、
findViewByIdで
`android.R.id.content` を指定するとxml上で見えるrootのさらに上の階層のviewを取得できますが、（viewの階層構造に関しては割愛。DecorViewとかContentFrameLayoutとかだいぶ深い階層になっています）
DataBindingを使っている場合、binding.rootで取得したrootはxml上のrootです。

```java
/**
  * Returns the outermost View in the layout file associated with the Binding. If this
  * binding is for a merge layout file, this will return the first root in the merge tag.
  *
  * @return the outermost View in the layout file associated with the Binding.
  */
  @NonNull
  public View getRoot() {
      return mRoot;
  }
```
> the outermost View in the layout file associated with the Binding.

ということなので。
bindingしているViewのみを扱うことを考えたら、当然といえば当然かもしれません。
なので、 rootがScrollViewなLayoutにて、`binding.root` を渡すとクラッシュします。

## 対処法
対処法としては、
https://stackoverflow.com/questions/4486034/get-root-view-from-current-activity
にあるように、 `android.R.id.content` を指定して、ScrollViewではないrootが渡るように保証してあげるなどが考えられます。
例えば、

```kotlin
fun View.showError(
    @StringRes errorMessageResId: Int = R.string.error
) {
    Snackbar.make(findViewById(android.R.id.content), errorMessageResId, Snackbar.LENGTH_LONG)
            .show()
}
```
のように、Viewのextensionにして共通で使うなど。
ただし、rootをCoordinatorLayoutにしたい場合は別途対応が必要です。