FlutterKaigi 2023 ホームページへコントリビュートしました

本記事は、**[YUMEMI Flutter Advent Calendar 2023](https://qiita.com/advent-calendar/2023/yumemi-flutter) 5日目の記事です**
昨日 4日目 は、同期の[@Yo2_enginner](https://qiita.com/Yo2_engineer)さんによる [ゆめみFlutterギルドのキーボードを調査してみた](https://qiita.com/Yo2_engineer/items/45998bb35e12f1d2146c) でした。

## 0. はじめに

こんにちは 株式会社ゆめみでFlutterエンジニアをしている もぐもぐ([@YumNumm](https://twitter.com/YumNumm))です。 よろしくお願いします。

2023年11月10日に開催したFlutterKaigiのWebサイト制作にコントリビュートしたので、今回はその話をしていこうと思います。


## 1. FlutterKaigiとは?

> 日本国内でFlutterをメインに扱う技術カンファレンス。
> FlutterやDartの深い知見を持つ開発者によるセッションを多数企画します。
(https://flutterkaigi.jp/2023/ より引用)


FlutterKaigi 2021からスタートしていて 今年が３年目、**初のオフライン開催**でした。


## 2. 運営参加のきっかけ

ゆめみのFlutterリードエンジニアの林さん([@K9i_apps](https://twitter.com/K9i_apps)さん)がFlutterKaigiの運営に参加されており、社内Slackでよかったらスタッフ参加しないか〜 という話を頂きました。

https://note.com/k9i/n/nb5c2cece1fbd

https://medium.com/flutterkaigi/flutterkaigi-2023-%E9%81%8B%E5%96%B6%E3%82%B9%E3%82%BF%E3%83%83%E3%83%95%E5%8B%9F%E9%9B%86%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6-3f78fa8f8e7f

ゆめみのFlutterテックリード おかやまんさん([@blendthink](https://twitter.com/blendthink)さん)の FlutterKaigi 2022での発表を見てから「いつか自分も、この場所で発表したい」という思いがありました。

それなら、その第一歩として内部のお手伝いをしてみるのも良いと思い参加させていただきました。

運営内のウェブサイトチームに所属し、FlutterKaigi公式Webサイトの制作に携わりました。

https://flutterkaigi.jp/

https://github.com/FlutterKaigi/2023


## 3. FlutterKaigi 2023 HP制作での知見

今までFlutter on the webをあまり使ったことなかったので、色々な経験をすることができました。

:::note info
- **FlutterKaigi 2023では renderにHTMLを指定しています。**
  Canvaskitは利用していません。
- **Flutter SDKは当時(2023/9頃)最新のbeta版を利用しています**
:::

### 知見1. Flutter on the webにHTML Elementの埋め込み

SEOや高速化を目的として`iframe`を用いて **HTML要素の一部としてFlutterを埋め込む** 手法は広く知られています。

https://speakerdeck.com/masumitsu/flutterwohtml-elementni-mai-meip-mu

https://github.com/Kurogoma4D/flutter_web_integration

しかし今回やりたいのは、逆です。
**Flutter on the WebのWidgetとしてHTML要素をiframeとして埋め込む**ことをしたいです。
これで、何を実現したかったかというと、**インタラクティブなマップ**です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489005/6f1cb29d-9722-d995-6d5b-d4abe53bd640.png)

Webでよく見る至って普通のマップを実装しました。

https://www.docswell.com/s/tfandkusu/NK6475-FlutterWebHtmlElement#p20

高田 晴彦さん([@tfandkusu](https://twitter.com/tfandkusu)さん)のこちらのスライドを参考に実装を進めていきました。

#### 埋め込むiframeを取ってくる
今回は東京駅で試してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489005/7b17128a-9437-1b3e-1c39-a5ad8264f588.png)

Google Mapの共有ボタンから HTMLを取得できます。

```html
<iframe
    src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3240.8280306898473!2d139.76454987665878!3d35.68123617258739!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x60188bfbd89f700b%3A0x277c49ba34ed38!2z5p2x5Lqs6aeF!5e0!3m2!1sja!2sjp!4v1701673125721!5m2!1sja!2sjp"
    width="600"
    height="450"
    style="border: 0;"
    allowfullscreen=""
    loading="lazy"
    referrerpolicy="no-referrer-when-downgrade"
></iframe>
```


#### 埋め込むWidgetを用意する

`HtmlElementView Widget`を利用します。
Flutter WebのWidget-treeにHTML要素を注入できます。
(Flutter Web以外では `UnimplementedError`になるので クロスプラットフォームで開発している場合は注意が必要です。 Web以外では WebViewを利用しましょう。)

https://api.flutter.dev/flutter/widgets/HtmlElementView-class.html

ドキュメントにも書いてある通り、HTMLの埋め込みは高価な処理を必要とするため、頻繁に埋め込みを利用したり 無駄に利用するべきではありません。
今回は、マップをFlutter Engineを使い描画するよりも HTML埋め込みを行ったほうが遥かに楽でUXも良いため 埋め込みで問題無さそうです。

```dart
class _MapWidget extends StatelessWidget {
  const _MapWidget();

  @override
  Widget build(BuildContext context) {
    final width = 600.0;
    final height = 450.0;
    const viewId = 'google-map';
    // TODO: Register HtmlElement 
    return SizedBox(
      height: height,
      width: width,
      child: const HtmlElementView(
        viewType: viewId,
      ),
    );
  }
}
```

#### `HtmlElementView`の中身を実装してあげる

`HtmlElementView`に登録した`viewId`にHtml要素を登録します。

Google Mapから取ってきた`iframe`要素を`IframeElement()`を使い書き換えていきます。

```diff_dart
+ import 'dart:ui_web' as ui;

class _MapWidget extends StatelessWidget {
  const _MapWidget();

  @override
  Widget build(BuildContext context) {
    final width = 600.0;
    final height = 450.0;
+   const url = 'https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3240.8280306898473!2d139.76454987665878!3d35.68123617258739!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x60188bfbd89f700b%3A0x277c49ba34ed38!2z5p2x5Lqs6aeF!5e0!3m2!1sja!2sjp!4v1701673125721!5m2!1sja!2sjp';
+   // ui variable is imported from dart:ui_web
+   ui.platformViewRegistry.registerViewFactory(
+      viewId,
+     (int viewId) => IFrameElement()
+        ..height = height.toString()
+        ..width = width.toString()
+        ..style.border = '0'
+        ..referrerPolicy = 'no-referrer-when-downgrade'
+        // ignore: unsafe_html
+        ..src = url,
+   );
    const viewId = 'google-map';
    // TODO: Register HtmlElement 
    return SizedBox(
      height: height,
      width: width,
      child: const HtmlElementView(
        viewType: viewId,
      ),
    );
  }
}
```

これで、HTML要素埋め込みの完成です。
実行中のHTMLを確認しても`iframe`で埋め込まれていることがわかります。ヨシ👍

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489005/4e6830da-9fd7-1217-e6b2-55220606ba6c.png)


`HtmlElementView`を利用することで、HTMLを内部に埋め込むことができました。
今回は、FlutterとHtml要素でデータのやり取りは行っていませんが、「js_interopを利用して、カウンターを同期する」みたいなこともやってみたいですね。(HTMLの子要素としてFlutterを表示、FlutterのViewの中にHTML埋め込みみたいなこともできるんですかね...? 検証してみたいです)




### 知見2. レンダリング周りで問題が残る
いちばん辛かったです。この問題は今も残っています。

#### グラデーションつき文字が正しく表示されない

|Chrome 121|Safari 17.2|
|---|---|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489005/8b790ba9-d3a1-cd72-8bd4-6850354774b2.png)|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489005/3c521916-2ee2-a8ac-519d-f9b04390ba36.png)|

(ちなみに、iPhoneやiPadではSafariでもChromeでもどちらでもバグります。背景としては、AppleがブラウザレンダリングエンジンにWebKitを使うことを強制しているからです。
PCやAndroidではBlink Engineを利用していますが、iOS/iPad版ではWebKitを利用するようになっています。 ~~**やめてくれ Appleさんよ**~~)

お分かり頂けたでしょうか。
`Access`の文字内部のグラデーションが適用されていません。

ShaderMask->Text の比較的単純な構造です。なのになぜ問題が起きるのでしょうか?

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489005/17696272-da21-0c9c-cf99-d9a778b70a11.png)

https://github.com/flutter/engine/pull/31887

過去に、HTML Rendererを利用した時にグラデーションが正しく表示されないIssueに対するPRが作成されており、修正済みなはずですが まだ再現してしまいました。


#### 解決策A: RendererをCanvaskitにする

HTMLでの描画から、Canvaskitを用いた描画に切り替えることで、描画崩れ・パフォーマンス改善を図ろうとしました。

しかし、いくつか問題がありました。

##### 1. CORSエラー

FlutterKaigi 2023では、スタッフ情報・スタッフプロフィール画像やニュース情報の取得に [NEWT CMS](https://www.newt.so/)を利用していました。
NEWT CMSでは、プロフィール画像をGoogle Cloud Storageへ保存しており、この画像をFetchする際に`CORS`(`Cross-Origin Resource Sharing`: オリジン間リソース共有)の同一オリジンポリシー(`Same-Origin Policy`)に引っかかってしまいます。
HTMLレンダラーを利用した場合、スタッフプロフィール画像は HTMLの`image`要素として描画されるため問題になりませんが、Canvaskitを利用した場合、トランスパイルしたJavaScriptからfetch APIを利用して画像が取得されます。
ここでエラーとなり、結果的に画像の描画に失敗してしまいます。

解決策として、**NEWT CMS上で画像の保存先 自前のAWS S3やGCP Storageへ変える**方法もありましたが、ここにお金をかけるべきではないと判断し、選択肢から消えました。
もう1つの解決策として、**NEWT CMS上のJSON/画像を/assetsに入れる**でした。

https://github.com/FlutterKaigi/2023/pull/126

こうして、NEWT CMSへの依存を剥がし RendererをCanvaskitに変えて完s..........

と、ここでまた問題が発生しました。


###### 2. Canvaskitに切り替えると iPhone実機でクラッシュする

**とんでもない問題が発生しました。**

Canvaskitにすると iPhone実機でクラッシュしてしまいます。
しかも、macOSからiPhoneへSafariのデバッグツールを接続しても 何もエラーは出ず、iPhoneのコンソールを確認しても何も出力されません。
ただ再読み込みが走って、数回すると 「このページは読み込めません」と表示されます。

https://github.com/flutter/flutter/issues/117164

Material 3 Demo Appでも、同様の問題が発生していたらしく、Canvas2Dを多く/大きく割り当てすぎているため だと議論されていました。

> Under some circumstances, we allocate too many canvases, or we allocate canvases that are bigger than necessary (or both!). We also had a bug that caused unnecessary canvas allocations, that was fixed in [flutter/engine#38640](https://github.com/flutter/engine/pull/38640).
> The remaining issues contributing to this crash are:
> 
> - Canvas Reuse: when we reuse a previously allocated canvas, some times we reuse a canvas that's too big for our needs.
> - Canvas<->DOM switching: the html renderer decides some times to switch from canvas mode to DOM mode, then if it switches back to canvas mode, it allocates a new canvas.
> 
> (https://github.com/flutter/flutter/issues/117164#issuecomment-1379470593)

グラデーションや`ShaderMask`を多数利用するデザインだったため、このような問題が発生してしまったものだと考えられます。

最終的に、このクラッシュする問題を解決することができず、根本の「グラデーション付き文字が正しく表示されない」問題を解決できないという悔しい終わり方となってしまいました。

(開発中のメモを置いておきます)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489005/5e57fd70-3aa6-21c5-dd53-b6e242700de7.png)




## 4. まとめ

FlutterKaigi 2023の運営(カンファレンスウェブサイトチーム)に所属し、Flutter on the webに関わる知見を増やすことができました。
最終的には、33個のPRを作成し FlutterKaigiへ貢献することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/489005/8921b616-e12d-36c1-d8ce-6d2af02d04fb.png)

FlutterKaigiという大規模なイベントの運営へ参加することで、ホームページだけでなくイベント自体がどのように組み立てられていくのかを知ることができました。

ウェブサイトチーム・関連チームの方々にはご迷惑をおかけしたかもしれませんが、開発に携われたこと大変感謝しています。本当にありがとうございました。

来年度、Flutter 2024を行う際もぜひFlutterKaigiへ貢献できるよう、技術力を磨いていきたいと思います。



## お決まり文句

### 採用

株式会社ゆめみではFlutterエンジニア以外にも様々なエンジニアを募集しています。

#### 新卒採用
現在、2024年卒以降を採用しています。

https://hrmos.co/pages/yumemi/jobs/101000002400

Flutterインターンについても検討中ですので、`ref.watch(yumemiRecruitmentProvider)`して頂ければ幸いです。

https://twitter.com/blendthink/status/1696482620111225040

#### 中途採用

先日、Flutterエンジニアの中途採用が公開されました。

https://hrmos.co/pages/yumemi/jobs/1791024548071079948

また、Flutterリードエンジニアの採用も行っています。

https://hrmos.co/pages/yumemi/jobs/1733762895981203458