Flutter でアプリを作ってみた感想


# はじめに

Flutter が本格的に流行ってきたので、勉強のため、架空のショッピングアプリの開発を Flutter を用いて開発してみました。
そこで得られた知見をまとめたいと思います。

# 開発したアプリの概要

開発するアプリとして架空の飲食店または小売店での注文アプリを作成しました。
この手のアプリの中ではマクドナルドのオーダーアプリがかなりよくできていたので、それを参考にして作成しました。

Flutter の勉強が主な目的ですが、相性の良い Firebase の機能も試してみたかったので、店舗情報、商品情報、注文履歴などのデータはアプリ内で固定値で持たずに Firestore に格納するようにしました。

ただし、商品マスタ登録、店舗マスタ登録や商品在庫といった運用の考慮どはしていません。
また、決済機能は実装対象外としました。

最終的な画面数は 10 画面。
実装期間は土日を 2 ヶ月間ほどです。

# アーキテクチャ（全体）

![a.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/a04afa46-12f5-a759-8675-10f49e847ccb.png)

# アーキテクチャ（CI/CD）

今回作ったアプリでは CI/CD はストアリリースまでではなく、開発中の内部でのテストアプリ配信を想定して構築しました。

![b.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/c30592a9-1628-f096-f326-72728a2ac4a7.png)


develop ブランチへの Push をトリガーに Codemagic 経由で App Store（TestFlight）と Google Play Console（内部テスト）に配信するところまでを作成しました。
ちなみに、いきなりこのようになったのではなく、以下の経緯を経てこのような形に落ち着きました。

- 第一形態：手動で .ipa や .apk ファイルを Firebase App Distribution にアップロードして配信
  - 手動で配信するのはダサかったので Fastlane を採用することに。
- 第二形態：コマンドラインから Fastlane を使って Firebase App Distribution で配信
  - どうせなら GitHub での Push をトリガーに配布したく CI/CD のサービスを検討。
- 第三形態：Bitrise で Fastlane を使って Firebase App Distribution で配信
  - モバイルアプリ開発における CI/CD デファクトスタンダードの Bitrise を採用。
  - Bitrise の無料枠に iOS ビルドが終わらずで断念。
- 最終形態：Codemagic で App Store（TestFlight）、Google Play Console（内部テスト）で配信
  - Flutter に特化した CI/CD サービスかつ、無料枠が多い（500分/月）の Codemagic を採用。
  - Fastlane で Firebase App Distribution にアップロードするよりも直接ストアに配布するほうが設定が楽だったので Firebase App Distribution を断念。
  - iOS は TestFlight のアプリがよくできているのでバージョン、ビルド番号ごとに別れるが、Android はメールでストアへのリンクがくるだけなので Android のテスト配信は Firebase App Distribution のほうがよさそうです。
  - 初めて使ってみた感想ですが、UI も Bitrise よりもわかりやすく、ドキュメントもしっかりしていて問題なく動いたので Flutter で CI/CD を採用するなら Codemagic がおすすめだと思います。
 
# 実装した機能

- ユーザー登録
- ログイン
  - Google Sing In
  - メールアドレス/パスワード認証 Sing In
- ログアウト
- 地図機能(Google Maps API)
  - 店舗の位置の表示
  - 現在位置の表示
- 店舗一覧表示
- 商品一覧表示
  - カテゴリーごとにタブ分け
  - 検索機能
- 商品詳細表示
  - カートへ追加
- カートの確認
  - 商品の追加/削除
- 注文詳細
  - 受け取り方法の時間/方法/決済方法の指定
- 注文履歴一覧表示
- 注文履歴詳細表示

# ディレクトリ構成

lib 以下はこのように作成しました。

```
$ cd lib
$ tree
.
├── api
│   ├── request
│   │   └── publish_order_request.dart
│   └── response
│       └── publish_response.dart
├── common
│   ├── analytics.dart
│   └── theme.dart
├── entities
│   ├── account.dart
│   ├── cart.dart
│   ├── order.dart
│   ├── payment_method.dart
│   ├── product.dart
│   ├── product_category.dart
│   ├── receive_method.dart
│   └── store.dart
├── main.dart
├── models
│   └── app_model.dart
└── screens
    ├── auth_layer
    │   ├── login_screen.dart
    │   └── sign_up_screen.dart
    ├── order_layer
    │   ├── cart_screen.dart
    │   ├── order_confirmation_screen.dart
    │   ├── order_form_screen.dart
    │   ├── product_detail_screen.dart
    │   └── product_list_screen.dart
    ├── top_layer
    │   ├── order_detail_screen.dart
    │   ├── order_list_screen.dart
    │   └── selete_store_screen.dart
    └── top_screen.dart
```

- screen フォルダはアプリ内の階層を意識してフォルダ分けしましたが、正直、ずらっと並べてもよかったかもしれません。
- もしくは model と screen のセットでそれぞれの画面ごとにフォルダ分けをしてもよかったのです。
- しかし、model を機能ごとにバラバラにするよりもひとつにまとめたほうが書きやすかったのでこうなってしまいました（これをうまくやる方法があるはず、、、）

# 使用したパッケージ

