LoginSignup
56
40

More than 3 years have passed since last update.

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

Posted at

お題

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

完成イメージ

未選択時
時刻を選択
選択後

StatefulWidgetsetStateを使う

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パターンの解説

StreamBuilderを使って書き換えよう

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を通して流れ込み、StreamBuildersnapShot.dataに流れ着きます。

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

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

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

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

56
40
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
56
40