はじめに
dart2wasm対応のために自作Flutter Webアプリをdart:html
からpackage:web
に移行しようとしたところ、サードパーティのパッケージと名前のコンフリクトが多数発生しました。これはむしろサードパーティパッケージが不要になったという良いシグナルだろうと考え、これもpackage:web
他に移行したという話です。
File System Access APIとは
File System Access APIはWebアプリ(ブラウザアプリ)からローカルのファイルシステムへのアクセスを提供します。自作アプリではディレクトリピッカで選択させたディレクトリの中のファイルを読み書きしています。これまでは、公式パッケージのサポートがなかったため、サードパーティのpackage:file_system_accsess_api
を利用していました。
公式新パッケージへの移行
Dart SDK 3.3.0でextension type
とそれを用いたpackage:web
が利用できるようになったのでこちらに移行します。これはブラウザAPIへのオーバヘッドの無いバインディングをWeb IDLから自動生成したものだそうです。
Dart PMのkevmooさん曰くpackage:webはほとんどクレイジー。
The new package:web is pretty crazy. We use Dart code + JS code (NPM modules), compiled to JS to generate Dart code (pkg:web /lib contents) that we then compile to JS (and WebAssembly).
JSにコンパイルしたDartコードとJSコード(NPMモジュール)でDartコードを生成し(これがpackage:web
の本体)、後にこれをJS(およびWASM)にコンパイルして利用する...
なお、後述の通りwindow.showDirectoryPicker()
が提供されていないのでごにょごにょするためにdart:js_interop
も利用しています。
パッケージの変更
pubspac.yamlの変更
environment:
sdk: '>=2.18.0 <3.0.0'
dependencies:
file_system_access_api: ^2.0.0
environment:
sdk: '>=3.3.0 <4.0.0'
dependencies:
なお、ごにょごにょするために、Dart SDKの下限も3.3.0にしております。
importの変更
import 'dart:html';
import 'package:file_system_access_api/file_system_access_api.dart';
import 'dart:js_interop';
import 'package:web/web.dart' as web;
プレフィクス追加
Fultterとpackage:web
でクラス名Text
がコンフリクトしました。当然(?)Flutterを優先してpackage:web
をas web
としました。その結果として未解決となった箇所にプレフィクスweb.
を追加していきます。
FileSystemWritableFileStream? logStream;
web.FileSystemWritableFileStream? logStream;
JS型とDart型の相互変換
残る静的エラーの大半はDart型とJS型の差異によるものです。エラーメッセージとIDEの推奨に従いtoJS
またはtoDart
を追加することで簡単に解消できます。
await logStream!.writeAsText('$message\n');
await logStream!.write('$message\n'.toJS).toDart;
なお、この例ではメソッドの変更も必要でした。
名前付きオプション引数をextension type
に変更
ブラウザAPIはオプション群としてJSのオブジェクトリテラルを受け取ることがあります。package:file_system_access_api
では手作りでこれらをDartらしい名前付きオプションパラメタに展開していました。package:web
移行に伴い、オブジェクトリテラルに戻す必要があります。
こちらもIDEの推奨に従い、それっぽい名前のextension type
を選ぶだけの簡単な作業です。
await dirHandle.getFileHandle('lockfile', create: true);
await dirHandle
.getFileHandle('lockfile', web.FileSystemGetFileOptions(create: true))
.toDart;
とはいえ、extension type
の名前が長いのが気になりますね。
ディレクトリピッカのコール
ディレクトリピッカの露出
Compatibilityによると、experimentalであるwindow.showDirectoryPicker()
をSafariとFirefoxがサポートしていないために、package:web
のWindow
に露出していません。
これを利用するためには、公式ドキュメントInterop type membersに従い、Window
のextension type
をもう一つ(例:@JS('Window')Window2
)作るという方法が有ります。external
なメソッドを宣言すると、あとはDart SDKが良いように計らってくれます。
extensionで回避
とはいえ、package:web
が既にWindow
を提供してるので、これにメソッドを追加することを考えました。同公式ドキュメントを読み進めると、下記の様にextension
にexternal
なメソッドを宣言してあげることでwindow.showDirectoryPicker()
にアクセスできます。
extension on web.Window {
external JSPromise<web.FileSystemDirectoryHandle> showDirectoryPicker(
[JSAny? options]);
}
options
window.showDirectoryPicker()
はJavascriptオブジェクトリテラルの形でオプションを受け取ります。ここではディレクトリへの書き込み権限が欲しかったので、なんとかオブジェクトリテラルを渡す必要が有ります。以下の様にすることで、{mode: 'readwrite'}
を生成できるようになりました。
@JS()
extension type Options._(JSObject _) implements JSObject {
external Options({String mode});
}
なお、Dart SDK 3.3.0ではそのバグ回避に下記のおまじないも必要です。
@JS()
library;
と言った直後に、Dart SDK 3.3.1がリリースされ、そのバグが解消されました。
ディレクトリピッカのコール
これで晴れて、showDirectoryPicker()
が呼べるようになりました。
dirHandle =
await window.showDirectoryPicker(mode: PermissionMode.readwrite);
dirHandle = await web.window
.showDirectoryPicker(Options(mode: 'readwrite'))
.toDart;
マイグレーション全容
マイグレーション全容を下記に示しておきます。
migrate from package:file_system_access_api to package:web · Cat-sushi/fmscreen_flutter@1a58e30
おわりに
実際にやってみると、たいして難しくはありません。
また、dart:js_interop
のおかげでJS FFIが自分で作れるのが良いですね。今回の様に、Dart SDKのサポートも、サードパーティAPIの出現も、W3Cにおけるexperimentalの卒業すらも待つ必要がありませんので。
課題
JSオブジェクトリテラル生成が面倒
汎用的にMapからオブジェクトリテラルを作成できる手段があると良いと思いました。Web APIのoptions実現のためならMap<String, dynamic>
からオブジェクトリテラルが作成できるだけでもだいぶ違うと思います。
いやいや、Dartにおける名前付きパラメタ群をJSにける最終パラメタ(オブジェクトリテラルを期待)にすれば良いんではないでしょうか?
package:web
で警告が出ない
Web向けプラグイン以外でdart:html
をインポートすると警告が出ていましたが、package:web
にマイグレーションしたところ警告が出なくなりました。
まあ、自作アプリはWeb専用なので警告は出ないほうが良いのですが...