【Flutter Web】package:webを利用したWeb Workerによる並列処理

この記事は約10分で読めます。

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にコンパイルすることとします。呼び出し側と同じくonmessagepostMessageを使用することになります。

まず、呼び出し側からのメッセージを受信する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型であるということに気付かず少し時間を無駄にしてしまいました。この記事が参考になれば幸いです。

コメント

タイトルとURLをコピーしました