【Flutter】 FutureとStream の違いって何?

FutureとStream 初心者向け

FutureとStream 、よく出てくるけど、一体何なの?

何が違うのかわからないわ!

Flutterでコードを書いていて出てくるFutureStream
どちらも非同期処理でよく使われるものとなっていますが、難しくてよくわからないですよね。

本記事ではFutureStreamそれぞれについて解説し、
違いについてまとめます。

Flutter初心者必見の内容となっています。
ぜひ読んでみてください!

Futureとは

Future ってよく出てくるけど、一体何なんだろう?

Futureとは、「非同期処理の返り値を表すクラス」です。

例えば、以下のような処理を考えてみましょう。

  1. コンソールに「明日の天気は」を出力する
  2. ウェブページからデータを取得する
  3. コンソールに「晴れです」を出力する

コンソールに「明日の天気は晴れです」と表示したい処理と、
全く関係なくウェブページからデータを取得する処理を同時に行なっている例です。

ウェブページからデータを取得する処理は時間がかかります。
また、Flutter/Dartではプログラムは上から順番に実行されます。
普通は途中で順序が変わったりしません。
そのため、上の処理を行うと、「明日の天気は」の出力と、「晴れです」の出力の間に、
ウェブページからデータを取得している時間分、待ち時間が発生します。

これは、あまりユーザーフレンドリーではありませんよね。
理想としては、ウェブページからデータを取得している間に、
「晴れです」を表示させて、
「明日の天気は」と「晴れです」の待ち時間を無くしたいです。

ここで登場するのがFutureです。
Futureを返り値に保つ関数は、非同期処理となり、その関数が実行している間に、
他の処理を実行することが可能となります。

今回の例で言うと、実行に時間のかかるウェブページからデータを取得する処理を
Futureを返り値にして非同期処理にすることで、
ウェブページからデータを取得している間に「晴れです」を表示させることができます。

Futureを使う意味、わかりましたでしょうか?

一般的に処理に時間がかかる関数はFutureを返り値に持っています。
今回例に出したウェブページからデータを取得する処理も、
普通はFutureを返り値に持ちます。

Futureクラスは正式には、Future<T>と書かれます。
Tの部分に元々の返り値のクラスを設定することができます。
例: Future<int>

厳密には、Flutter/Dartでは、処理の同時実行はできません。
実際の処理は実行に時間のかかる処理(Future関数)の処理順を最後にする、
実行順の入れ替えを行なっています。

Futureクラスは、 event queue の末尾にアイテムを登録します。

The Event Loop and Dart (翻訳)

今回の例で言うと、

  1. コンソールに「明日の天気は」を出力する
  2. コンソールに「晴れです」を出力する
  3. ウェブページからデータを取得する

の順に実行順を入れ替えています。

コンソールに表示する処理は同期処理で実行に時間がかからないため、
あたかもウェブページからデータを取得する処理と、「晴れです」を出力する処理が
同時に実行しているように見えるわけです。

同期処理、非同期処理についてはこちらも併せてご覧ください。

async / await について

Futureと切っても切り離せない関係にあるのが、async / awaitです。

以下の処理をまとめた関数を考えましょう。

  1. コンソールに「明日の天気は」を出力する
  2. ウェブページから天気データを取得する (Future関数)
  3. コンソールに「”天気データ”です」を出力する

今回は前の例と違い、ウェブページから取得したデータを利用して、
コンソールに天気を出力しています。

この場合実行順序はどうなるでしょうか?
答えは、次のようになります。

  1. コンソールに「明日の天気は」を出力する
  2. コンソールに「”天気データ”です」を出力する
  3. ウェブページから天気データを取得する (Future関数)

ウェブページから天気データを取得する関数はFuture関数なので、実行順序が変わり、
天気データがないのに天気データを表示する、という矛盾した状態となってしまいます。

一体どうすれば良いでしょうか?

ウェブページから天気データを取得する処理を、
Future関数じゃなくするのが手っ取り早い気がしますが、
今回の場合、既にパッケージ等で定義済の関数のためそれはできないとします。

ここで登場するのがawaitです。

awaitを関数の前につけると、実行順序の変更が行われず、
付けた関数の結果を待って次の関数の処理が実行されます。

なので、今回の場合、awaitをウェブページから天気データを取得する関数の前につければ、
実行順序を変更することなく、以下の順序で実行可能です。

  1. コンソールに「明日の天気は」を出力する
  2. ウェブページから天気データを取得する (Future関数)
  3. コンソールに「”天気データ”です」を出力する

