【Flutter】IsolateとRecordで複雑なデータのバックグラウンド処理を簡単に

対象者

  • Flutterで非同期処理を学びたい初心者から中級者
  • DartのIsolateを効果的に活用したい開発者
  • 非同期処理の進捗管理やパフォーマンス測定に興味がある方

はじめに

先日、FlutterでIsolateを使用したシンプルなサンプルを作成しましたが、今回はさらに一歩踏み込んで、より複雑なケースに挑戦してみました。この記事では、新たに取り組んだ3つのポイントについて詳しく解説します。

【Flutter】Isolateで解決!重い処理とUIの両立

まず、Record型を使用して複雑な値を引数として渡す方法について紹介します。これにより、実行ごとに異なる引数を渡すことが可能になります。
今回の例では、処理するメソッドにダウンロードURLを動的に変更できるようになります。これによりメソッド実行ごとに異なるURLや値で実行できるようになりました。また同じく受け取る値を、処理の進捗率と完了フラグの2つにしました。これにより、複数の値をIsolateから受け取ることができます

次に、Isolateの処理完了後に結果を返すだけでなく、処理を行っている最中に進捗率を返すようにしました。これを活用することで、処理中にリアルタイムでLinearProgressIndicatorに進捗率を反映させることができます。

最後に、Isolateの実用性について検証するために、処理時間を計測する方法を紹介します。Stopwatchを使用して、Isolateの起動時間、実行時間、終了処理時間を計測することで、その効果を明確に確認できます。

以上の内容を基に、今回の記事を作成しました。この記事が、あなたのFlutter開発におけるIsolateの理解と活用の一助となれば幸いです。ぜひ最後までご覧ください。

Recordを使用する

Isolateで複数の値をやり取りするために、DartのRecordを使用します。これにより、Isolate間の通信で複数のデータを一度に渡すことができます。

Isolateのメソッドへ複数の値を渡す

Isolateに渡す値をまとめるために、data変数に(receivePort.sendPort, url)という形でSendPortとURLをセットします。

final data = (receivePort.sendPort, url);

Dart 3 のRecord:複数の戻り値を効率的に扱う

Isolateから複数の値を受け取る

Isolateからのデータ受け取り時には、(double, bool)という型で進捗率と完了フラグを受け取ります。

final castedData = receivedData as (double, bool);
final progress = castedData.$1;
final isDone = castedData.$2;

リアルタイムに値を反映

Isolateで進捗率が更新されるたびに、receivePort.listenでデータを受け取り、setStateを使用してプログレスバーの値を更新し、UIに反映させます。

時間測定の方法

処理の時間を測定するために、Stopwatchを使用して、Isolateの開始から終了までの時間を計測します。各段階での時刻は、_gapStart_gapCount_gapClose_gapDoneに格納されます。

Isolate完了の処理

処理の完了

ダウンロード処理が完了した際には、isolate.killreceivePort.closeを呼び出して、Isolateとポートを閉じます。

isolate.kill(priority: Isolate.immediate);
receivePort.close();

処理後の完了

Isolateの処理が完了し、ポートが閉じられた(receivePort.close())後には、onDoneコールバックが呼び出され、_canDownloadtrueに設定して再度ダウンロードが可能になるようにします。

onDone: () {
  setState(() => _canDownload = true);
  setState(() => _gapDone = stopwatch.elapsedMilliseconds);
  stopwatch.stop();
},

このようにして、Isolateを使用してバックグラウンドで処理を行いつつ、その進捗をリアルタイムでメインIsolateに反映させる方法を実装することができます。

Flutter Isolateでの非同期処理のパフォーマンス検証

今回、私はIsolateを使用して実際にどの程度のパフォーマンスが得られるのかを検証してみました。

Android Studioでのデバッグ実行

まず、Android Studioでデバッグモードでアプリを実行し、Isolateの処理時間を計測しました。その結果、処理の起動時には約138msの時間がかかりました。また終了時には約10ms程度が時間が生じました。

初回実行では若干時間が掛かりましたが、2回目以降は起動時間がほとんどなくなり、高いパフォーマンスが得られました。これにより、Isolateを使用してもデバッグモードでの実行は問題なく、高速な処理が可能であることが確認できました。

リリースビルドでの実機実行

