Flutterでモバイルアプリ開発着手からリリース後まで。【感想】

この記事はリリースまでの一連のざっくりとした流れと感想を述べるにとどまり、年一回あるかないかぐらいの貴重なポエムとなっていることご了承ください。

約2年前の貴重なポエムはこちら

https://qiita.com/MasaoSasaki/items/bcdf9272437bdc6001c5

## 経緯
私はDMM WEBCAMPでRoRを学習したのち、インフラエンジニアに転職しました。
入社後の一年はインフラ系の入門的な資格を取得し、情シスなどを経てインフラ周りの業務知識を広く浅く習得しました。
インフラの知識・経験を得てゆくゆくはいつか開発エンジニアにという意志もあり、ご縁もあって今年の4月に開発職に転職しました。
入社後1ヶ月はLaravelのOJTがあり5月に入ると、上司から「スマホアプリどう？」と唐突に提案があり、少し迷いましたがその日のうちにやりますと返事をして、そこからはFlutterのOJTが始まりました。

余談かつ周りにはあまり言って来ませんでしたが、スクールの卒業前後には開発職になりたいという意志の裏に、Webアプリがある程度作れるようになったらモバイルアプリ開発もいつかやりたいと思っていました。

私は過去に個人ブログやゲームの攻略サイトの運営を１,2年ずつやっていたのですが、その頃よく見ていたGoogleAnalyticsによると訪問者のデバイスの割合は全体の8割がスマホからのアクセス、1.5がPC、0.5がタブレットという比率で、スマホ率の高さを見た時の衝撃がずっと今も印象的に残り続けています。
ペルソナ次第な部分は大いにありますが、一般ユーザーのWeb利用時のデバイス比率は大体こんな感じだということがわかりました。
という経験もあり、スマホに合わせた開発ができるようになるのは必須だと思い、自分がWebサイトを作るときは必ずモバイルファーストで作成していました。
もちろん求められる要件やターゲット次第で
**WebアプリをPC向けで作る**
**Webアプリをモバイル向けで作る**
**スマホアプリを作る**
のどれにするかは要検討です。

私は今回業務で触らせていただくチャンスをいただき、「もう少しLaravelできるようになってからの方がいいか？」とか考えましたが、「スクールでもMVCやってたし、Laravel必要になってもすぐ戻れるんちゃう？」という後押しもありすぐに決めました。

今はFlutter開発メインでAPIの変更があればLaravelも少し触っています。

## Flutterキャッチアップ
「状態管理」「画面描画」というワードが頻繁に出てくるのですが、元々ReactやNext.jsを個人レベルで触っていたこともあって親近感を覚えました。
ただ、Reactは関数コンポーネントが推奨となっているので、基本的には関数を定義してその中でjsxを書き、HTMLに変換されていますが、
FlutterはReactでいうところのクラスコンポーネントに近いので、クラス設計やオブジェクト指向の考え方が重要だなと感じました。

### 画面生成
Webアプリは.phpや.erbや.tsxなどの言語やフレームワークごとに用意されているテンプレートエンジンに、時にそれぞれの言語で条件分岐やループを混ぜながらHTMLっぽく書いて、完成後のWebサイトはHTMLに変換されてブラウザ上に表示されています。

FlutterではDart言語で.dartファイルにクラスを定義してその中にWidgetと呼ばれる画面描画メソッドとWidgetが内包されているクラスを積み重ねていくイメージです。
例えばクラスの中にText()ウィジェットを設置すると引数に入れた文字列がスマホの画面上に表示されます。
このText()ウィジェットをCenter()ウィジェットでラップすると画面上に表示されているテキストが画面の中央によります。
例えばElevatedButton()というWidgetを配置すると、よくあるテキスト入りのいい感じのボタンが表示されます。

### 画面とコード
実際のコードを用いて軽く説明していきます。
以下はFlutterプロジェクト作成直後のサンプルコードです。
```main.dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  void _incrementCounter() {
    setState(() => _counter++);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text(widget.title)),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Text>[
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

```
このコードでエミュレーターを起動した画面が以下です。
![スクリーンショット 2022-12-10 0.45.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643639/63453585-204e-d852-d143-a7f5359a5f13.png)
エミュレーターを立ち上げると、PC画面上に仮想デバイスが立ち上がり、出荷状態のデバイス上には自分が今作っているアプリが表示されています。
今回でいうとUntitledとなっているFlutterアイコンのアプリになります。(その左のは後述する個人開発中のアプリです。)
![スクリーンショット 2022-12-10 0.45.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643639/1d21b47e-1747-14dd-a971-12059ee4c0e0.png)
Untitledをクリックすると、先ほどのコードの内容が画面上に表示されています。