ここで、awaitを使う時の注意が2点あります。

  • 全体の関数をFuture関数にすること
  • 全体の関数にasyncをつけること

上のFutureについては、『awaitを使う = 処理に時間がかかる』
なので理解していただけるかと思います。

下のasyncのつける位置は『 { 』の前となります。
これはawaitを使う時の決まりのようなものです。
忘れずに付けましょう。

今回の例をコードにすると以下のようになります。
(あくまで例であり、実際に動作するコードでないことにご留意ください。)

Future<void> showWeather()  async{
  
  //コンソールに「明日の天気は」を出力する
  print('明日の天気は');

  //ウェブページから天気データを取得する 
  String weather = await getWeather();

  //コンソールに「"天気データ"です」を出力する
  print('$weather です');
}

async / await についてはこちらも併せてご覧ください。

FutureBuilder

Future関数の結果を使ってUIを作成するにはどうしたらいいの?

Widgetを組み上げるbuild関数は Future関数でないため、
この中でasync / awaitを使うことはできません。

ここで登場するのがFutureBuilder Widgetです。
FutureBuilder Widgetを使えば、Future関数の結果を使ってUIを組み上げることが可能です。

以下のサンプルコードを使って、使い方を解説していきます。

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({
    super.key,
  });

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Future<String> data;

  //5秒待って、文字列を返す関数
  Future<String> getData() async {
    //1
    return Future<String>.delayed(
      const Duration(seconds: 5),
      () => 'データがありました',
    );
  }

  @override
  void initState() {
    //2
    data = getData();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('FutureBuilder'),
      ),
      body: Center(
        //3
        child: FutureBuilder(
          //4
          future: data,
          //5
          builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
            //6
            if (snapshot.hasData) {
              //7
              return Text(snapshot.data!);
              //8
            } else if (snapshot.hasError) {
              //9
              return Text('${snapshot.error!}');
            } else {
              //10
              return const CircularProgressIndicator();
            }
          },
        ),
      ),
    );
  }
}

//1
5秒待ち、その後Stringを返す処理です。

//2
Future<String>型の変数 dataの初期化部分です。
ここで//1で定義したgetDataの関数を実行します。
このdataにFuture関数の結果が保持されます。

//3
FutureBuilderの定義部分です。

//4
futureの定義部分です。
ここで定義したインスタンスの状態(エラーだったり取得中だったり)で、
次に説明するsnapshotの状態が変わります。

//5
builderの定義部分です。
通常のbuilderと同様にBuildContextを引数に持ちます。
もう一つのAsyncSnapshot<T>の引数が上で定義したfutureの状態によって状態を変え、
futureの結果のデータだったり、エラー結果を持ちます。
Tの部分にはfutureで設定したFuture<T>関数のTのクラスが入ります。)

//6
snapshot.hasDataで、futureで指定した関数に結果が返ってきたかどうかを判定します。

//7
snapshot.datafutureで指定した関数の実行結果が入ります。

//8
snapshot.hasErrorで、futureで指定した関数でエラーが発生したかどうかを判定します。

//9
snapshot.errorfutureで指定した関数のエラー結果が入ります。

//10
エラーも結果も返ってきていない状態、実行中の状態の時の処理です。

Streamとは

Stream って難しそうなんだよなぁ・・・

ちょっとイメージするのが難しいですが、イメージを掴めばこれ以上ない武器になります!

Streamとは、データオブジェクトを一つずつ渡していく川の流れのようなものです。

例えばFirebase等のデータベースを考えてみましょう。
データベースのデータは色々なユーザーによって更新されていきます。

この更新されたデータ一つ一つが川の流れのように流れていき、
あなたのアプリでデータの変化を確認する、そんなことが、Streamを使うと可能となります。

Streamのサンプルコードを見てみましょう。

Stream<int> countStream() async* {
  for (int i = 1; i <= 50; i++) {
    await Future.delayed(Duration(seconds:1));
    yield i;
  }
}

1秒ごとに1から50まで数字を返していくStream関数となります。

Futureではasyncを使いましたが、Streamではasync*を使います。
また、Streamでは、returnの代わりにyieldを使い値を何度も返します。

このようにデータの流れを作るようにデータを返すのがStreamとなります。

StreamBuilder

Stream関数の結果を使ってUIを作成するにはどうしたらいいの?

