Flutter WebではIsolateが並列処理ではない
Flutterでは並列処理を行うためにIsolateを使用することが出来ます。ですが、Flutter WebではIsolateを使用しても並列処理にはなりません。代わりにWeb Workerという仕組みを使う必要があります。本記事では、package:webを利用したWeb Workerの使用方法について見ていきたいと思います。
Workerの使い方
Workerの使い方について見ていきます。package:web
のクラスを利用していることが分かりやすいように、import 'package:web/web.dart' as web
としてインポートしている前提で説明します。
呼び出し側(親)
呼び出し側ではWorker
コンストラクタにより、Workerを作成します。引数にはjavascriptのURLを取ります。例えば、下記のようになります。
final worker = web.Worker('path/to/worker.js');
Workerに並列処理を行ってもらうためにはpostMessage関数を使用してメッセージを送信します。例えば、下記のようになります。
worker.postMessage(3.toJS);
上記の例では、数値の3というデータを送信しています。Dartにおける3とjavascriptにおける3は異なるので、javascriptの数値に変換して渡す必要があります。そのため、toJS
により変換を行っています。
Workerからのメッセージを受け取るためにはworker.onmessage
を設定しておく必要があります。例えば、下記のようになります。
worker.onmessage = ((web.MessageEvent message) {
print((message.data as JSNumber?)?.toDartInt);
}).toJS;
onmessage
はjavascriptの関数として呼び出されるので、toJS
により変換したものを代入する必要があります。また、引数はweb.MessageEvent
型となります。data
メンバにWorker側から送信されてきたデータが格納されているので、それを取り出して処理することになります。
Worker側(子)
Worker側はjavascriptで実装してもよいのですが、ここではDartで実装してjavascriptにコンパイルすることとします。呼び出し側と同じくonmessage
とpostMessage
を使用することになります。
まず、呼び出し側からのメッセージを受信するonmessage
についてです。呼び出し側と同様にonmessage
には自身で処理内容を定義した関数を代入する必要があります。代入先となるonmessage
自体はjavascript側となるので、それをDart側から代入できるようにするための記述を行います。
@JS('onmessage')
external set onmessage(JSAny? message);
1行目がjavascript側でonmessage
という名称であることを表しており、2行目でDart側から代入可能であること(set
)、なんらかの引数をとること(JSAny?
)を表しています。
onmessage
への関数の代入はmain
関数内で行います。例えば、下記のようになります。
void main() {
onmessage = ((web.MessageEvent message) {
postMessage(message.data);
}).toJS;
}
onmessage
に対して代入可能にする宣言では引数をJSAny?
型としていましたが、ここではweb.MessageEvent
型としています。web.MessageEvent
型のdata
に呼び出し側から送信されたデータが格納されています。postMessage
については後述しますが、この例では呼び出し側から受け取ったデータをそのまま呼び出し側に返しています。
既に登場してしまっていますが、呼び出し側にメッセージを送るためにはpostMessage
を使用します。postMessage
はjavascript側に予め定義されているので、それをDartから呼び出し可能とするために下記の記述を行います。
@JS('postMessage')
external void postMessage(JSAny? message);
ここまでで呼び出し側からWorker側に処理を投げることと、Worker側で処理が終わったことを呼び出し側に投げることが可能になりました。
Workerを使った例
重い処理を下記3通りの方法で行った場合の動作を比較するためのFlutterアプリを作ってみました。
- Worker
- Compute
- 同期処理
各テキストを押すと1秒程度かかる重い処理を行った後に下に表示されている数値に値が加算されるというサンプルです。実際に動作を見てみると分かると思いますが、Worker以外では並列処理とならないためインジケータのアニメーションが止まってしまいます。
Workerを使用するのは時間がかかる重い処理をする場合が多いと思います。今回は、重い処理を想定したcalc
関数を実装したlib/calc.dartを使用します。
import 'dart:core';
// 重い計算処理
int calc(int n) {
final t = DateTime.now();
// 重い処理のかわりに1秒かかるループを回す
while (true) {
if (DateTime.now().difference(t).inMilliseconds >= 1000) {
break;
}
}
return n * 2;
}
呼び出し側を定義したlib/main.dartは下記のようになっています。
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:web/web.dart' as web;
import 'dart:js_interop';
import 'dart:ui_web' as ui;
import 'calc.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(theme: ThemeData.dark(), home: const _MainView());
}
}
class _MainView extends StatefulWidget {
const _MainView();
@override
_MainViewState createState() => _MainViewState();
}
class _MainViewState extends State<_MainView> {
var count = 0;
// アセットのassets/worker.jsからWorkerを生成
final worker = web.Worker(ui.assetManager.getAssetUrl('assets/worker.js'));
@override
void initState() {
super.initState();
// Worker側からのメッセージ受信
worker.onmessage = ((web.MessageEvent message) {
setState(() {
count += (message.data as JSNumber?)?.toDartInt ?? 0;
});
}).toJS;
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Flexible(
child: TextButton(
child: const Text('Worker (+6)'),
onPressed: () {
// Workerへのメッセージ送信
worker.postMessage(3.toJS);
},
)),
Flexible(
child: TextButton(
child: const Text('Compute (+4)'),
onPressed: () {
compute(calc, 2).then((n) {
setState(() {
count += n;
});
});
},
)),
Flexible(
child: TextButton(
child: const Text('Sync (+2)'),
onPressed: () {
setState(() {
count += calc(1);
});
},
)),
// 並列実行できているかどうかを確かめるためのインジケータ
const Flexible(child: CircularProgressIndicator()),
Flexible(child: Text('$count'))
],
)));
}
}
Worker側はjavascriptとしてアセットにするので、一旦適当な場所にworker.dartとして作成します。
import 'dart:js_interop';
import 'package:web/web.dart' as web;
// プロジェクト内に定義されているlib/calc.dartを参照
import 'package:test_worker/calc.dart';
// postMessageを呼び出し可能とするための宣言
@JS('postMessage')
external void postMessage(JSAny? message);
// onmessageに関数を代入可能とするための宣言
@JS('onmessage')
external set onmessage(JSAny? message);
void main() {
// onmessageに関数を代入
onmessage = ((web.MessageEvent message) {
postMessage(calc((message.data as JSNumber?)?.toDartInt ?? 0).toJS);
postMessage(message.data);
}).toJS;
}
このソースをjavascriptにコンパイルするため、assets以下で下記のコマンドを実行します(この例ではassets以下にworker.dartがある形です)。
$ dart compile js worker.dart -o worker.js
このコマンドにより、assets以下にworker.jsが生成されます。
アセットとしてworker.jsを扱うためにpubspec.yamlも修正が必要です。flutter:
の箇所に追記します。
flutter:
〜中略〜
# 以下を追記
assets:
- assets/
これでビルドすれば掲載したFlutterアプリと同じ動作をするはずです。
まとめ
本記事ではFlutter Webで並列処理を行うためのWeb Workerを使用する方法について説明しました。個人的にはonmessage
に代入する関数の引数がweb.MessageEvent
型であるということに気付かず少し時間を無駄にしてしまいました。この記事が参考になれば幸いです。
コメント