🔥

Flutterの新しいjs_interopまとめ

に公開

ついに、package:jsがdiscontinuedとなり、dart:jsもあまり推奨されずjs_interopにそろそろ移行した方が良さそうな感じになりました。
しかし、新しいことで、情報が少ない上に、Flutter webを使っているという少ない人口の上に、ネイティブのコードを扱うという非常に情報が見つかりにくいので、わかったことを、まとめておきます。

そもそも、js_interopって

dart:jspackage:jsは、どちらもflutterwasmビルドに対応していません。
まず、js_interopという名前は、JSinteroperabilityという言葉からできていますが、interoperabilityという言葉を初めて聴いた人も多いのではないでしょうか。interoperabilityは「相互互換性」という意味で、dartjavascriptwasmの両方を同じページで共存させ、相互の橋渡しをするという意味だと思われます。

難しいことは考えず、新しい「DartとJSをつなぐ道具」と考えれば大丈夫です。

使い方

Javascriptという非厳密な言語との橋渡しで、型的に安全ではないコードになりがちなので、少しずつ試しながら、再確認しながら、定義しないと、後でやってみてうまくいかなくて、エラーの内容が空っぽだったり意味不明だったり、スタックトレースが難読化されていたりと大変なことになるかもしれません。

インポート

import 'dart:js_interop';

最近、以下のように書いてエラーになって数時間悩んだので、注意

import 'dart:js_interop/js_interop.dart';

Webなのに、こんなことを言われてしまいます。

Dart library 'dart:js_interop/js_interop.dart' is not available on this platform.

Dart => JS (JS(index.htmlなど)の関数を、Dartから呼び出す)

JSの関数をDartから呼び出すときは、まずDartでJSの関数やクラスの設計図を定義します。このことで、Dartの中では型安全に使うことができます。ただ、もちろん、その設計図が間違っていたら、型安全でなくなってしまうので、JS側のAPIの仕様を確認してから書いた方が良さそうですね。

関数

というわけで、まず、Window.alertの橋渡しを定義してみましょう。

import 'dart:js_interop';

("alert")
external void dartAlert(String alert); // 厳密には文字列に変換できるオブジェクトを受け入れるが、APIがString「も」許容すれば大丈夫

void main() {
    dartAlert("Hello World!");
}

externalというのは、名前の通り外部のどこかで定義するからここでは書かない、という意味です。まあ、js_interopぐらいでしかあまり使われない気もします。
@JS、のようなものは、メタデータ/アノテーションと呼ばれるものです。js_interopに限らず、例えば@overrideや、@deprecatedなどのアノテーションがあります。Pythonのデコレーターと違い、直接関数の動きを変えたりせず、ある意味ただのマークとしての意味を果たします。
@JSexternalで定義された関数が実行されたときにJSの関数を呼び出すのが使命で、引数を省略すると、dartAlertという関数をJSで実行します。命名規則がJSと違うときなどに引数を使うと便利です。
https://dart.dev/language/metadata

クラス

私が調べた限りでは、2通りの定義のやり方があります。
なんか、私にはエラー文の方が分かりやすかったので、一応載せておきます。

エラー文
compileNewDDC
main.dart:4:15: Error: External JS interop member contains invalid types in its function signature: 'void Function(*dynamic*)'.
Use one of these valid types instead: JS types from 'dart:js_interop', ExternalDartReference, void, bool, num, double, int, String, extension types that erase to one of these types, '@staticInterop' types, 'dart:html' types when compiling to JS, or a type parameter that is a subtype of a valid non-primitive type.
external void jsLog(dynamic message);
              ^

https://dartpad.dev/?null_safety=true&id=732668852d92d610a7e1a8ac31ba86bd

extension

公式の解説がわかりやすいので(英語ですが)、それもぜひ読んでみて下さい。

extension type Person._(JSObject _) implements JSObject {
  external Person(String firstName, String lastName, int age);
  external factory Person.onlyName(String firstName, String lastName);

  external int name;
  external int age;
  external bool getFullName();

  bool getIsAdult() => age >= adultThreshold;

  external static Person adultThreshold; // 18
  external static Person getAgeDifference(Person person1, Person person2);
}

@JS

()

class Person {
  external factory Person(String firstName, String lastName, int age);

  external factory Person.onlyName(String firstName, String lastName);
}

extension PersonExtension on Person {
  external String get firstName;
  external String get lastName;
  external num get age;
  external bool getFullName();
}

("Person.adultThreshold")
external int personAdultThreshold;

("Person.getAgeDifference")
external int personGetAgeDifference(Person person1, Person person2);

@staticInteropは必須で、静的なときに、@JS()と並べて@staticInteropを書くことができ、プロパティなどはextensionとして定義できます。
ちなみに、@staticInteropがないと、こうなります。

The '@JS' annotation from 'dart:js_interop' can only be used for static interop, either through extension types or '@staticInterop' classes.

また、interfaceとかで、Dartから「クラス」にはアクセスできず、Objectになっていた場合、@JS``@staticInteropと並べて@anonymousが使えます。

使い分けは... 私には分かりませんでした...

Dart内の変換

toJS

おそらくextensionで定義されているので、importを忘れずに(忘れると存在しないというようなエラーになりドキュメントを読み直すことになるかもしれません)。

print(5.toJS); // JSNumber

toDart

toJSの反対

print(5.toJS.toDart); // 5

jsfunction

dart:jsallowInteropの代わりに、js_interopではこれを使用します。

()
void callback(int param1, String param2) {
    return "$param2$param1";
}

良い習慣かどうかわかりませんが... ラッパーを作っても機能します。プラグインとかを開発するときはこのような方法がないと大変ですね。

class SuccessCallbackWrapper {
  const SuccessCallbackWrapper(this.callback);
  final SuccessCallback callback;
  ()
  void successCallback(JSString decodedText, Html5QrcodeResult result) =>
      callback(decodedText.toDart, result);
}

まとめ

js_interopは、Flutter WebやDartでJavaScriptと安全かつ柔軟にやりとりするための新しい標準的な方法です。従来のdart:jspackage:jsからの移行が推奨されており、型安全性や将来性の面でもメリットがあります。まだ情報が少ない部分もありますが、公式ドキュメントやサンプルを参考にしながら、少しずつ試していくのが良さそうです。まだまだアップデートが増えていきそうなので、要チェックですね!
https://dart.dev/interop/js-interop

Discussion