😊

Dart 3.3がFlutter Webに与える影響について

2024/02/21に公開4

2024年2月16日、Flutter 3.19.0がリリースされました。

https://medium.com/flutter/whats-new-in-flutter-3-19-58b1aae242d2

同時に、Dart 3.3.0がリリースされています。

https://medium.com/dartlang/dart-3-3-325bf2bf6c13

この、Dart 3.3の目玉機能がextension typeです。この機能は、ぱっと見だと「どこで利用するんだろう」という印象のある機能なのですが[1]、Flutter Webにおいては非常に大きな影響を与える機能となっています。
以下、公式の紹介テキストです。

Evolving JavaScript Interop

Dart 3.3 introduces a new model for interoperating with JavaScript libraries and the web. It starts with a new set of APIs for interacting with JavaScript: the dart:js_interop library. Now Dart developers have access to a typed API for interacting with JavaScript. This API clearly defines the boundary between the two languages with static enforcement. This eliminates an entire class of issues before compile time. In addition to new APIs to access JavaScript code, Dart now includes a new model for representing JavaScript types in Dart using extension types.

import 'dart:js_interop';

/// Represents the `console` browser API.
extension type MyConsole(JSObject _) implements JSObject {
  external void log(JSAny? value);
  external void debug(JSAny? value);
  external void info(JSAny? value);
  external void warn(JSAny? value);
}

この文章が紹介している内容を、ほぼ紹介し直すのが本記事の半分ぐらいの箇所になります。このため、英文から「なるほど、そういうことか」となった方は、ザザッとわかった範囲を読み飛ばしてもらえればと。たぶん、「なるほど打倒TypeScriptなのか」となった方は、読み飛ばしてもらってOKです。

Dart 3.3がFlutter Webに与える影響

筆者が感じている、Dart 3.3がFlutter Webに与える影響は次の3点です。

  1. JavaScriptのAPIに、Dartからアクセスするのが簡単に型安全になる
  2. JavaScriptのAPIの型をDartの世界に持ってきた時に、Dartの型としてGenericsが活用できるようになる
  3. package:jsのような不安定なパッケージから、dart:js_interopへの移行が起きる

今回のDart 3.3はJavaScript APIやJavaScriptのパッケージを、Dartで活用できるようになる大きな一歩です。10年前、もしくは5年前にあれば、色々と今と状況が異なっていたように思います。それぐらい、大きな一歩です。
大きな一歩である反動と言ってもいいのかもしれませんが、Dart 3.3の導入とdart:js_interopの大幅な更新は、既存のFlutter Webアプリケーションに破壊的な変更をもたらしています。影響を見ると、『Flutter Webのstable化と同時であれば許容できたかな……』と感じる類のものです。Flutter Webを採用している、もしくはこれから採用しようとしている方は、今回の変更とその影響を抑えておく必要があります。

大きな変更が起きた理由

先ほど引用したように、extension typeによるJavaScript APIとの連携機能が強化されました。Dart 3.3で導入されたextension typeを利用するためには、Dart 3.3以上が必要になります。このため、extension typeを利用するパッケージは、Dart 3.3以上を要求するようになります。
ここで問題となるのは、dart:js_interopextension typeを前提とする状態に更新されたことです。これにより、Dart 3.2以下向けのdart:js_interopとDart 3.3以上向けのdart:js_interopが存在することになります。

https://api.flutter.dev/flutter/dart-js_interop/dart-js_interop-library.html

多くの場合、Dartの発展に伴って、特定のDartのバージョン以上が求められることは問題視されません。特定のパッケージまでは旧バージョン、特定のバージョンからは新バージョン向けとなることで、漸進的なアップデートが実現されるためです。
最近では、Dart 3.0以上を要求するライブラリが存在します。record typeswitch式などを、パッケージの内部で利用しているケースです。これらは、パッケージの更新を工夫することで、開発しているアプリケーションで柔軟にバージョンアップを取り込むことができます。


dart:js_interopで起きていることは、これらのアップデートの流れと、異なるものになります。
Dart 3.2向けに書かれた一部のFlutter Web向けライブラリFlutter 3.19.0で動作しないDart 3.3向けに書かれたFlutter Web向けパッケージFlutter 3.16.9以下で動作しない、この2つの状況が発生します。厳密には、前者はDart 3.2以下でdart:js_interopを利用していた場合、後者はDart 3.3以上でdart:js_interopを利用している場合です。[2]

また、この影響はFlutter Web向けのパッケージのみではありません。Flutter Web向けにDart 3.3以上のサポートを行うと、パッケージ全体がDart 3.3以上を求めることになります。結果として、Webをサポートしているライブラリを利用している、AndroidやiOSのみのプロジェクトに影響が発生することになります。
適切にバージョニングの管理がなされていたり、アプリケーションで利用されているFlutterのバージョンが最新に追従できていれば問題はありませんが、そうでない場合には想定していなかった問題が発生することとなります。

