Go + Flutter でモバイルアプリを開発しリリースした話

この度所属している大学の大学生向けアプリを開発し AppleApp StoreGoogle の Play Store にリリースしました。

play.google.com

マイルストーン

マイルストーン

  • MILESTONE HENSHUKAI, GENERAL INC. ASSOCIATION
  • 教育
  • 無料

apps.apple.com

アプリのダウンロード数などのグラフを示している。合計DL数が3800程度。

リリースから1週間経った時点でのアプリストアでのダウンロード数などのグラフです。自分が思った以上に使われていますね。

思えば生まれて初めてこういった形で自分の書いたコードで実際に人に使われるものが作れたなと感慨深いです。この記事ではアプリの開発の裏側、利用している技術や開発過程などを説明していきます。

マイルストーンとは

マイルストーンエクスプレスは、早稲田大学の非公認サークル「マイルストーン編集会*1」が発行している学生向け雑誌で、学生からのアンケートに基づいた授業の評価をまとめた逆評定を掲載しています。

今回作製したアプリでは、マイルストーンエクスプレスに掲載されている逆評定のデータを検索し、お気に入りに登録することができます。ただし利用には書籍購入時に付属しているシリアルコードをアプリで利用するアカウントに紐付ける必要があります。

iOSAndroid双方で利用可能で、開発期間は去年の9月頃から始まり、3/10の書籍発売に合わせリリースできました。相方はコードはあまり書けないのですが、サークルに連絡を取ったり、リリースに必要な文章や利用規約の準備等事務作業を全て担当してくれました。そもそもアプリの制作という話をサークルに持ちかけたのも彼なので、彼無しでは完成しなかったどころか始まってすらいなかったでしょう。バックエンドは全て私が担当し、フロントエンドのデザインは相方に任せそれ以外を担当しました。

利用している技術

OpenAPI(Swagger)

今回最も使ってよかった技術の一つです。アクセスポイントをyamljsonで定義することができるのですが、このファイルをもとにしてコードの自動生成が可能になります。またここに記述したサンプルを返すモックサーバーが簡単に建てられるので、フロントエンドとバックエンドを並行して開発することが用意になります。今回は私が両方やってるので恩恵は受けられませんでしたが、このようにしてAPIを定義しておくことで大変管理がしやすくなりました。

Go

なんだか流行ってますよね、Go。コードがわかりやすいし、速いので大変好みです。

中高では Javascript をメインで書いていたのですが、いかんせん node.js が好きじゃなく代替を探したときに出会ったのが Go でした。といっても当時は殆ど先に進むことはなく学ぶのを辞めてしまったのですが、その経験があるので大学でバックエンドサーバーを何で書くか迷ったときに真っ先に候補に挙がったのが Go でした。実際に触ってみると非常にシンプルで、コードが複雑になりにくいところが大きな利点であると感じました。他の言語と大きく異なり、エラーが例外ではなく値によって返却されるという点も好みでした。無論 err!=nil がコードに大量発生するという汚点はあるのですが、わかりやすさはこの欠点を上回ります。

学習するにあたり Go言語実践開発入門と Go Programming Blueprints を読みましたが、他の言語での開発経験がある場合は不要だと思います。ネット上の文書が大変充実しているのでそもそも技術書を買う必要は無いと言う人もいますが、私は書籍の消化が得意なのでスピード重視で書籍を購入し、結果的に買って良かったと考えています。使用したパッケージなどを説明していきます。

Clean Archiecture, DI (wire)

コードを書く際には依存性を分離することを意識し、また依存性の注入(DI)を行うことで単体テストをしやすくしています。具体的にはDBなど外の要素にアクセスするレイヤー(infra *2 )、これを組み合わせてやりたい操作を実現するレイヤー(usecase)、さらにサーバーへのアクセスを解釈し usecase を用いて望みの操作を実現するレイヤー(handler)を用意しそれぞれ1つ下のレイヤーにのみ関心を持つことでコードの複雑さを下げています。DI は wire を用いて一括で行っています。

