SingleChildScrollViewの中で上下中央揃えしてPull to Refreshもする

# やりたいこと

こういうことがしたい。

![](https://github.com/kikuchy/centering-in-singlechildscrollview/raw/master/art/sample.gif)

ちゃんと文字に起こすと、要件はこんな感じ。

* リストの空表示をしたい
    * Pull to Refreshしたいので、[RefreshIndicator](https://docs.flutter.io/flutter/material/RefreshIndicator-class.html)で全体をくくって、中をスクロール可能にしたい
    * [SingleChildScrollView](https://docs.flutter.io/flutter/widgets/SingleChildScrollView-class.html)の中に自作Widgetを中央表示したい


`RefreshIndicator` の中身を `SingleChildScrollView` で包んでやれば良いのはわかるのだけれども、なかなか一筋縄では行きません。


# 先に解決策

[LayoutBuilder](https://docs.flutter.io/flutter/widgets/LayoutBuilder-class.html)と[ConstrainedBox](https://docs.flutter.io/flutter/widgets/ConstrainedBox-class.html)、[AlwaysScrollableScrollPhysics](https://docs.flutter.io/flutter/widgets/AlwaysScrollableScrollPhysics-class.html)を使用します。

```dart
RefreshIndicator(
  child: LayoutBuilder(
    builder: (context, constraints) => SingleChildScrollView(
      physics: AlwaysScrollableScrollPhysics(),
      child: ConstrainedBox(
        constraints: BoxConstraints(minHeight: constraints.maxHeight),
        child: Center(child: EmptyView()),
))))
```

これでSingleChildScrollViewの中身を表示域と同じサイズにできるので、目的のWidgetを上下中央に表示できます。

[サンプルのソースコードはこちら。](https://github.com/kikuchy/centering-in-singlechildscrollview)


# 解決すべき問題点と解決策

## 表示域の高さ不定問題

通常であれば、Widgetを表示域の中央に持ってきたければ `Center` を使用します。

```dart
Center(
  child: EmptyView()
)
```

これをスクロールできればいいので、 `SingleChildScrollView` で括ってやりましょう。以上！なんて簡単なんだ！！

```dart
SingleChildScrollView(
  child: Center(
    child: EmptyView()
  )
)
```

↓

![Simulator Screen Shot - iPhone XR - 2019-04-09 at 19.27.49.png](https://qiita-image-store.s3.amazonaws.com/0/41896/e37966b5-5a61-d690-be0d-f770c55adfa5.png)

:thinking: 

`SingleChildScrollView` の子Widgetは高さの最大値が決まらない（いくらでも縦に伸ばせてしまうため）、 `Center` は表示域（この場合はScaffoldのbody部分）いっぱいに広がってくれないようです。

これを解決するには[LayoutBuilder](https://docs.flutter.io/flutter/widgets/LayoutBuilder-class.html)と[ConstrainedBox](https://docs.flutter.io/flutter/widgets/ConstrainedBox-class.html)を使います。
[SingleChildScrollViewのDocに書いてありますね！！](https://docs.flutter.io/flutter/widgets/SingleChildScrollView-class.html)

`LayoutBuilder` は、FlutterがWidgetツリーを構築するbuildメソッドが呼ばれたときまで、子Widgetツリーの生成を遅延し、その時点での表示域の制約を取得してくれます。
ここでは、 `Center` を表示域の高さまで広げられればよいので、この制約の情報が欲しいわけです。

`ConstrainedBox` はサイズに制約をもたせた `Container` のようなものです。
ここでは、`Center` の親Widgetの高さは表示域の高さと等しいかそれより大きくなってもらう必要があるので、 `minHeight` の制約を使用します。

```dart
LayoutBuilder(
  builder: (context, constraints) => SingleChildScrollView(
    child: ConstrainedBox(
      constraints: BoxConstraints(minHeight: constraints.maxHeight),
      child: Center(child: EmptyView()),
    ),
))
```

↓

![Simulator Screen Shot - iPhone XR - 2019-04-09 at 19.41.04.png](https://qiita-image-store.s3.amazonaws.com/0/41896/8a7363ae-47f1-5c6e-9992-aae9240ed32f.png)

ちゃんと中央表示できました！！やったね！！ :tada: 


しかし、触ってみるとわかりますが、このLayoutBuilderの親に `RefreshIndicator` を置いてもPull to Refreshできません。なぜだ。

## Pull to Refreshできない問題

```dart
RefreshIndicator(
  child: LayoutBuilder(
    builder: (context, constraints) => SingleChildScrollView(
      child: ConstrainedBox(
        constraints: BoxConstraints(minHeight: constraints.maxHeight),
        child: Center(child: EmptyView()),
))))
```

今のままでは、 `SingleChildScrollView` 内要素が `SingleChildScrollView` と同じ高さになっているため、スクロールが行われません。

スクロール可能にするならば、内要素が `SingleChildScrollView` より長くなっている必要があります。
が、長くしてしまうとせっかく `Center` で表示域中央に持ってきたのが、中央からずれてしまいます。

そこで、 `SingleChildScrollView` 内要素が `SingleChildScrollView` より短くてもスクロール可能にする方法を取ります。

`physics:` に [AlwaysScrollableScrollPhysics](https://docs.flutter.io/flutter/widgets/AlwaysScrollableScrollPhysics-class.html) を指定する方法です。
`AlwaysScrollableScrollPhysics` はスクロール可能なWidgetを、いつでもスクロールさせるものです。

これで `RefreshIndicator` をトリガーさせることができました！ :congratulations: 

# まとめ

* `LayoutBuilder` で表示域の制約を取得できる
* `ConstrainedBox` で子Widgetのサイズに制約をかけることができる
* Scroll系のWidgetをいつでもスクロールさせたいときは `AlwaysScrollableScrollPhysics` を使う
* 公式のドキュメントはよく読む