- [firebase_core](https://pub.dev/packages/firebase_core)
  - Firebse 関連。
- [firebase_auth](https://pub.dev/packages/firebase_auth)
  - Firebse 関連。
- [firebase_analytics](https://pub.dev/packages/firebase_analytics)
  - Firebse 関連。
- [cloud_firestore](https://pub.dev/packages/cloud_firestore)
  - Firebse 関連。
- [firebase_storage](https://pub.dev/packages/firebase_storage)
  - Firebse 関連。
- [firebase_crashlytics](https://pub.dev/packages/firebase_crashlytics)
  - Firebse 関連。
- [google_maps_flutter](https://pub.dev/packages/google_maps_flutter)
  - Google Map を使用するためのパッケージ。
- [location](https://pub.dev/packages/location)
  - 位置情報を取得するためのパッケージ。
- [provider](https://pub.dev/packages/provider)
  - provider による state 管理をするパッケージ。
- [http](https://pub.dev/packages/http)
  - API 通信をするために使用したパッケージ。
- [uuid](https://pub.dev/packages/uuid)
  - uuid を発行するパッケージ。ユーザーを識別するために使用。
- [intl](https://pub.dev/packages/intl)
  - ロケーションの設定するパッケージ。
  - 参考：https://medium.com/flutter-jp/intl-beb5b9e8ee73
- [fluttertoast](https://pub.dev/packages/fluttertoast)
  - トーストを簡単に実装できるパッケージ。↓サンプル

![fluttertoast.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/de4a6dd4-c8a0-7375-0170-22cde5ed907d.png)


- [badges](https://pub.dev/packages/badges)
  - Icon ウィジェットにバッチを簡単に配置できるパッケージ。↓サンプル

![badges.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/d87d3507-1e84-2666-610e-b4eec6b46310.png)


- [google_sign_in](https://pub.dev/packages/google_sign_in)
  - Google Sing In を使用するために必要なパッケージ。

- [flutter_signin_button](https://pub.dev/packages/flutter_signin_button)
  - よく見るサードパーティ認証のボタンを提供してくれるパッケージ。

- [shared_preferences](https://pub.dev/packages/shared_preferences)
ローカルの値を保持可能な Key-Value Store を使えるパッケージ。
iOS でいうところの User Defaults。

- [flutter_launcher_icons](https://pub.dev/packages/flutter_launcher_icons)
  - アプリアイコンを簡単に設定できるパッケージ。 ↓サンプル
 
![flutter_signin_button.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259125/148b9ac2-21fe-6f14-1671-37c7ba729f2f.png)


# 感想

実際に Flutter での開発を経験してみての感想は以下になります。

## ポジティブ意見

- ひとつのソースでiOS/Androidのアプリケーションが両方作られることが単純にすごい
- 公式ドキュメントがちゃんとしている
  - Flutter、Firebase、FlutterFire、Pub公式などのドキュメントがかなりまとも（Googleさんすごい！！）
- ホットリロードがすごい
  - SwiftUIのようななんちゃってホットリロードではない。SwiftUIではホットロード用のコードを書かないといけないのもイケていない
- ライブラリの導入が簡単
  - Pub 一択であり、Swift のようにライブラリ管理のサービスが乱立していない
  - つまり、Swift Package Manager vs CocoaPods vs Carthage のようなカオスなことになっていない
- Firebase との相性がかなりよい
  - ドキュメントがちゃんとしていて、Firebase の導入が簡単
- 宣言的 UI での実装は直感的
  - SwiftUIっぽく書けるので SwiftUI での実装経験がいきる
- CI/CD サービスの Codemagic が使いやすかった
- Android Studio が意外にも使えた
  - ウィジェットを指定して Option + Enter で階層を変えたり、ウィジェットの種類を変更できるのが便利
  - Xcode より軽い

## ネガティブ意見

- iOS/Android のソースが一つになるからといって作業が単純に 1/2 になるというわけではない
  - 結局、info.plist や AndroidManifest.xml などは iOS、Android それどれの設定は必要
  - ストアリリースに必要な証明書の準備や、ストア掲載画像はそれぞれ用意しなければならない
  - この辺の知識は iOS/Android 両方の知識を求めらる瞬間があり、知らないと調べるのに時間がかかる（特にストア配信あたり）
- iOS のビルドにやや時間がかかる
  - Firebase を組み込むと初回ビルドに 3 分ほどかかる
  - ただし、一度ビルドすればあとはホットリロードで更新されるのでそんなにビルドするタイミングは多くない
  - しかし、CI/CD の速度は iOS の ビルド速度にほぼ依存しそう（今回のアプリでは Android 10 分に対して、iOS は 40 分ほどかかりました）
- Dart がいまいち好きになれない（Swift 信者の意見）
  - willSet、didSet のようなプロパティオブザーバーのような機能はない
  - Optional の変数のアンラップ方法として guard let や if let のような書き方がない
  - Struct がない
  - enum はあるが機能が少ない
- Padding の指定が SwiftUI に比べて書きづらい
  - Padding を素直に使ったり SizedBox を使ったりするテクニックがあるがコードが見づらくなる（これは本当にどうにかしてほしいです）
- XD to Flutter があまり使えなかった
  - AdobeXD のデザインファイルからコードを生成できる「XD to Flutter」で出力されるコードは実用的ではないので期待しないほうがいい（戒め）

# 実際に案件で採用する際の注意点

以下の記事の内容に概ね同意です。
よくまとめられていると思います。

参考：モバイルアプリ開発は、Flutter一択なのか？
https://zenn.dev/tetsukick/articles/c297b6ee1e64397432e5

ただ、Google が思ったより Flutter の開発に積極的で、つい先日発表された Flutter 2.2 では Google Pay に対応したりとモバイルのネイティブ機能の活用の幅は広がっていきそうなので、このままだと Flutter 一択の世界があるかもしれません。

# さいごに

正直、自分が思っていたよりも簡単に iOS/Android の両方の OS で動くアプリが作れることに驚きました。
しかも、Firebase の連携もそうですが、いろいろなライブラリであったり、CI/CD 環境や公式ドキュメントも揃っているので、今後、多くのモバイルアプリケーション開発が Flutter で作られる未来はそう遠くないなと感じました。

Flutter かなり熱いと思います。