 Flutterの記事を整理し本にしました
 Flutterの記事を整理し本にしました  
- 本稿の記事を含む様々な記事を体系的に整理し本にまとめました
- 今後はこちらを最新化するため、最新情報はこちらをご確認ください
- 10万文字を超える超大作になっています(笑)
はじめに
Futureの非同期処理を整理しました。
まとめ
同期と非同期
まずは、同期と非同期を整理します。
同期は、処理のタイミングを合わせ、1つ目の開始→完了、2つ目の開始→完了、3つ目の開始→完了と順番に行っていく処理方法です。
非同期は、1,2,3を開始→各々終わったら結果を返すというやり方です。
補足ですが、Dartの実行モデルはシングルスレッドのイベントループとなっています。
非同期のメリットとデメリット
非同期のメリットは、無駄な待ち時間が少ないということです。
始められるメソッドは前のメソッドの完了を待たずにスタートするので、並行で始められるメソッドの作業開始待ちを防げます。
非同期のデメリットは、待ち合わせや順序性の確保が面倒ということです。
前のメソッドの出力結果に依存するなどの、前が完全に終わってからでないと、次を開始できないような場合は、同期や待ち合わせをする必要があります。
同期の例
通常の関数
HelloWorldをカスタマイズをして、動作を確認します。
同期/非同期を行うAsyncというクラスを使って確認します。
まずは、通常の関数からです。
import 'package:flutter/material.dart';
import 'package:hello_world/async.dart';
void main() {
  runApp(MyApp());
}
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, this.title}) : super(key: key);
  final String? title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  void _incrementCounter() {
    setState(() {
      _counter++;
    });
+    Async().asynctest1(); // 2-4も同様にここで呼び出す
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title!),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              key: Key('counter'),
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        key: Key('increment'),
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}
import 'dart:async';
import 'dart:io';
class Async {
  void asynctest1() {
    print("method begin");
    print(DateTime.now().toString());
    print("data1 start");
    print(syncFunc("data1", 3));
    print("data2 start");
    print(syncFunc("data2", 2));
    print("data3 start");
    print(syncFunc("data3", 1));
  }
  // timeの時間分スリープし、その後現在時間を返す関数
  String syncFunc(String name, int time) {
    sleep(Duration(seconds: time));
    return name + ":" + DateTime.now().toString();
  }
通常の関数呼び出しですので、上から順番に処理を行っていきます。
I/flutter (15069): method begin
I/flutter (15069): 2021-04-10 14:50:32.686241
I/flutter (15069): data1 start
I/flutter (15069): data1:2021-04-10 14:50:35.691230
I/flutter (15069): data2 start
I/flutter (15069): data2:2021-04-10 14:50:37.696335
I/flutter (15069): data3 start
I/flutter (15069): data3:2021-04-10 14:50:38.699074
特段違和感はないかと思います。
非同期の例
それでは、非同期処理を見ていきます。
Future
まず、ソースコードの実装例を見てみましょう。
import 'dart:async';
import 'dart:io';
class Async {
  void asynctest2() {
    print("method begin");
    print(DateTime.now().toString());
    print("data1 start");
    print(asyncFunc("data1", 3));
    print("data2 start");
    print(asyncFunc("data2", 2));
    print("data3 start");
    print(asyncFunc("data3", 1));
  }
  // timeの時間分スリープし、その後現在時間を返す関数
  Future<String> asyncFunc(String name, int time) async {
    return Future.delayed(Duration(seconds: time), () {
      return name + ":" + DateTime.now().toString();
    });
  }
asynctest2の中では、asyncFuncという非同期メソッドを呼び出しています。
非同期のメソッドを定義する場合はasyncのキーワードをメソッドに付与します。
asyncはバージョンにより挙動が異なるので注意が必要です。
Dart 1系ではすぐに非同期処理になります。
Dart.2系ではすぐに非同期処理になるのではなく、最初のawaitまたはreturnに達するまでは同期処理で実行します。
戻り値が見慣れないFuture<String>というものが設定されています。
これは、将来的にはStringが返却されるという意味になります。
I/flutter (15069): method begin
I/flutter (15069): 2021-04-10 14:51:43.855018
I/flutter (15069): data1 start
I/flutter (15069): Instance of 'Future<String>'
I/flutter (15069): data2 start
I/flutter (15069): Instance of 'Future<String>'
I/flutter (15069): data3 start
I/flutter (15069): Instance of 'Future<String>'
asynctest2は、それぞれのメソッドを呼び出しますが、Future<String>というものを仮で受け取ったあとに、すぐにその次の処理に移っています。
時間の表示がないので分かりづらいですが、各メソッドからFuture<String>が返ってくるのはsleepする時間を待たずに(処理を完了させずに)返ってきています
つまり、呼び出し側はそれぞれのメソッドを呼び出した後に、次の処理に移れるため非同期の処理を実現することができます。
then
Future<String>は将来的にStringが戻ってくることを記述していますが、具体的なタイミングやStringへの変換の仕方がまだ決められていません。
大きくやり方は2種類あり、1つめは、値が来た時のコールバック関数で定義することです。
import 'dart:async';
import 'dart:io';
class Async {
  void asynctest3() async {
    print("method begin");
    print(DateTime.now().toString());
    print("data1 start");
    Future<String> result1 = asyncFunc("data1", 3);
    result1.then((result) {
      print(result);
    });
    print("data2 start");
    Future<String> result2 = asyncFunc("data2", 2);
    result2.then((result) {
      print(result);
    });
    print("data3 start");
    Future<String> result3 = asyncFunc("data3", 1);
    result3.then((result) {
      print(result);
    });
  }
  // timeの時間分スリープし、その後現在時間を返す関数
  Future<String> asyncFunc(String name, int time) async {
    return Future.delayed(Duration(seconds: time), () {
      return name + ":" + DateTime.now().toString();
    });
  }
Futureにはthenというメソッドが準備されています。
このメソッドは、処理が終わった時に呼び出されます。
今回はシンプルに、値が確定したらprintしています
今回は省略しましたが、catchErrorというメソッドもあり、エラーが発生した時に呼び出されるメソッドです。
I/flutter (15069): method begin
I/flutter (15069): 2021-04-10 14:52:49.054188
I/flutter (15069): data1 start
I/flutter (15069): data2 start
I/flutter (15069): data3 start
I/flutter (15069): data3:2021-04-10 14:52:50.070597
I/flutter (15069): data2:2021-04-10 14:52:51.069553
I/flutter (15069): data1:2021-04-10 14:52:52.067980
結果を見てみると、まずdata1,2,3がstartし、その後終わった順(呼び出した順ではなく)にdata3,2,1と文字列を表示しています。
1,2,3はそれぞれ3秒,2秒,1秒スリープするようになっていますので、逆順で出力されています。
asynctest3と3つのasyncFuncはそれぞれ独立して非同期になっていることがわかります。
複数のFuture全てで待ち合わせをする場合は、thenが入れ子になり、いわゆるコールバック地獄と呼ばれる状態になることがあります。
そのような場合には、Future.wait(List)のようなメソッドが準備されており、すべてが揃うまでまとめてawaitすることができます。
await
thenによるコールバック以外にも、もう1つのやり方があります。
それは、awaitによる値が確定するまで待つというやり方です。
awaitは非同期関数内で関数の戻り値を待ち合わせをするというものなので、async関数内でしか使えません。asyncがないとコンパイルエラーになります。
import 'dart:async';
import 'dart:io';
class Async {
  void asynctest4() async {
    print("method begin");
    print(DateTime.now().toString());
    print("data1 start");
    print(await asyncFunc("data1", 3));
    print("data2 start");
    print(await asyncFunc("data2", 2));
    print("data3 start");
    print(await asyncFunc("data3", 1));
  }
  // timeの時間分スリープし、その後現在時間を返す関数
  Future<String> asyncFunc(String name, int time) async {
    return Future.delayed(Duration(seconds: time), () {
      return name + ":" + DateTime.now().toString();
    });
  }
awaitというキーワードを使うと、その場で非同期関数の終了を待ちます。
I/flutter (15069): method begin
I/flutter (15069): 2021-04-10 14:54:09.874908
I/flutter (15069): data1 start
I/flutter (15069): data1:2021-04-10 14:54:12.887107
I/flutter (15069): data2 start
I/flutter (15069): data2:2021-04-10 14:54:14.895110
I/flutter (15069): data3 start
I/flutter (15069): data3:2021-04-10 14:54:15.910756
awaitの場合は、data1をはじめた後に、data2の開始を行うのではなく、data1のasyncFuncが終わるのを待っています。
つまり、待ち合わせを行い同期的に動いています。
非同期関数からの戻り値をFutureで受け取ることなく、直接Stringであつかえるので便利ですが、そもそも非同期処理は重たい処理を並行して行いたい時に使うので、終わるまで次に進まないというのは、パフォーマンスの劣化につながる点に注意が必要です。
