【Widget of the Week】#6 FutureBuilder

【#6 FutureBuilder】動画

【#6 FutureBuilder】概要

Flutterの非同期処理はFutureを使おう!

Futureを使ったウィジェットのビルドはどうやるの?

【#6 FutureBuilder】解説

①Flutterの非同期処理はFutureを使おう!

FlutterではFutureを活用して非同期処理を実装することができます。

そもそも非同期処理がよくわからないという方のために簡単に概要だけ解説します。

非同期処理を理解するためにはまず同期処理を理解する必要がありますので、同期処理の解説からはじめますね。

同期処理

同期処理とは簡潔にいうと、書いたプログラムが必ず1行ずつ順番に実行される方式のことです。

図のようにタスク1は必ずタスク2の処理が終了するまで次の処理に移行することはありません。

このメリットとしては処理全体が把握しやすく、プログラムが複雑化することを回避できます。

しかし、デメリットとしては処理待ちが多く発生することが挙げられます。

特にタスク2はWeb通信をしているので時間のかかる処理であることが予想されます。

ユーザ視点で見るとタスク1・タスク2両方が終了しないとレスポンスが帰ってこないため、しばらく画面全体が固まってしまったかのように見えてしまうことがあります。

その問題を解決する非同期処理について解説していきます。

非同期処理

時間のかかる処理の結果を何もせず待つのではなく、その待ち時間も別の処理やろうよというのが非同期処理です。

同期処理ではリクエストAの処理が全て終了するまで他のリクエストをブロックしてしまう仕組みになっていました。

それに対して非同期処理ではリクエストAを処理している間にも他のリクエストBを処理することができるため、ユーザーAはリクエストAの処理を待たずに結果を取得できています。

非同期処理により処理効率を上げることができ、特にクライアント側では処理待ちによるユーザビィティの低下を防ぐことができます。

その一方プログラムが複雑化しやすく、今なんの処理をしているのかが追いにくくなる可能性もあります。

※非同期処理の具体的な使用例はこちら(Google Mapで使われているAjax)

Future, Await Async

前項で非同期処理の仕組みを解説しましたが、実際にどうコーディングするのか解説していきたいと思います。

Future

1. 時間のかかる処理は一旦Futureインスタンスを返す!

Future<String> returnFuture(){
  String text;

  // 時間のかかる処理
  for (int i = 0; i <= 100000; i++){
    if(i == 100000) text = 'Finished';
  }

  // 一旦Futureインスタンスを返す
  return Future<String>.value(text);
}

void main(){
  // Futureが返ってくる
  final text = returnFuture()

  // Futureを出力
  print(text);

  print('the last sentence of main');
}

出力結果

Instance of '_Future<String>'
the last sentence of main

2. 処理が終わったらちゃんと結果を受け取るよ!

Future<String> returnFuture(){
  String text;

  // 時間のかかる処理
  for (int i = 0; i <= 100000; i++){
    if(i == 100000) text = 'Finished';
  }

  // ①一旦Futureインスタンスを返す
  // ③処理が終了するとFuture<String>.valueに'Finished'の文字列が入る
  return Future<String>.value(text);
}

void main(){
  // ②Futureが返ってくる
  final text = returnFuture()

  // ④returnFuture()の処理が終了したらthenメソッドが実行され、’Finished'の文字列を受け取り出力する
  text.then((value) => {
    print(value)
  });

  print('the last sentence of main');
}

出力結果

the last sentence of main
Finished

ここまで解説を見てjavascriptのPromiseじゃん!と思われた方、その通りです笑

Dartは元々javascriptをベースとしているのでこういった共通点が多くあります。

Promiseは「結果を必ず返す約束をするよ」Futureは「将来的に結果をちゃんと返すよ」といった意味で命名されているのかなと思います(完全に推測です)

次はFutureをもっと使いやすくした書き方AwaitとAsyncについて解説します。

Await Async

1. thenメソッドをAwait Asyncに書き換えてみよう!

Future<String> returnFuture() {
  String text;

  // 時間のかかる処理
  for (int i = 0; i <= 100000; i++){
    if(i == 100000) text = 'Finished';
  }

  // ①一旦Futureインスタンスを返す
  // ③処理が終了するとFuture<String>.valueに'Finished'の文字列が入る
  return Future<String>.value(text);
}

void main() async{
  // ②Futureが返ってくる
  // ④returnFuture()の処理が終了したら'Finished'の文字列を受け取る
  final text = await returnFuture()

  // ⑤returnFutureの処理が完了するまで実行されない
  print(text);
  print('the last sentence of main');
}

出力結果

Finished
the last sentence of main

Await と Asyncを使用することで、thenメソッドなどの非同期処理特有の文法を意識しなくても書けるようになりましたね。

FlutterではAwait Asyncの書き方が推奨されていますので、ガンガン使っていきましょう!

②Futureを使ったウィジェットのビルドはどうやるの?

Futureは前項で解説したようにちゃんとした処理結果をあとで返すという仕組みがあります。