Flutterでrunが実行されると、まずはmain.dartのmain()関数が最初に呼ばれ、引数に入れているクラスが呼ばれます。
そのクラスの中でさらに別のクラスが順番に呼び出しが行われ、クラスの中で定義されている`Widget build(...)`関数の中身が実際に画面に表示されている内容になっています。
Scaffoldウィジェットの引数appBar:が画面上部のタイトル、引数body:が画面の9割、引数floatingActionButton:が右下の青丸いアイコンです。(通称FAB)
基本的にはウィジェットの名前付き引数にウィジェットを渡して、渡した名前付き引数の中にもウィジェットを渡してという構造になるので、ネストはどうしても深くなってしまいがちです。
画面の更新はsetState関数を呼び出します。
上記コードだと、FABボタンが押された時に_incrementCounterメソッドが呼ばれ中のインクリメント処理と画面更新が行われます。

すごいざっくりですけど、画面が作られる仕組みはこんな感じです。

## 公開に向けて
ある程度アプリが完成したら、いよいよ公開に向けての動きになります。

### ビルド
作成したアプリケーションをストアに公開するには、アプリのファイルをストアにアップロードし、ユーザーはストアからアプリのファイルをダウンロード、インストールを行います。
Flutterはクロスプラットフォームなので、
`$ flutter build ios`コマンドを実行したら、iOS用のアプリファイル
`$ flutter build appbundle`コマンドを実行したら、Android用のアプリファイル
がそれぞれ作成され、それらを各々のストアにアップロードして公開申請をします。
ちなみに、公開は無料ではなくそれぞれでデベロッパー登録を行う必要があり
* [Google Play デベロッパー アカウント](https://support.google.com/googleplay/android-developer/answer/6112435?hl=ja#zippy=%2C%E3%82%B9%E3%83%86%E3%83%83%E3%83%97-%E7%99%BB%E9%8C%B2%E6%96%99%E3%82%92%E6%94%AF%E6%89%95%E3%81%86)への登録は**一回限りUS$25**
* [Apple Developer Program](https://developer.apple.com/jp/support/compare-memberships/)への登録は**年間99米ドル**
必要になります。公開するだけで赤字です。

### 審査
どちらも公開に際して審査が待ち受けています。
GoogleとAppleで同じ内容で審査に提出したのですが、Googleの方は一回で審査に合格し、すぐにPlayストアに公開されたのですが、Appleの方は3回ぐらい審査に落とされてしまいました。
主な理由としては、
* 画面内にテスト広告が表示されている。
* 画面内にデバッグ中の文字が入っている。
というものでした。。。Googleさんは許してくれたのに！！！
しかしAppleも的確に指摘してくれるため、修正箇所が割と明確でしたので、そんなに時間がかかるものではなかったです。

### 公開
公開後にまずやったことは周りに認知してもらうことです。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">【MATCH MAKER】<br>🎉v1.0.0 AppStore リリース🎉<br>テニスの試合表を乱数で作るアプリ<br><br>先日のGooglePlayリリースに引き続き、iPhoneユーザー様待望(？)のiOSリリース！<br><br>ネイティブは触ったことないけど、Androgd / iOSが同時リリースできる体験はかなりよい☺️☺️<a href="https://twitter.com/hashtag/Flutter?src=hash&amp;ref_src=twsrc%5Etfw">#Flutter</a><a href="https://t.co/UMn4zYoH9e">https://t.co/UMn4zYoH9e</a></p>&mdash; まさ🧩アプリ開発 (@masa_soli_s) <a href="https://twitter.com/masa_soli_s/status/1586179127929110529?ref_src=twsrc%5Etfw">October 29, 2022</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

そして、アプリを使ってくれるであろう周りの知り合いに一人一人声をかけて実際に使ってもらいました。
趣味がテニスということもあって、ダブルスの組み合わせを決めてくれるアプリを作ったのですが、割と好評でした。
「こんな機能欲しい」とか「ここの表示崩れてるよ」とか色々フィードバックをもらってアップデートを重ねているところです。
[MATCH MAKER(iOS)](https://apps.apple.com/jp/app/match-maker-%E8%A9%A6%E5%90%88%E8%A1%A8%E4%BD%9C%E6%88%90/id6443987111) / [MATCH MAKER(Android)](https://play.google.com/store/apps/details?id=com.match_maker.app&pli=1)

## 感想
### ダウンロード数
日々、自分のアプリがどれぐらいダウンロードされているのか、アナリティクスで確認できます。
やはりiOSのダウンロード数が圧倒的に多いので、年間99ドル払う価値はあったなと思いました。

### 広告収益
アプリ内にはGoogle AdMobという広告ユニットを表示しているので、クリックされたら何十円かの報酬につながっています。
アプリを公開したのは10月末でしたが、11月いっぱいで665円でした。(公開は今回だけにしておきます。)
![スクリーンショット 2022-12-10 12.46.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643639/bae6c745-1886-299f-a8f5-4e72d3754084.png)
システム上8000円まで到達しないと出金ができないようになっていて、これはブログをやっていた時のGoogle AdSenseと同じ仕組みでした。
黒字化には程遠いですが、引き続き頑張ります。

### フィードバック嬉しい
今回作ったプリは、日頃からこういうアプリあったらいいのになと思っていたものを形にした結果です。
まずは自分が使ってみて良さそうなら、周りに少しずつ広めてぐらいの気持ちでしたが、実際に持ち込んでみると結構好評で、作ってよかったなと思いました。
一応競合も調査してみて、他のアプリやサービスに欠けていると思った部分をフォローし切ったつもりです。
それもあってとても期待感を込められて、色々な意見をいただき、アップデートして見せて喜んでもらえてというサイクルがとても楽しくモチベーションにつながっています。
日頃テニスを一緒にやっているみなさんも非常に協力的で、「他のサークルでも使ってみる！」と言ってもらえたり、広告の仕組みを聞かれて話したら「毎日クリックする！」ぐらい言ってくださっていて、とても助けられています。（__※意図的に過度な広告クリックは規約違反になるので止めましたw__）
どなたかわかりませんがレーティングもつき始めていて、これも嬉しいですね。ありがとうございます。
![3.7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643639/649309e0-679a-45af-1223-69bdd689d0fa.png)

## これからやりたいこと
今回MatchMakerというアプリを作りましたが、機能的にもマーケティング的にもできることはまだまだあると思っています。

### SEO
Search Engine Optimizationの略で、日本語にすると検索エンジン最適化です。
これはGoogle検索結果のことなのでアプリとは一見関係なさそうですが、アプリの宣伝サイトを作成してそこからの流入を狙いたいと思っています。
個人ブログやゲームの攻略サイトを運営して狙ったワードで検索一位を取った経験があるので、そのSEOの知識が今いきてくるのではと考えています。
1からSEOを勉強するのは心が折れるかもしれませんが、経験があるならそれを活かさない手は無いなと。(当時と検索のアルゴリズムは変わってるかもなのでその差分は埋めないとですが、根本は変わってないと思っています。)
ちらっとお見せすると、下記は[Google Trends](https://trends.google.co.jp/trends/explore?geo=JP&q=%E3%83%86%E3%83%8B%E3%82%B9%20%E4%B9%B1%E6%95%B0%E8%A1%A8,%E3%83%86%E3%83%8B%E3%82%B9%20%E4%B9%B1%E6%95%B0,%E3%83%86%E3%83%8B%E3%82%B9%20%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B,%E3%83%86%E3%83%8B%E3%82%B9%20%E7%B5%84%E5%90%88%E3%81%9B,%E3%83%86%E3%83%8B%E3%82%B9%20%E8%A9%A6%E5%90%88%E8%A1%A8)というサイトで、Google検索の検索ボリュームを比較できるサービスです。
![スクリーンショット 2022-12-10 13.02.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643639/84916d7c-2df6-28a4-7da9-6cc838d84a14.png)
本サービスはダブルスの組み合わせを決める乱数表を作るサービスなので、関連するワードを調べてみると、どうやらグラフ赤色の「テニス 乱数」というワードが一番検索されてそうです。
これが例えば「テニス」だけにすると検索ボリュームは高くなりますが、競合が多すぎるのと、「テニス」で調べた人の意図がわからないので避けるべきです。(テニスとはどんなスポーツか検索した人が乱数表を求めているとか考えにくいです。)
意図がはっきりしてる検索ワードを探してボリュームが一番大きいところを狙っていきます。

### ASO
App Store Optimizationの略で、日本語にするとアプリストア最適化です。
Googleの検索結果ではなく、Storeでの検索結果に表示されやすくする方法です。
これは未開の地なので、0からキャッチアップをしていくことになりますが、SEO以上に必要となる技術だと思うので、そろそろ本格的に着手していきたいと思っています。
今のところ私のアプリはアプリタイトルで検索してもかなり下の方に埋もれてしまっています。。。

### もっと作りたい
他にもこんなアプリあったらいいなと思うものはいっぱいあるので、今後も新規リリースをやっていきます。
せっかくデベロッパー登録もしたことですので、色々やっていきたいです。


## おわりに
ポエムだけだとさすがに申し訳ないので、Flutterを触ったことない方向けに少し仕組みの解説も入れてみましたが、いかがだったでしょうか？
モバイルアプリ開発いいなと少しでも思っていただけていたら幸いです。

アプリが気になった方はよかったら触ってみてフィードバックをいただけると嬉しく思います！
* [MATCH MAKER(iOS)](https://apps.apple.com/jp/app/match-maker-%E8%A9%A6%E5%90%88%E8%A1%A8%E4%BD%9C%E6%88%90/id6443987111)
* [MATCH MAKER(Android)](https://play.google.com/store/apps/details?id=com.match_maker.app&pli=1)

最後まで読んでいただきありがとうございました。