Flutter for Webですでに公開しているFlutterアプリをWebに対応させてみた

#はじめに
この記事は、すでにFlutterで開発してiOS・Androidにて公開していることアプリを、Flutter for Webを使ってWeb対応させた時の経験や気付きなどをまとめたものです。
なお、Flutter for Webはまだbetaであり、製品として公開することは公式に推奨されていません。
ご承知の上でご覧ください。

#Web対応させたアプリについて
###概要
今回Webに対応させるアプリはcubookという個人開発のアプリです。
ボーイスカウトのカブスカウトという小学校3年生〜5年生の子供とその指導者向けの、カブブックと呼ばれる教科書の画像や動画を含めた進捗の共有や管理を行うアプリになります。
[ホームページ](https://sites.google.com/view/cubookinfo/)
[GitHub](https://github.com/yamamotokotaro/cubook)
###アーキテクチャ
![スクリーンショット 2020-12-16 18.35.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/148759/b0c7aaa6-a2b1-5cfe-851d-feb026b50a78.png)


#FlutterでWebアプリを動かす手順
チャンネルをベータにして、FlutterのWebを有効にします

```sh
$ flutter channel beta
$ flutter upgrade
$ flutter config --enable-web
```
有効になると、デバイス一覧にChromeが表示されるようになります

```sh
$ flutter devices
1 connected device:

Chrome (web) • chrome • web-javascript • Google Chrome 87.0.4280.88
```
プロジェクトのルートディレクトリで以下のコマンドを実行し、Web対応にします

```sh
$ flutter create .
```
Chrome上でアプリを実行します

```sh

$ flutter run -d chrome
```

#Web対応のために修正したコード
プラグインなどによって、コードの書き方を変える必要などが出てきます。
###Cloud Storage
Upload Taskの作成にはアプリとWebで処理を切り替える必要があるので要注意
以下がサンプルコードです

```dart
import 'package:firebase_storage/firebase_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/foundation.dart' show kIsWeb;

//画像を取得する
final file = await ImagePicker().getImage(source: ImageSource.gallery, imageQuality: 50);

//保存するディレクトリとファイル名の指定
int timestamp = DateTime.now().millisecondsSinceEpoch;
String subDirectoryName = 'hogehoge';
final ref = FirebaseStorage.instance
    .ref()
    .child(subDirectoryName)
    .child('${timestamp}');

//Upload Taskの作成
UploadTask uploadTask;
if (kIsWeb) {
  uploadTask = ref.putData(await file.readAsBytes()); //Webでの処理
} else {
  uploadTask = ref.putFile(File(file.path)); //アプリでの処理
}

//アップロード
dynamic snapshot = await Future.value(uploadTask);
```
### Cloud Functions
以下のように、httpリクエストでCloud Functionsを呼び出している場合、CORSエラーが発生してしまいます。

```dart
String url = "https://asia-northeast1-xxxx.cloudfunctions.net/somefunction";
Map<String, String> headers = {'content-type': 'application/json'};
String body = json.encode(<String, dynamic>{
  'hoge': 'hoge',
});
http.Response resp = await http.post(url, headers: headers, body: body);
```

関数をonRequestからonCallに変更してプラグインで呼び出すようにするのが一番手っ取り早いです。

```dart
HttpsCallable callable = FirebaseFunctions.instanceFor(region: 'asia-northeast1').httpsCallable('somefunction',options: HttpsCallableOptions(timeout: Duration(seconds: 5),));
final results = await callable(<String, String>{
  'hoge': 'hoge',
});
String resp = results.data;
```
###ImagePicker
ImagePickerで放置していたMigrationの箇所でエラーが起きてしまっていたので、

```dart

File image = await ImagePicker.pickImage(source: ImageSource.gallery, imageQuality: 50);
```
から

```dart

final image = await ImagePicker().getImage(source: ImageSource.gallery, imageQuality: 50);
```
に変更

###showLicensePage
package_infoがWebに対応していないため、showLicensePageを読み出す際にapplicationVersionの指定を工夫する必要があります。
今回は以下のように対応しました。

```dart

ListTile(
    leading: Icon(Icons.list),
    title: Text('ライセンスを表示'),
    onTap: () => kIsWeb
        ? showLicensePage(
            context: context,
            applicationName: 'cubook',
            applicationVersion: 'web',
            applicationLegalese: '©︎ 2020 山本虎太郎',
          )
        : showLicensePage(
              context: context,
              applicationName: 'cubook',
              applicationVersion: packageInfo.version,
              applicationLegalese: '©︎ 2020 山本虎太郎',
     ),
),
```
###AdMob
公式とサードパーティのプラグインを使っていたが、全てWebに非対応。
駄菓子が買えるかくらいの収益しか入っていなかったのでこれを機に削除。(大赤字)
###Firebase UI
アプリの認証では、Firebase UIというネイティブアプリ向けにGoogleが事前に用意したログインフローを適用してきました。Web向けにも用意はされていますが、Flutter for Webにはアプリ版のMethod Channelのような仕組みが無いため表示することが技術的に不可能です。そのため、Webでのログイン画面は自分で新たに作成しました。（Googleさん、Flutter用のFirebase UI作ってくれるとすごーく嬉しいです）

#Webに対応していなかったプラグイン（執筆時点）
###AdMob
このアプリの場合は機能を消してしまいました
###Firebase messaging
現状Web版で通知は送れないです

###Firebase Clashlytics
現状Web版でクラッシュ情報の収集はできません
###open_file
データをエクセルに出力したファイルを開くために使っていました。
pub.devを見るとWebに対応しているような記述があるのですが、このアプリでは動作していません。

#雑感
当初Web対応には２・３週間はかかることを覚悟していましたが、着手してから一週間も経たずにある程度動くところまでできたのは、さすがFlutterと感じました。最初にも触れた通りFlutter for Webはまだbetaですが、公式プラグインを中心に段々と対応してきています。パフォーマンスにおいては普通のWebアプリやアプリとして動かしたFlutterアプリより劣ってはいます。とはいえ、まだbetaであることや、何より大部分を共通のコードで動かせているので、今後より改善されると期待しています。

#参照
[Building a web application with Flutter](https://flutter.dev/docs/get-started/web)
[pub.dev](pub.dev)