dart:js_interopのbreaking changeとその影響

httpはFlutter 3.19.0のリリース直後に、v1.2.1をリリースしています。この内容を確認してみましょう。

https://pub.dev/packages/http/changelog#121

web^0.5.0に、Dartが^3.3.0となったことが、Changelogに明記されています。
PRを見ると、サクッと最新のwebに更新していることがわかります。

https://github.com/dart-lang/http/pull/1132

httpは、非常に安定的に更新がなされているパッケージです。今回サクッと更新がされた理由、つまりDart 3.3でdart:js_interopがどう変化したのかと、それがwebにどう影響しているかを見てみましょう。


まず、Dart 3.3とDart 3.2のdart:js_interopの違いを把握しましょう。手前味噌ではありますが、先日スクラップにまとめたものがあるので、以下の説明だと不十分だと思う方はこちらも参照してください。

https://zenn.dev/koji_1009/scraps/b7ed67932982e6

重要なのはdart:js_interopはDart 3.3でJSAnyJSPromiseといった、基本的な型に根本的な変化が入っている点です。
Dart 3.2の実装から確認します。js_interopで定義されているJSAnyJSPromiseは、sdkの内部パッケージであるjs_typesの型に別名をつけているだけです。

https://github.com/dart-lang/sdk/blob/3.2.0/sdk/lib/js_interop/js_interop.dart

型の宣言元を見に行くと、@JS@staticInteropの2つのアノテーションをつけた、中身がほぼ空のクラス定義であることがわかります。単なるクラス定義であるため、とりあえずDartの型とJavaScriptの型を結びつけた状態です。
これらの構文はPast JS interopで紹介されているのですが、今後利用することはないので、おおよそ「JavaScriptの型とDartの方を結びつけるおまじない」と認識しておけばOKです。

https://github.com/dart-lang/sdk/blob/3.2.0/sdk/lib/_internal/js_shared/lib/js_types.dart

繰り返しになりますが、Dart 3.2までのJS関連のクラスは、JSAnyimplementsしたクラス群です。型定義は最低限で、例えばJSArrayは配列として保持するオブジェクトの型を指定することはできません。


Dart 3.3からはextension typeが利用できるようになります。extension typeは、"Extension types introduce zero-cost wrappers for types."と公式Mediumで紹介されているように、型と対応するメソッドを宣言できます。
よって、JSPromiseJSArray<T>を活用した定義に更新されています。

https://github.com/dart-lang/sdk/blob/3.3.0/sdk/lib/js_interop/js_interop.dart

以上よりDart 3.2とDart 3.3の型定義は別物です。Dart 3.2を前提にJSPromiseを利用している場合、Dart 3.3以降でビルドができたとしても、想定されない動作となることがあります。逆に、Dart 3.3向けに書かれたJSPromiseJSArrayは、Dart 3.2以下でビルドができないことになります。
dart:js_interopはDart sdkに含まれているので、コンパイルするDartのバージョンに依存します。Dart 3.3に合わせて、dart:js_interopにbreaking changeが入ったと言えるでしょう。


以上を踏まえた上で、webのv0.3.0とv0.5.0の差分を見てみましょう。JavaScriptの型を利用している箇所に、変更が入っていることが確認できます。
引用すると大変なので、以下のブロックを適当にスクロールしてみてください。

https://github.com/dart-lang/web/compare/v0.3.0...v0.5.0

以上の理由で、web^0.5.0に設定すると同時に、Dartを^3.3.0に設定しているということです。
dart:js_interopをDart 3.3以上のものに変えた、ということですね。言い換えると、http^1.2.1以上は、Dart 3.2以下と混在させることができなくなっています。これはパッケージ全体に影響を与えているので、動作に影響があるのはWebですが、ビルド環境としてはAndroidやiOSにも影響が及んでいます。

必要になる対応

当たり前ではあるのですが、Dartによるロジックのみを持つパッケージは、今回の変更の影響を受けません。例えばcryptoは、Dependencyにflutterが入っていない、純粋なDartのパッケージです。このため、webdart:js_interopを利用していません。


アプリケーション内でwebdart:js_interopを利用している場合、Dart 3.3以上が必須です。lintを厳しく設定している場合には、Genericsが利用できるのに型を指定していないということで、ワーニングが大量に出るはずです。それらを潰しつつ、@staticInteropを書いていた箇所を、ひとつひとつextension typeに書き換えていく必要があります。
この対応自体は、そこまで難しくないでしょう。

難しいのは、パッケージ経由でwebdart:js_interopを利用している箇所です。

