FlutterのStreamBuilderをつかって無駄な再描画を減らそう

# お題
ポップアップで選択して、選択結果を表示する画面を考えます。

## 完成イメージ
[未選択時](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168736/6dce197b-6241-6713-25fb-9ad528509af4.png)
[時刻を選択](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168736/69489834-f1f7-9fd3-1703-c11dada0971b.png)
[選択後](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168736/97a5388e-65c8-941d-99d0-11d977a5a5c0.png)

## `StatefulWidget`と`setState`を使う
```dart
class HomeWidget extends StatefulWidget {
  @override
  HomeState createState() => HomeState();
}
class HomeState extends State<HomeWidget> {
  TimeOfDay _time;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Container(
          decoration: BoxDecoration(
            border: Border(
              bottom: BorderSide(width: 3),
            ),
          ),
          child: Text(
            _time == null ? "未選択" : _time.format(context),
            style: TextStyle(fontSize: 50),
          ),
        ),
        RaisedButton(
          child: Text(
            "時刻を選択する",
            style: TextStyle(fontSize: 20),
          ),
          color: Colors.deepOrangeAccent,
          onPressed: () async {
            final select = await showTimePicker(
              context: context,
              initialTime: TimeOfDay.now(),
            );
            this.setState(() => _time = select);
          },
        ),
      ],
    );
  }
}
```
この例だと、ボタンを押して時刻を選択すると、テキストだけではなく、ボタンも再描画されています。

## `StreamBuilder`を使う

### Stream？
Streamのイメージは、蛇口だ。
`sink`にデータを流すと、`stream`に流れるので、`listen`で捕まえて処理するのだ。

[長めだけどたぶんわかりやすいBLoCパターンの解説](https://qiita.com/kabochapo/items/8738223894fb74f952d3#%E3%81%BE%E3%81%9Astream%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6)

### StreamBuilderを使って書き換えよう
```dart
class HomeWidget extends StatefulWidget {
  @override
  HomeState createState() => HomeState();
}

class HomeState extends State<HomeWidget> {
  final _onTimeChange = StreamController<TimeOfDay>();

  @override
  void dispose() {
    // StreamControllerは必ず開放する
    _onTimeChange.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        // 2つのWidgetは親のもつStreamControllerのstreamとsinkを使ってデータの受け渡しを行う
        TimeText(stream: _onTimeChange.stream),
        TimeSelector(sink: _onTimeChange.sink),
      ],
    );
  }
}

/// 時刻を受け取って表示する
class TimeText extends StatelessWidget {
  /// 受け口
  final Stream<TimeOfDay> stream;
  TimeText({this.stream});

  @override
  Widget build(BuildContext context) {
    return Container(
      decoration: BoxDecoration(
        border: Border(
          bottom: BorderSide(width: 3),
        ),
      ),
      child: StreamBuilder(
        // 指定したstreamにデータが流れてくると再描画される
        stream: this.stream,
        builder: (BuildContext context, AsyncSnapshot<TimeOfDay> snapShot) {
          // StreamControllerから流れてきたデータを使って再描画
          return Text(
            snapShot.hasData ? snapShot.data.format(context) : "未選択",
            style: TextStyle(fontSize: 50),
          );
        },
      ),
    );
  }
}

/// 時刻を選択して、選択結果を渡す
class TimeSelector extends StatelessWidget {
  /// 渡し口
  final StreamSink<TimeOfDay> sink;
  TimeSelector({this.sink});

  @override
  Widget build(BuildContext context) {
    return RaisedButton(
      child: const Text(
        "時刻を選択する",
        style: TextStyle(fontSize: 20),
      ),
      color: Colors.deepOrangeAccent,
      onPressed: () async {
        final select = await showTimePicker(
          context: context,
          initialTime: TimeOfDay.now(),
        );
        // sinkに選択した時刻を流す
        this.sink.add(select);
      },
    );
  }
}
```
StreamBuilderは指定した`stream`にデータが流れてくると、自動で再描画が実行されます。

今回の例だと、時刻を選択すると、選択したデータ`<TimeOfDay>`が`sink`を通して流れ込み、`StreamBuilder`の`snapShot.data`に流れ着きます。

ボタンやその他のWidgetを再描画することなく、テキストだけを再描画することができます。

また、ボタンからテキストへは`StreamController`を通してデータの受け渡しがされているため、
`StreamController`さえ両方のWidgetに渡してしまえば、うまくデータのやり取りが可能です。

こうすることで、無駄な再描画を抑えつつ、状態管理の煩雑さから開放され、各Widgetをうまくパーツ化、再利用することができるようになります。

みんなもStreamBuilderをうまく使って快適なFlutterライフを！！！！