Streamの値によって画面の値を変えていくにはStreamBuilderを使うと便利です。

こちらもサンプルコードで解説していきます。

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({
    super.key,
  });

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({
    super.key,
  });

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Stream<int> data;

  //1秒ごとに数値を返す関数
  Stream<int> countStream() async* {
    for (var i = 1; i <= 50; i++) {
      await Future<void>.delayed(const Duration(seconds: 1));
      yield i;
    }
  }

  @override
  void initState() {
    //1
    data = countStream();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('StreamBuilder'),
      ),
      body: Center(
        //2
        child: StreamBuilder(
          //3
          stream: data,
          //4
          builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
            //5
            if (snapshot.hasData) {
              return Text(snapshot.data!.toString());
            } else if (snapshot.hasError) {
              return Text('${snapshot.error!}');
            } else {
              return const CircularProgressIndicator();
            }
          },
        ),
      ),
    );
  }
}

//1
dataStream関数を設定している部分です。
StreamBuilderでこのdataを監視することで、
Streamのデータを取得します。

//2
StreamBuilderの定義部分です。

//3
上で定義したdataを設定します。

//4
builderの定義部分です。
FutureBuilderで解説した内容と同様です。
通常のbuilderと同様にBuildContextを引数に持ちます。
もう一つのAsyncSnapshot<T>の引数が上で定義したStreamの状態によって状態を変え、
Streamの結果のデータだったり、エラー結果を持ちます。
Tの部分にはstreamで設定したStream<T>インスタンスのTのクラスが入ります。)

//5
以下、FutureBuilderと同内容のため省略します。

FutureとStream の違い

最後にFutureStreamの違いを3つ紹介します。

検知する変数データの違い

Futureは一度その関数を実行すれば返り値は一つのため、一つの結果しか出てきません。
そのため、変数の変化を追う、ということはできません。

一方Streamはデータの流れを作るため、変数の変化を追うことは得意です。

使う演算子の違い

Future関数では、asyncreturnを用いて値を返します。

一方で、Stream関数では、async*yieldを使って値を返します。

Builderの違い

Future関数を用いるFutureBuilderは一度だけデータを取得し画面を構築する、
という処理が得意です。

一方で、Stream関数を用いるStreamBuilderは、データを監視し続け、
変化するデータによって画面を更新する、という処理が得意です。
特に、リアルタイムのチャットアプリの実装等で役に立ちます。

まとめ

本記事ではFutureStreamそれぞれについて解説し、
違いについてまとめました。

FutureStreamも、少し複雑なアプリを作成しようとすると、
避けては通れないクラスだと思います。

この後の参考に本記事を書くにあたって参考にした資料を並べてあります。
もっと理解を深めたい方は、ぜひ読んでみてください。

本記事がアプリ制作の一助となれば幸いです。

参考

What is the difference between stream and future in Flutter?
In today's this vs that version of Flutter, we will see the difference between Streams and Futures. Streams and Futures are widely used when...
Future class - dart:async library - Dart API
API docs for the Future class from the dart:async library, for the Dart programming language.
FutureBuilder class - widgets library - Dart API
API docs for the FutureBuilder class from the widgets library, for the Dart programming language.
The Event Loop and Dart (翻訳) - Qiita
注意:英語よく分からないので間違ってたら教えてください Dartとイベントループ 原文: 2014/12/10時点の内容を元に翻訳 ...
Asynchronous programming: Streams
Learn how to consume single-subscriber and broadcast streams.
Dart 2 Language Guide
StreamBuilder class - widgets library - Dart API
API docs for the StreamBuilder class from the widgets library, for the Dart programming language.

編集後記(2022/03/09)

本日3月9日午前3時から、Appleの新商品発表会がありましたね。

Flutter大学内でも、Mac Studio 買う?どうする?という話題でもちきりになっていました。

かくいう自分もFlutter大学メンバーとリアルタイムで発表会を見て、
リアクションしつつとても楽しんでいました。

自分としてはiPad Airが気になりましたね。
性能も申し分ないし、キーボードさえ用意すれば
ブログを書くのに役に立ちそうです。

みなさんはどんな商品が気になりましたか?
私のTwitterにて是非是非コメントいただけると嬉しいです。

週刊Flutter大学では、Flutterに関する技術記事、Flutter大学についての紹介記事を投稿していきます。
記事の更新情報はFlutter大学Twitterにて告知します。
ぜひぜひフォローをお願いいたします。

タイトルとURLをコピーしました