あんまり覚えてないですが、確かツイッターで「企業の面接を受けたらアーキテクチャの話をされて全然わからなかった」みたいな記事を読んだのがきっかけでアーキテクチャについて調べ始めて、意識するようになったはずです。思い返してみればそれまではほんとにスパゲッティコードを書いてたので本当にありがたい出会いでした。

Testing (mockgen, gotests, kin-openapi)

依存性を注入した部分のテストには mockgen を利用しています。使うのにちょっと手間がかかるので gotests と組み合わせて、mockgenを利用するための準備の部分を自動生成できるようにしています。DBにアクセスする部分のテストはモックを用意するのが面倒だったのでシェルを叩いて毎回テスト用にDBを用意するコードとサンプルデータを入れるsqlファイルを用意しこれらをテスト実行時に走らせてテストしていました。ポコポコDBを作ったり消したりするのあんまり良くないんじゃないかという気がしてるので別の方法を考えています。APIのレスポンス検証をkin-openapiを用いたテストで行っています(後述)。

ORM (sqlboiler, sql-migrate)

ORMには sqlboiler を採用しました。当初は GORM を利用していたのですが、テーブルを JOIN する際に手間取ったり、struct を自分で用意するのが面倒になったので既存のDBスキーマから自動でマップする構造体などを生成してくれる sqlboiler へと乗り換えました。これにより事前に生成されたコードを用いるのでGORMより速く、 JOIN や複雑なクエリの発行も GORM に比べて簡単に、また殆どの処理を型安全に書くことができるようになりました。DBスキーマsql-migrate を利用して管理し、もしスキーマが変更されコードを書き直す必要が生じた場合は再生成後静的解析により警告が表示されるようになります。この移行は結構手間がかかりましたが、やって良かったです。DIを行なっていたために他のレイヤーのコードには一切手をつけることなく移行が完了したので、コードを丁寧に書いててよかったと思いました。

Handling errors (標準のerrors)

エラーハンドリングにはカスタムエラーパッケージを用意し、error を Wrap するごとに呼び出し元の関数の情報を追加していったり独自のエラーコードを用いて発生したエラーによる条件分岐を可能にしたりしています。基本的には上の階層に受け渡されていき、一番上の handler 層で適切な HTTP Status Code と共にエラーとしてクライアント側に返却されます。独自エラー型はIsメソッドを持っておりerrors.Isの際にエラーコードが同じかどうかで比較をしています。エラーコード型はエラーコードの文字列を返すメソッドとステータスコードを返すメソッドを持っており、必ず403を返すerr403とエラーコードの文字列の組み合わせなどで表されています。呼び出し関数の取得はruntime.Callerから得られた文字列を/や.で区切っていじることで取得しており、もう少しきれいなやり方はないかと思いつつ動いてるのでまあいいかとそのままにしてあります。

API (oapi-codegen, kin-openapi)

oapi-codegenを利用しており、定義したAPIを自動でサーブするためのインターフェースを用意しパラメータを自動でパースしてくれたり返却する際JSONにMarshallするための構造体も自動で生成してくれるので大変助かりました。テストはkin-openapiのレスポンス検証を使っており、もし定義と異なるレスポンスが帰ってきた場合はテストが通らないので定義通りに実装されているかが確認できます。

認証

GoogleAppleでのソーシャルログインに対応しています。外部の認証サーバーからトークンを受け取ってこっちのサーバー側で検証したらそのサービス固有のIDと一緒にユーザー登録してJWTを発行しています。このときJWTに所持している本の情報も入れているため以後アクセストークンが有効であればユーザーDBアクセスなしで検索可能*3となります。アクセストークンの有効期限は15分ほどで切れた場合はリフレッシュトークンを用いてリフレッシュすることが必要です。この際に外部サーバーのトークンが有効かどうか確認することでアプリの利用を停止されてないか確かめています。

Flutter (フロントエンド)