さらに、リリースビルドを行い、Androidの実機でアプリを実行してみました。すると、処理の起動処理や終了処理は1ミリ秒未満になることが確認できました。後処理においても時間は生じず、非常に高いパフォーマンスを実現しています。

結論

この検証結果から、Isolateを使用した非同期処理は、リリースビルドにおいて非常に高いパフォーマンスを発揮することがわかりました。デバッグモードでの実行時にも、問題のない速度で実施できます。

以前はFlutter開発において、起動時間が長く、通常の非同期処理の方がパフォーマンスがよい、ということがありました。しかし速度改善がなされ、現在では非同期処理の効率化に、Isolateを選択しても問題ないことが確認できました。

Q&A

Q1: FlutterでIsolateを使用するメリットは何ですか?

A1: FlutterでIsolateを使用するメリットは、メインIsolateとは別にバックグラウンドで処理を行えることです。これにより、重い処理を行ってもUIの応答性を維持でき、ユーザーエクスペリエンスを向上させることができます。

Q2: Record型をIsolate間の通信で使用する理由は何ですか?

A2: Record型を使用する理由は、複数の値を一度に渡すことができるためです。Isolate間の通信では、通常は一つの値しかやり取りできませんが、Record型を使うことで、複数のデータをセットにして渡すことが可能になります。

Q3: Isolateの処理が完了した後の後処理はどのように行うべきですか?

A3: Isolateの処理が完了した後の後処理では、まずisolate.killreceivePort.closeを呼び出してIsolateとポートを閉じることが重要です。その後、onDoneコールバック内で必要な状態更新などの処理を行います。これにより、リソースを適切に解放し、再度処理を実行できる状態に戻すことができます。

まとめ

この記事では、FlutterでIsolateを使った非同期処理の高度な使い方を学びました。Record型を活用して複数の値をやり取りし、リアルタイムで進捗を反映させる方法を理解しました。また、Stopwatchを使った処理時間の計測や、Isolateの完了処理についても勉強しました。これらの知識は、Flutter開発における非同期処理の理解と活用に役立つでしょう。

ソース(main.dartにコピペして動作確認用)

import 'dart:isolate';

import 'package:flutter/material.dart';

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

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  var _rate = 0.0;
  var _canDownload = true;

  var _gapStart = 0;
  var _gapCount = 0;
  var _gapClose = 0;
  var _gapDone = 0;

  void _startDownloading(String url) async {
    setState(() => _canDownload = false);

    final receivePort = ReceivePort();
    final data = (receivePort.sendPort, url);

    final stopwatch = Stopwatch()..start();
    final isolate = await Isolate.spawn(downloadFile, data);
    setState(() => _gapStart = stopwatch.elapsedMilliseconds);

    receivePort.listen(
      (receivedData) {
        final castedData = receivedData as (double, bool);
        final progress = castedData.$1;
        final isDone = castedData.$2;

        setState(() => _rate = progress);
        setState(() => _gapCount = stopwatch.elapsedMilliseconds);

        if (isDone) {
          receivePort.close();
          isolate.kill(priority: Isolate.immediate);
          setState(() => _gapClose = stopwatch.elapsedMilliseconds);
        }
      },
      onDone: () {
        setState(() => _canDownload = true);
        setState(() => _gapDone = stopwatch.elapsedMilliseconds);
        stopwatch.stop();
      },
    );
  }

  static void downloadFile((SendPort, String) data) async {
    final (sendPort, url) = data;

    print('download from $url');

    // ダウンロード処理を模擬
    for (var progress = 0.0; progress < 1.0; progress += 0.1) {
      await Future.delayed(
        const Duration(milliseconds: 100),
        () => sendPort.send((progress, false)),
      );
    }

    sendPort.send((1.0, true));
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Isolate Demo'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              FilledButton(
                onPressed:
                    _canDownload ? () => _startDownloading('test') : null,
                child: const Text('Download imitation'),
              ),
              Padding(
                padding: const EdgeInsets.symmetric(horizontal: 32),
                child: LinearProgressIndicator(value: _rate),
              ),
              const SizedBox(height: 32),
              Text('開始の時差: $_gapStart'),
              Text('処理中の時差: $_gapCount'),
              Text('終了の時差: $_gapClose'),
              Text('後処理の時差: $_gapDone'),
            ],
          ),
        ),
      ),
    );
  }
}