先ほど紹介したようにhttpはFlutter Web向けにwebを利用しています。このため、httpパッケージを利用しているアプリケーションは、今回の変更に対応する必要があります。なお、dioは大丈夫かもしれません。というのも、diowebdart:js_interopも利用していなさそうだからです。
このように、パッケージがwebdart:js_interopを利用しているかどうかを確認しなければ、アプリケーションに影響があるかどうかを判断することができません。例えばfirebase_core_webは、dart:js_interopも利用するようになっています。

https://github.com/firebase/flutterfire/pull/12239

影響のまとめ

大袈裟に言えば、extension typeの導入による影響を受けないFlutter Webのアプリケーションは存在しません。この荒波を、みんなで乗り越えていく必要があります。

影響を受けるパッケージを見つけ、Dart 3.3以上をサポートするようにコードを書き、パッケージを更新しましょう。OSSのいいところはそこです。

extension typeは必要なのか

extension typeは、Flutter Webにとって必要な機能です。
筆者は2020年ごろからFlutter Web向けにJavaScriptのライブラリをパースするパッケージを開発していますが、パッケージ開発中は(何もわからない……)となっていました。特にpackage:jsを利用するケースなどは、確認できるソースコードもなく、またおまじないに対しておまじないを唱えるような体験でした。

対して、extension typeによるJavaScritp APIへのアクセスは、非常に明快な手法です。
Flutter Webを採用した際に、なんらかのJavaScriptライブラリと連携が必要なるケースにおいて、一般的な開発者が実装できるようになったと言えます。[3]

またJavaScriptに対してDartの型システムを活用できるようになる点、特にコンパイル時に型のチェックが実現できるようになる点は、Dartを使う理由を与えてくれます。Dartの型システム上でJavaScript APIを呼び出せるようになることは、DartからJavaScriptの資産を活用する道を開いた、と言ってもいいのではないでしょうか。[4]

おわりに

以上、Dart 3.3がFlutter Webに与える影響の紹介でした。

今回の対応はFlutter Webをstable版としながらも、大規模に壊してきたな……? という思いもあります。筆者はFlutter Webを利用している方なので、Flutter 3.19.0を利用したWebページは1〜2ヶ月安定しないものとして、対応を進める予定です。
たまたま、今から新規にFlutter Webでアプリケーションを作ろうとしている方がいれば、開発コミュニティに「状況どうなの?」と投げかけてみることをお勧めします。

いいように捉えすぎなのかもしれませんが、今回の変更は、Flutter Webに対しての本気度が高いからこそ起きていると言えます。長い目で見れば、extension typeの導入は、Flutter Webの開発をより快適にするでしょう。
悪い面と同時にいい面も見れれば、発生するであろう混乱をポジティブに乗り切れるのではないかな、というのが本記事執筆のモチベーションです。一緒に頑張りましょう。

脚注
  1. https://github.com/dart-lang/language/issues/83#issuecomment-1954471407 などが例になりそうです ↩︎

  2. 当然ではありますが、dart:js_interopを利用していないパッケージには影響はありません。 ↩︎

  3. この記事を読んでくれたあなたは、dart:js_interopを使って書けるようになったはずです ↩︎

  4. 言い過ぎかもしれません ↩︎

GitHubで編集を提案

Discussion

Cat-sushiCat-sushi

Wasmにも関連している様ですね。
JSにコンパイルしなくなるので、Dartの世界とJSの世界を透過的に繋ぐ必要性が高まったと。

koji-1009koji-1009

下記の箇所にある、WASM向けビルドの問題点の話ですね。

https://docs.flutter.dev/platform-integration/web/wasm#requires-preview-js-interop-to-access-browser-and-js-apis

(おそらく将来消えてしまうので、引用します。)

Requires preview JS-interop to access browser and JS APIs

To support Wasm, Dart is shifting how it targets browser and JavaScript APIs. This shift prevents Dart code that uses dart:html or package:js from compiling to Wasm. Most platform-specific packages, such as package:url_launcher, use these libraries. As a result, they are currently incompatible with Wasm support in Flutter.

ご指摘のとおり、dart:js_interoppackage:jsを置き換えているのは、WASMサポートが進む影響と理解しています。
Flutter WebのWASMサポートは、WASMをサポートしているブラウザにはWASM版を、そうでないブラウザにはJS版を、それぞれ配布する仕組みだったかなと[1]。なので、1つのDartのコードで2つの結果を出力できる仕組みが必要になる……はず……。

extension typeとJavaScript APIのiteropが進んだこと、Dart 3.3でWASM向けのコンパイルが進んだことと、DOMの操作がpackage:web にまとまったこと。これら3つが同時にあることで、Flutter Webの世界が大きく前に進んだと認識してますー!

脚注
  1. どこで読んだかパッと思い出せないのですが…… ↩︎