フロントエンドには Flutter を採用しました。iOSAndroid 双方で利用できるようにする必要があったのでクロスプラットフォームなフロントエンドフレームワークを探したのですが、まず私がWeb技術、特にHTMLとCSSが苦手なため React Native などの Web技術ベースのフレームワークは却下となり、他のフレームワークと比較した結果情報量が多く、また個人的に C より Java に比較的馴染みがあるので Flutter を採用することにしました。

実際に使ってみるとプラットフォーム間の差異を殆ど気にすることなく開発を行うことができ、ツールも充実しているためかなり満足度が高いです。普段は Linux を用いているのですが、Linux ネイティブでも Android エミュレータと同じものが動くことに大変驚きました。今回のアプリ開発では私が Mac を持っていないために最後のひと月以外は全て Android のみで動作確認していたのですが、全く問題なく iOS でも動作してくれました。

Flutter の経験は1週間程でしたが、随分その経験が活きたように思います。

github.com

2021年12月末から翌年1月、大学1年の冬休みは Flutter と Go を書いて過ごしていたのですが、大学が始まってから全く触っていないので本当にこのレポジトリにあるものが全てです。

状態管理 (riverpod)

上に出したレポジトリではChangeNotifierを用いてMVVMっぽく書いていましたが、現在は Riverpod 2.0の Code Generationを用いています。中身はNotifierProviderとAsyncNotifierProviderで、特にAsyncNotifierが非同期処理を扱う際にはとても便利です。殆どのWidgetをStatelessにしてViewModelで一括でステートを保持しています。ChangeNotifierを使っている部分もあるためすべてRiverpod 2.0に統一したいです。

最初はMVVMっぽくだいたい全ての画面に対応するViewModelを用意していましたが、最近は無駄が多い気もしています。ルーティング用のProviderを用意したらVMは取っ払っちゃって今その一個下にあるレイヤーを画面から直接呼んじゃうのもありかなという気がしています。そうなるとGoでやってる階層構造とあんまり変わりませんね。

DI, Routing (GetX)

一番下のレイヤー、RepositoryのDIとルーティングにGetXを用いていますが、全てRiverpodに統一できるはずなので置き換えようかな〜と思いつつも特に困ったことがないためそのままになっています。色々見てるとちょっと評判悪いみたいです。置き換えて比べてみようと思います。

API叩く部分 (openapi-codegen, dio)

openapi-codegendioのコードを自動生成しており、通信部分は全く自力で書いていません。めちゃめちゃ楽で定義を更新して再生成すると静的解析により対応が必要な部分がわかるのも大変良いです。あんまり情報が見つからなかったのが難点といえば難点でしょうか。

認証

ログイン処理では外部のOauth認証サーバーにアクセスし、もらってきたトークンをバックエンドに送り有効であると確かめられた場合にトークンが返却されるのでそれをSecure Storageに保管しています。Flutter側でもトークンの情報を活用することは考えましたが、トークンの形式を変更するかもしれないことを考えてFlutter側では完全にノータッチで、仮にJWT以外に置き換えたとしてもフロント側のアプデは一切必要ないようにしています。

トークン有効期限切れで403エラーが返ってきた際に時にdioのinterceptorで上の階層から見えないようにトークンをリフレッシュしてリトライする処理がちょっと難しかったです。リポジトリのディスカッションなどを参考に実装しました。

DB (PostgreSQL)

特に文句ありません。現在は基本的な機能しか使っていないので当然といえば当然ですが、これからpg_bigmを用いた類似度検索などを実装しようと思っているので、そこで高機能さにお世話になるかもしれません。

DB設計なんてもちろん初めてだったので達人DBを読みました。日雇いのバイトに持ってって休憩中に読んでたら社員の人に褒められて、後でジュースとお菓子おごってもらいました。達人DBはめっちゃわかりやすくてかつ読みやすいので超オススメです。読んだことなかったら騙されたと思って買ってみてほしいです。

Deploy (render.com)

