【個人開発/Flutter】飲み会の後二次会会場を爆速で決定するiOS/Androidアプリ開発

モバイルエンジニアのEtsuwoです。

この度、Flutterを使用してモバイルアプリ開発を行いAndroid・iOSの両方に対応したアプリをリリースしました！
今までSwiftやKotlinを使用したアプリ開発の経験はありましたが、Flutterは**全くの初心者**なので0スタートです。
しかしこのFlutter、個人開発ではかなり使い勝手が良く、発案から2ヶ月・実装開始から1ヶ月程度という短期間で開発が完了しアプリリリースを行うことができました。


アプリ内容だけでなく、開発の流れや利用技術もざっくり記事にしています。
同じようにFlutterを使ってモバイル開発を考えている方に読んでいただけると嬉しいです。

# アプリ概要

この世の全ての酒飲みが経験したことあると思うのですが、

「二次会行きたいけどこの辺のいい感じの居酒屋知らんな...」
「この時間開いてる店どっかあったっけ...」
「しゃーなしでいつものとこ行っとくか...」

ってなることありませんか？
僕はこの寒空の下開いてる店を探すべくスマホをポチポチしてる時間が一番嫌いです。
でも二次会は行きたい。

この悩みをバスターすべく、二次会会場を爆速で決定するモバイルアプリ「**二次会ルーレット**」を作成しました！

![feature_graphic.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383380/248c9a4c-7ea7-d0fe-9d38-970803bdb18d.png)

本アプリでは以下のような簡単3ステップにより**爆速**で二次会会場が決まります！

![nizikai_flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383380/b19c6fc5-430f-dd79-6e8c-ed385fa6ec77.png)


実際にアプリでやっていることは、

1. 位置情報を利用して自身の周囲1キロ圏内で営業している飲食店を検索
1. 検索した飲食店をルーレットに乗せて回す

の二つです。

本アプリはこれらの機能により**快適な二次会ライフ**を提供します。
Android・iOSの両OSでリリースしていますので、同じように二次会に悩める方は是非インストールしてみて下さい！

### QR
![スクリーンショット 2022-10-11 1.19.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383380/abd6822d-0353-0bdc-92f4-0f067e16d1c7.png)

### ストアリンク
iOS: https://apps.apple.com/jp/app/%E4%BA%8C%E6%AC%A1%E4%BC%9A%E3%83%AB%E3%83%BC%E3%83%AC%E3%83%83%E3%83%88/id6443666990
Android: https://play.google.com/store/apps/details?id=com.gmail.etsushi.nizikai_flutter&pli=1

# 用件定義　
アプリを作成するにあたって、最初にどんな機能を実装するか決めていきます。

発案時は「良い案思い浮かんだぞー！」とテンションが高くなっていて、あれもこれも色々と機能をつけたくなってしまいます。しかし、開発期間が長いと段々とモチベーションが下がり、めんどくさくなってついにはリリースを諦めてしまうでしょう。このため、「とりあえずリリースしてしまうために最小限の機能を持ったアプリを作る、機能追加は後」という方針を取ります。

以上の方針の下、目的である「二次会会場決めにかかる時間をバスターする」ために必要な最小の機能を考えます。今回のアプリの場合、以下の機能を最小として実装することにしました。

- 自身の周辺の飲食店を検索する機能
- 飲食店をルーレットで決定する機能
- 広告表示
- 強制アップデート

「おいおい、広告入ってるじゃん！最小限じゃないじゃん！」と思われるかも知れませんが、広告入れた方がワクワクして僕のモチベーションも維持しやすいよね！ということで入れてます。広告で一攫千金はモバイルエンジニアの夢ですからね。あと後述するAdMobを使えばお手軽に実装することができます。

飲食店の検索機能に関しては、飲食店のカテゴリ・営業時間・価格帯等の要素で絞り込めると便利そうですが、とりあえず最初は検索できたらOKなので最初のリリースには含めません。

強制アップデートは重大なバグに対応したり機能を大きく変えたアップデート時に必要になります。このため、出来るだけ早く入れるに越したことは無いので入れておきます。

# デザイン　
用件が決まればどのような画面構成にするか決まります。
今回は、以下の4画面を作ります。　

- スプラッシュ
- ルーレット回す画面
- 飲食店を検索する画面
- 強制アップデート画面

また、デザインはFigmaを使って作成していきます。

![スクリーンショット 2022-11-30 15.57.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383380/bcfa657b-b60b-239c-7282-91831611831b.png)


僕はモバイル開発こそ経験がありますが、デザインについては一切勉強しておらずズブの素人です。なので、App StoreやPlay Storeで公開されている他のルーレットアプリや実物のルーレットを参考に作っていきました。この工程が一番辛く、完成物も**しんどめのデザイン**になってしまった気がするのでそのうちリメイクしたいところです。

https://www.figma.com/ja/

# 実装
## アーキテクチャ
アーキテクチャはMVVMを使用しています。MVVMとは、簡単に言うと、コードをView、ViewModel、Modelの3層に分割するアプリの構築法です。また、Model層はdata層、Repository層、UseCase層に分けて管理します。アプリを適切な単位で分割することにより、今後の追加開発やリファクタリングを行いやすくなります。

![nizikai_arch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/383380/e7858a00-93ea-5ab5-45cd-cb7c0796ddf5.png)


## 外部サービス
本アプリでは以下を主に使用しています。

- リクルートWEBサービス
- AdMob
- Firebase

