対象者
- Flutterで非同期処理を学びたい初心者から中級者
- DartのIsolateを効果的に活用したい開発者
- 非同期処理の進捗管理やパフォーマンス測定に興味がある方
はじめに
先日、FlutterでIsolateを使用したシンプルなサンプルを作成しましたが、今回はさらに一歩踏み込んで、より複雑なケースに挑戦してみました。この記事では、新たに取り組んだ3つのポイントについて詳しく解説します。
まず、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);
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.kill
とreceivePort.close
を呼び出して、Isolateとポートを閉じます。
isolate.kill(priority: Isolate.immediate);
receivePort.close();
処理後の完了
Isolateの処理が完了し、ポートが閉じられた(receivePort.close()
)後には、onDone
コールバックが呼び出され、_canDownload
をtrue
に設定して再度ダウンロードが可能になるようにします。
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.kill
とreceivePort.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'),
],
),
),
),
);
}
}