日本ではあまり知名度がないみたいですが、使いやすいPaaSです。友達が使ってたので使ってみたら本当に便利で離れられなくなりました。無料枠もあるので気軽に試せますし、GitHubのレポジトリを選択するだけでデプロイができます。無料版であってもPR previewというプルリクエスト毎にプレビュー版をデプロイしてくれる機能もあり、コミットがあれば自動でビルド・デプロイしてくれます。難点といえば日本サーバーがなくシンガポールサーバーを使わざるを得ない点ですが、そこまでレスポンスは悪くないので問題ないかなと思います。DBもこのサービス上にデプロイしていて、24時間毎の自動バックアップには助けられました。

コード管理 (GitHub)

コードは基本GitHubで管理していました。途中まではバックエンドとフロントエンドで分けていましたが途中からリポジトリを合体させモノレポにしました。これは開発中に出てたハッカソンで友達が同じようにモノレポで収めていたのを参考にしてやったはずです。Issueを立ててバグの原因特定してそれがバックエンドでもフロントエンドでもブランチ切って同じリポジトリで対応できたり、ある時点でのフロントとバックのコードが簡単に取得できたりとメリットが多かったです。ブランチはリリース前は初期状態のmain、developと都度のfeatureブランチのみで、リリース時にreleaseブランチを切って調節した後にmainにマージしてmainブランチをrender.comでデプロイしました。今はgit flowのとおりに基本的にはやっていて、リリースバージョンごとのタグもつけいます。バグや機能追加毎にIssueを立ててブランチを作り、マイルストーン*4によって次に出すバージョンに含める機能を管理しています。依存性を分離することで複数のファイルを編集することをあまりせずに共同作業ができたので、フロントの見た目だけをいじる相方と裏のロジックをいじる自分でコンフリクトを多少避けることができたのも良い点でした。ただ結局見た目のコードもかなり手を入れたので、コンフリクトしてたことも多々ありめんどくさかったです。

今後やりたいこと

まずフロントのエラーハンドリングが適当なので改善したいです。ずっとGoのerror as a valueを扱ってた結果例外をthrowするDartに慣れないままとりあえず動く状態にしてリリースになんとか間に合わせたという状況なので、全然エラーをうまくハンドルすることができていません。実際エラーが出ないことでストアのレビューに低評価が出てしまったので早急に対応したいところです。

あとユーザーに関係ない部分になってしまいますが、管理画面を作ろうと思っています。サービスリリースして初日にユーザー数を確認しようとSQL叩いたら、ミスってDBのユーザーテーブルを全部飛ばしてしまったのでなるべく生で触りたくないです。

機能の追加も随時やっていきます。今はリリースに必要な最低限の機能だけでリリースしているので、所持している本を確認したりより詳細な検索条件を設定したり、シラバスを参照できるようにしたり、記事を配信できるようにしたりとやりたいことはたくさんあります。あと忘れちゃいけないのが広告で、例えば早稲田通りでティッシュ配ってる企業なんかはこういう早稲田生しか使わないようなアプリに広告出したいんじゃないかな〜と思うのでうまく広告を取り入れて費用を回収していきたいですね*5

最後に

アプリを使ってくれている人々、どうもありがとうございます。科目登録期間に旅行に行くことになってしまったが、アプリのおかげで旅行先に本を持っていかなくて良くなったという声や、弟・先輩の友達が実際に使っていたなどの話も聞けて大変嬉しく思っています。初めてのアプリ開発でしたが、無事にリリースできてまずは一安心です。

設計からリリースまで全部やったのはとても良い経験になりましたし、めちゃくちゃ大変でしたがその分大きな自信に繋がりました。これからもアプリに限らず色々作って、色々やっていくので、応援よろしくお願いします。 ;) 

*1:マイルストーン編集会とは – e-mile

*2:こう呼んでいますが、これが一般的かはわかりません 他も同様です

*3:ただし、トークンが発行されたあとにシリアルコードを紐付けると反映に時間がかかる

*4:アプリではなくGitHubの機能の方

*5:今は無給!!