【個人開発】Flutter webでLINEグルチャメンバーの子どもの顔と名前がわかる「かおなまいっち」というサービスを作った話

# はじめに
先日Flutter webで[かおなまいっち](https://kaonama-itch.com/)というサービスをリリースしました。
Flutter webを実サービスでつかった感想など交えながら開発全体を振り返ります。

## サービス概要
かおなまいっちは以下の機能を持つサービスです。

* LINEグループのメンバーの顔と名前を確認できる
* さらにメンバーの子どもや他のお迎えなどにくる家族の顔と名前も確認できる

私自身困っていた体験をもとに、このマンガのように利用されるサービスです。
|1ページ|2ページ|3ページ|4ページ|
|---|---|---|---|
|![１ページ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18273/c863dff0-73b9-ba88-185f-c8308cabd918.png)|![２ページ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18273/8579227c-1093-9dcd-737e-59146c7e8fb9.png)|![３ページ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18273/80954369-21ce-d7b8-2f33-eb39c3f4fd21.png)|![４ページ.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18273/b6337cdb-682e-6b6c-b151-99b01ed46e22.png)|

# タイムライン
時系列では以下のような流れです。

|時期|やったこと|
|---|---|
|2021/02/8|アイデアを思いつく|
|〜2021/03/19| LINE APIとにらめっこ。 APIが提供する機能で、やりたいことが実現できそうか考える→たぶんできそう <br> 全体のシステム構成ふわっと考える|
|2021/03/19|Flutterで作ることにする|
|〜2021/10|コア機能実装|
|〜2022/01|デザイン反映&ブラッシュアップ|
|2022/02/01|正式リリース|


# 技術選定
## フロントエンド
私は普段モバイルアプリ(iOS/Android/Flutter)をよく開発しています。
Webは管理コンソール用にAngularJsを使う程度。
しかし多くのユーザーに使ってもらうために、LINE上で動くLIFFアプリ(つまりWebアプリ)として開発する必要がありました。

まずVue,React,Angularあたりを考えていたんですが、以下の理由でFlutter webを採用することにしました。
* Angular以外はTypeScriptがまだ使いにくそう(素のjavascriptは触りたくない)
* materialみたいなライブラリあるとはいえ、CSSの微調整とかちょっとだるいな(←時間あまり取れないから本格的にキャッチアップもしにくい)

Flutter web使うか決めるにあたって事前に以下の技術検証をしておきました。
どちらもできることがわかったので、まぁ技術的には可能だろうとFlutter webで作る方向に舵をとりました。
* 画像選択できるか
* jsライブラリ使えるか(LINE API使うため)

## バックエンド
AWS/Firebaseの2択ですが、個人開発では手軽さ&スピード重視でFirebaseを使うことにしているので、Firebaseで実装することにしました。
Cloud Functionも使っているのでTypeScriptも使っています。

## デザイン
基本はFlutterのマテリアルデザインでコア機能を作ることにしました。
ただ今回はより多くの人に使ってもらえるように、コア機能実装後にデザインはプロに発注することにしました。

# 設計
## 画面設計
初期の画面設計はこんな感じで考えていました。
時間的にも凝った画面遷移図を作ることはしませんでした。
デザインの調整は入りましたが、最終的にはだいたいこれ通りになりました。
![画面 3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/18273/8d344b19-2765-47d7-eb1c-4435503f920a.jpeg)

## インフラ
開発効率を重視しFirebaseにまるっとおまかせです。
主な利用サービスは以下です。
* Firestore
* Storage
* Hosting
* Authentication

## DB設計
Firestoreを使いました。
これまでの経験からスキーマレスだからといって、documentにオブジェクトや配列をネストしすぎると辛いしことがわかっています。
またモデル化できないような雑多なデータを作るもの辛いです。
よって以下を意識してデータ設計しました。
* document内にネストされたオブジェクトは作らない
* ↑を実現するためにコストかかってもサブコレクションを使う(DartもJsのPromise.all相当の機能があるので並列化も楽)
* DartでもTypeScriptでもモデル定義できるようにデータ構造を定義する
* いつかマイグレーションの必要が出たときのために、各documentにデータ構造バージョンを付与する

開発初期にふわっとFirestoreのcollectionのパスと、documentのプロパティをイメージしました。
しかしその後LINE APIを調べていくうちに、実装しやすいように変わっていきました。
結局、実装しながらテーブル設計していくような進め方になりました。

### セキュリティルール
セキュリティルールのテストはFirestoreエミュレータを使って行いました。
https://firebase.google.com/docs/firestore/security/test-rules-emulator?hl=ja

# 開発をすすめて
## 認証
認証が必要 かつ LINEユーザーのみ使えるサービス なので、LINEログインでFirebaseのカスタムAuthenticationすることにしました。
基本的に以下の紹介どおりですが、WebでLINEアクセストークン取得し、バックエンドに送信して、カスタムトークンに変換して、カスタムトークンでFirebaseの認証するので、ちょっと時間かかる感じがあります。

https://developers-jp.googleblog.com/2016/11/authenticate-your-firebase-users-with-line-login.html

## Flutter webとjsライブラリ
LINEログインするために[LIFF SDK](https://developers.line.biz/ja/reference/liff/)を使う必要がありました。LIFF SDKはJavascriptライブラリとして提供されているので、FlutterからJavascriptのコードを呼び出す必要がありました。
具体的には以下のようなJavascript-Dart間の紐付けを手動で記述していきました。
今回はJavascriptライブラリへの依存が少なかったので、数が増えたり複雑になるとしんどそうです。

```dart
// jsのliffをDartのLiffクラスとして宣言
@JS('liff')
class Liff {
　　　　// jsのliff.login関数をDartのLiff.login関数として宣言
  @JS('login')
  external static login(LiffLoginParam param);
}

// login関数に渡すパラメタをクラスとして宣言
@anonymous
@JS()
class LiffInitParam {
  external factory LiffInitParam({String liffId});
}
```
https://medium.com/flutter-community/using-javascript-code-in-flutter-web-903de54a2000

## 画像リサイズ
[image](https://pub.dev/packages/image) でのCROP/RESIZE処理が重く、(たぶんシングルスレッドゆえ)インジケーターもリサイズが終わるまで固まってしまう問題に悩みました。
iOS,Androidだったら[flutter_image_native](https://pub.dev/packages/flutter_native_image)が使えるのですが、Webには該当ライブラリはありませんでした。
結局ここでは以下のようなコードでhtmlのcanvasへの描画を経由して高速にクロップやリサイズを行いました。

iOS/Androidで使えていたライブラリがWebでは使えないこともあるので注意が必要です。

```dart
  Future<Uint8List?> _cropImageWeb(Uint8List image, String mimeType, int left, int top, int size) async{
    String jpg64 = base64Encode(image);
    html.ImageElement myImageElement = html.ImageElement();
    myImageElement.src = 'data:$mimeType;base64,$jpg64';
    await myImageElement.onLoad.first; // allow time for browser to render
    html.CanvasElement myCanvas = html.CanvasElement(width: 512, height: 512);
    html.CanvasRenderingContext2D ctx = myCanvas.context2D;

    // ctx.drawImageScaled(myImageElement, 0, 0, size, size);
    ctx.drawImageToRect(myImageElement, html.Rectangle(0,0,512,512), sourceRect:html.Rectangle(left,top,size,size));

    return _getBlobData(await myCanvas.toBlob(mimeType));
  }
```
https://github.com/brendan-duncan/image/issues/336


## ナビゲーション
アプリだと基本的にはアプリ起動時に表示される画面が一つあり、他の画面へは順番に移動していきます。
しかしWebでは以下のような要件が自然と発生します。

* `/articles/1`のように、URLで指定されたページを直接開く必要がある
* 認証済みユーザーしかアクセスできないページをブロック

しかしこのあたりを上手く実現するいい方法が見つかりませんでした。
結局MaterialApp初期化のプロパティonGenerateRouteに分岐を沢山記述することで今回は対応しました。
https://qiita.com/sekitaka_1214/items/7c2ad34b6c2d042e907c

実装当時は知らなかったが[go_router](https://pub.dev/packages/go_router)あたりを使えばいい感じに実現できるんだろうか？

## 広告
広告は辛いです。
調べた限り簡単な方法でFlutter webで広告を表示する方法は無さそう。
技術検証レベルではアフィリエイトなどの広告表示用のタグを、Flutter上からhtmlタグを生成し表示することまではできました。
Adsenseやi-mobileは審査に落ちて技術検証すらできませんでした。

### 審査
adsenseとi-mobileは審査落ちた。
ログインしないと実際の機能が使えないのが問題なんだろうか。AppStoreのレビューみたいに返信して説明したいんですが、問い合わせ窓口がなかった。
どなたかこういった会員専用Webに広告を表示するノウハウお持ちの方いたら助けてほしいです。※有償

## 認証済みアカウント
Flutter関係ないですが、LINEの認証済みアカウントの審査落ち続けました。
認証済みアカウントとは
資本金1円なのがダメなのか、サービス内容がダメなのか、申請内容がダメなのか何もわからず。。。
折を見て再申請を試みようとは思ってます。
AppStoreみたいに少しでもやり取りする口があるとありがたいのだが。。

ちなみに広告主としてのLINE広告アカウントも審査落ちました。書いてて悲しくなってきた。


## Flutter的なアーキテクチャ
アーキテクチャ的な視点では以下のように作っています。
1画面1Stateという構成が気に入っています。 (書いてないけど)View周りのテストまでできそうな作りになるので。

* freezedとriverpodを使っています
* 1画面1State用モデルを定義してStateNotifierProviderでStateを反映しています
* 永続的データへのアクセスはRepository層を経由してアクセスするようにしています
* Firestoreのデータはmodel定義して使っています

riverpodは便利でした。でもある程度節度を持って使わないとわかりにくくなってしまいそうなリスクもあるなと感じた。
開発速度面では特にメリットがありそう。

# Flutter webどうだった？
全体的な感想としてはざっとこのような感じました。
もう少し成熟するとだいぶ楽に開発できるのではないかと思います。

## Flutter webのいい点
* マテリアルデザインはアプリと同様に動いている
* CSSから完全に開放された(←個人的にはこれが最も大きいメリット)
* ↑と関連するけどwidgetの独立性が嬉しい(DOM&CSSだとなかなか感じなかった)
* 素のJavascriptよりはDartが使いやすい。(TypeScriptとの比較だと同点くらいの印象)
## Flutter webの辛い点
* webに対応してないライブラリもある
* ↑のせいもあるけど、完全にモバイルアプリとWebでワンソースかというと、それはちょっと難しそうな印象
* アプリのホットリロードがきくほどの爆速開発ではない→一度indexというか初期ページに戻ってしまう(少しずつ改善されている感じはある)
## また個人でWebアプリ作るならFlutter使う？
* 要件を実現できて、広告を表示する必要がなければ使う
## チームでWebアプリ作るならFlutter使う？
* メンバーが既にFlutter使えていて、Flutterへのモチベーションがあるなら使う
* それ以外の場合は使わない

# サービス買いたい？
このサービスを購入したいという方はDMにてご連絡ください。
https://twitter.com/sekitaka_1214

# SpecialThanks
* [commew](https://commew.net/)のレビュー会というイベントでアイデアをブラッシュアップできました

# おわりに
たのむ！みんな使ってくれ！！
グループに導入する敷居が少し高いのかもしれないが、便利だし助かる人多いと思うんだ！