### リクルートWEBサービス
本アプリでは，飲食店，特に居酒屋やバーなどアルコールを提供している飲食店の情報が不可欠です．特に，位置情報を使って自身の周囲の飲食店を検索できるとベストです．

このため，本アプリでは，ホットペッパーに掲載されている飲食店の情報を取得できるリクルートWEBサービスのグルメサーチAPIを利用します．グルメサーチAPIは，上記の要件を満たしていることに加え，無料で利用することができるので今回のような小規模な個人開発にぴったりです．

https://webservice.recruit.co.jp/

### AdMob
AdMobは、モバイルアプリに広告を挿入できるGoogle製の広告SDKです。調べてみたところ、広告SDKにはSwiftやKotlinなら対応しているもののFlutterには未対応、または機能が限られるよ、というものがそこそこあるようです。しかし、ここはGoogle製同士サポートがしっかりしていて安心できます。

今回は、画面下部にバナー広告を表示させるために使用しています。本当は、ルーレットを回した後、結果表示と共に表示されるインタースティシャル広告も考えていたのですが、**鬱陶しくて２度と使ってくれなくなりそう**なのでやめました。

https://admob.google.com/intl/ja/home/

### Firebase
Firebaseは、自前でサーバを用意しなくてもログイン・ログアウト機能やアプリの解析、アプリ内通知など幅広い機能をアプリで提供できるようになるサービスです。本アプリでは、アプリ解析用にCrashlyticsとAnalytics、強制アップデート用にRemote Configを利用しています。

僕のようにサーバ周りはからっきしな場合や個人アプリ程度で工数増やしたくない場合は本当にありがたい存在です。

https://firebase.google.com/?hl=ja

## 状態管理
本アプリでは状態管理に以下の技術を使用しました。

- RiverPod
- FlutterHooks
- StateNotifier
- freezed

これらの技術を選定した理由は、調査当時（2022年8月）最も人気だろうと考えたからです。かなり雑な選定方法ですが、初のFlutterを利用したアプリ作成で、何が他と比べてどう良いのか、どんな歴史があるのかさっぱり分からないので「新しくて人気なやつをとりあえず使ってみる」という方針です。一応、RiverPodやFreezedがイケてるという話は聞いていたのでそこをとっかかりに調査しました。

### RiverPod
DI・状態管理・シングルトンの作成などが行えるライブラリです。後述するStateNotifierなど状態を保持して変更があれば購読するクラスを更新させる機構が備わっているので大変便利です。下記のようにProviderをグローバルに宣言してシングルトンの作成とDIを行います。

```dart:ForceUpdateUseCase.dart
// RemoteConfigClientを提供するProviderを作成
final remoteConfigClientProvider = Provider((ref) =>
    RemoteConfigClient()
);

// RemoteConfigClientProviderを利用してRemoteConfigClientをDI
final checkForceUpdateUseCaseProvider = Provider((ref) =>
    CheckForceUpdateUseCase(ref.watch(remoteConfigClientProvider))
);
```

https://riverpod.dev/ja/

### FlutterHooks
こちらも状態管理が行えるライブラリです。RiverPodとの違いの理解に苦労しましたが、今のところ「UIの状態管理においてRiverPodで扱えないもの・ふさわしくないもの」はこちらで処理したら良い、くらいの感覚です（違ったらすみません）。Reactのhooksから来ているらしいのですが、触ったことないのでどうも勝手が分かりませんね。

公式のトップに載っているAnimationControllerの例がとても鮮やかに見えたので採用しています。

https://pub.dev/packages/flutter_hooks

### StateNotifier
RiverPodを入れると一緒に入ってくるクラスです。状態(State)を保持してStateの変更を購読クラスに通知します。Stateはimmutableであることが前提となっており、後述するFreezedなどを使うとCopyWithメソッドを使ってStateを更新できるようになるのでいい感じです。

本アプリではViewModelの実装に使用しています。

https://riverpod.dev/ja/docs/providers/state_notifier_provider


### freezed
immutableなクラスの実装をサポートしてくれるライブラリです。freezedを使用して生成したクラスでは`toString()`や`==`，`copyWith()`が使用できるようになります。また、API通信時にJSONをデコードする場合に利用できる`fromJson()`なども生成できます。以下のような特定の書式で記述してbuild_runnerを走らせると生成してくれます。

```dart:GourmetResponse.dart
@freezed
class GourmetResponse with _$GourmetResponse {
  const factory GourmetResponse({
    required GourmetSearchResult results
  }) = _GourmetResponse;

  factory GourmetResponse.fromJson(Map<String, dynamic> json) => _$GourmetResponseFromJson(json);
}
```

本アプリでは、StateNotifierのState生成とAPI通信のレスポンスを格納するクラス生成に使用しています。

https://pub.dev/packages/freezed

# まとめ
いかがでしたでしょうか。あまり技術的な内容は深くありませんが、個人開発の流れやFlutterについて参考になれば幸いです。

僕としては今回初めてFlutterを使ってみて、「個人開発などリソースが少ない場合やアプリが小規模な場合は良いもんだな」と感じました。実際に発案から二ヶ月程度、実装だけなら一ヶ月程度でリリースできたので本当に爆速です。また、クロスネイティブの中で一番人気なだけあり、技術記事が豊富だったのも非常に助かりました。

今後は、アップデートを重ねて多くの人に使ってもらえるアプリを目指します。既に数回アップデートしており、上記より多少機能が増えてるのでぜひ触ってみてください！

ありがとうございました！