その結果が返ってくるまでウィジェットは何を表示したらいいのでしょう?

それを解決してくれるのがFutureBuilderウィジェットです。

【#6 FutureBuilder】サンプルコード① (基本的なプロパティ)

①future

型:Future<T>
Futureを返す非同期処理を設定する。

②builder

型:AsyncWidgetBuilder<T>
引数としてcontextとsnapshotを持っており、snapshotはfutureの返り値などを持っており非常に重要。
サンプルコード②で詳しく説明する。

※詳細はこちら(公式ドキュメント)

実行結果(_calculationメソッド終了前)

実行結果(_calculationメソッド終了後)

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Basic Properties'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  // 処理を5秒遅らせる
  final Future<String> _calculation = Future<String>.delayed(
    const Duration(seconds: 5),
        () => 'Data Loaded',
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Center(
          child: FutureBuilder<String>(
            future: _calculation,
            builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
             
              if (snapshot.hasData) {
                // データがある場合はResult: Data Loadedを表示
                return Text('Result: ${snapshot.data}');
              } else {
                // データがない場合はLoading...を表示する
                return Text('Loading...');
              }
            },
          ),
        )
    );
  }
}

【#6 FutureBuilder】サンプルコード②(AsyncSnapshot<T> snapshot)

FutureBuilderにおいてsnapshotは機能が多く、重要な引数なので少し掘り下げて解説したいと思います。

①data
型:T
非同期処理の結果を保持する。

具体例<サンプルコード①の場合>
futureプロパティに設定した_calculate の返り値は’Data Loaded’なので、
snapshot.dataは’Data Loaded’を保持する。

②error
型:Object
非同期処理でエラーが発生した場合にそのエラー情報を保持する。

③hasData
型:bool
dataプロパティに値があるかどうかをtrue / falseで保持する。

④hasError
型:errorプロパティに値があるかどうかをtrue / false で保持する。

⑤connectionState
型:ConnectionState
active, done, waitingなどで非同期処理の処理状態を保持する。

エラーハンドリング

hasErrorを使ってエラーが発生した場合はエラー情報を画面に出力する処理を追加

実行結果

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Error Handling'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  // 処理を5秒遅らせる
  Future<String> _calculation() async {
    await Future.delayed(Duration(seconds: 59),);

    try{
      // 必ずエラーを起こす
      throw Exception('Failed loading data');
    }catch (error){
      return Future.error(error);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Center(
          child: FutureBuilder<String>(
            future: _calculation(),
            builder: (BuildContext context, AsyncSnapshot<String> snapshot) {

              //エラーチェック処理を追加
              if(snapshot.hasError){
                return Text(snapshot.error.toString());
              }

              if (snapshot.hasData) {
                // データがある場合はResult: Data Loadedを表示
                return Text('Result: ${snapshot.data}');
              } else {
                // 処理が完了してもデータがない場合はdata is nothingを表示する
                return Text('data is nothing');
              }
            },
          ),
        )
    );
  }

}

connectionState

connectionStateを利用して非同期処理が完了するまではスピナーを表示するように変更する。

実行結果(非同期処理実行中)

実行結果(非同期処理終了後)

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo ConnectionState'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  // 処理を5秒遅らせる
  Future<String> _calculation() async {
    await Future.delayed(Duration(seconds: 59),);

    try{
      // 必ずエラーを起こす
      throw Exception('Failed loading data');
    }catch (error){
      return Future.error(error);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Center(
          child: FutureBuilder<String>(
            future: _calculation(),
            builder: (BuildContext context, AsyncSnapshot<String> snapshot) {

              // 処理が完了するまではスピナーを表示
              if (snapshot.connectionState != ConnectionState.done) {
                return CircularProgressIndicator();
              }

              //エラーチェック処理を追加
              if(snapshot.hasError){
                return Text(snapshot.error.toString());
              }

              if (snapshot.hasData) {
                // データがある場合はResult: Data Loadedを表示
                return Text('Result: ${snapshot.data}');
              } else {
                // 処理が完了してもデータがない場合はdata is nothingを表示する
                return Text('data is nothing');
              }
            },
          ),
        )
    );
  }

}

投稿者プロフィール

ArisawaRyoma
社会人歴2年目の駆け出しエンジニアです。

学生の時はがっつり文系(=遊んだただけ)でほぼPCに触れずに生きてきたので、プログラミングは日々勉強中。Vue / React / Django / Springなど色々浮気して今はFlutterにハマってます。

趣味は海外旅行、釣り、卓球など。

Twitterでも色々発信してるので良ければフォローお願いします。

関連記事

  1. 【Widget of the Week】#1 Safe Area

  2. 【Widget of the Week】#3 Wrap

  3. 【Widget of the Week】#9 PageView

  4. 【Widget of the Week】#7 FadeTransiti…

  5. 【Widget of the Week】#4 AnimatedCont…

  6. 【Widget of the Week】#5 Opacity