2020年9月、JetBrainsからKotlin Multiplatform Mobile(KMM)のアルファ版がリリースされました。アプリ開発エンジニアたるもの、一度はKMMを利用してロジックを共通化したい……と思ったことがあるのではないでしょうか? 本記事では、『ホットペッパーグルメ』のフルリプレイスプロジェクトでKMM導入を決めた経緯と、実際に導入しロジックを共通化してみてどうだったのかという事例を紹介します。
『ホットペッパーグルメ』フルリプレイスプロジェクト
『ホットペッパーグルメ』のアプリチームでは、iOS・Android共にコードを完全に新しいものに置き換えるフルリプレイスを実施しています。
アプリのリリースから10年以上経過しており、ソースコードが肥大化・複雑化し、その結果として開発スピードが鈍化したり障害防止の難易度が高まったりしているという課題が浮き彫りになってきたためです。
2020年10月から始めたこのフルリプレイスプロジェクトでは、まだアルファ版であるKotlin Multiplatform Mobile(以降KMM)を採用し、Android・iOSでのコードの共通化に取り組んでいます。
KMM導入の経緯
クロスプラットフォーム技術の必要性
リプレイスで解決したい課題の一つに、OS間の予期せぬ仕様差分がありました。
長らく正確な仕様書が作成されずに運用を続けてきたため、細かい部分でOS間のロジックに意図しない差分が多く生まれていました。その最たる例が分析用のログで、一見すると同じように見えるログでもiOSとAndroidでパラメータが微妙に違うなどの問題が大量に発生していました。
それらの問題を解決するために、仕様書の再定義に加えて、コードでの差分発生を防ぐ目的でクロスプラットフォーム技術を採用することにしました。
また、エンジニアとして技術的に挑戦をしたい・単純に開発工数を減らせるという点も魅力的でした。
KMM or Flutter?
クロスプラットフォーム技術として最後まで選択肢に残ったのがKMMとFlutterでした。
様々な点で比較を行いましたが、最終的にKMM導入の決め手となったのは下記の三点です。
共通化範囲
共通化の範囲は、FlutterではUIまで含めた全てのコード、KMMではUI以外のコードとなります。
今回のプロジェクトでは、UIはプラットフォームごとに適したで形で実装する方針としていました。その場合Flutterの恩恵をフルに受けられず、むしろ分岐の実装に苦労する懸念があるので、KMMに軍配が上がりました。
継続不可能なレベルの問題が発生した場合のリカバリー方法
大規模なプロジェクトであるため、万が一開発の途中でクロスプラットフォームプロジェクト起因での大きな問題が発生した場合のリカバリー方法についても検討する必要がありました。
Flutterで問題が発生して利用継続が不可能となった場合、UIまで含めて全てのコードを書き直す必要がありますが、KMMの場合はiOSの共通化した部分のみをSwiftで書き直せば良いため、比較的リスクが低いと言えます。
採用・育成
Flutterを導入した場合の大きな懸念となったのが人材の確保の難しさです。
まだ比較的新しいフレームワークだということもあり、社内でFlutterができる人材を確保するのは難しく、新しく人を雇用するにしてもFlutter経験者はそもそも絶対数が少ないと考えられます。
KMMであればKotlin経験のあるエンジニアは比較的低い学習コストで参画が可能であり、各OS部分は通常通りネイティブアプリのエンジニアがほぼ学習コストなしに参画が可能です。
実際、私たちのプロジェクトでは、多くのAndroid・Kotlin経験者をKMMのメンバーとして迎え入れています。
これらの点からKMMを利用するという方針を固め、実際に導入した場合に満足に開発を行うことができるか、期待したメリットを受けられるかを検証した上で、最終的な採用を決定しました。
アーキテクチャとチーム体制
KMMのプロジェクトでは、Android・KMM・iOS全てのコードを含めたシングルリポジトリでの進め方と、それぞれで独立したリポジトリを作成しKMMをライブラリとしてインポートする進め方が考えられます。
私たちのリプレイスプロジェクトでは、AndroidとKMMは同時に開発を行うため、KMMの改修をなるべく細かい頻度で手間なく取り込めるようにシングルリポジトリを選択しました。
全体のアーキテクチャは、クリーンアーキテクチャを取り入れた三層の構成となっています。
『ホットペッパーグルメ』リプレイスアプリのアーキテクチャ
- Domain層:アプリのロジックを持つ。UseCaseやRepositoryのInterfaceが置かれる。
- Data層:データへのアクセスを持つ。データオブジェクトやRepositoryの実装が置かれる。
- Presentation層:UIとそれに関連するロジックを持つ。Androidの場合ViewModelやActivity/Fragment・Viewが置かれる。
Domain層とData層をKMMで実装し、Presentation層はそれぞれのOSで実装します。
チームも同様に、KMMを担当するチームと、それぞれのOSを担当するAndroidチーム・iOSチームに分かれています。
AndroidとKMM部分はマルチモジュール化することで依存を強制し、クリーンアーキテクチャを守りやすいように工夫しています。
KMM導入の効果
当初期待した通り、Domain層・Data層をKMMで記述することができ、それによりロジック・ログの共通化が実現できました。 各OSは細かいことは気にせずKMMのメソッドを叩きさえすれば、仕様の差分なくロジック・ログを利用できます。
加えて、Presentation層での複雑な処理を一部KMMに置くなどの工夫により、さらに工数の削減に寄与しています。
副次的な効果として、チームの分割により各OSチームはAPIの細かい仕様の調査や調整をすることなくPresentation層に注力でき、KMMのチームはUI実装がないためビジネスロジックに集中できました。
チームが増えることでコミュニケーションコストが上がる懸念はありましたが、両OSのチームはAPIチームとコミュニケーションをとる代わりにKMMチームとコミュニケーションをとることになるだけでコミュニケーション対象は増えず、むしろアプリもAPIも分かっているKMMチームがAPIチームとの間に入ってくれるのでとても頼もしかったです。
また、API関連の相談はKMMチームを通すということがAPIチームにも伝わっていたので、APIチームとのコミュニケーションも円滑に進めることが可能でした。
KMM導入のデメリット
KMMの利用を中止するような大きな問題ではありませんでしたが、iOSに対しては工夫が必要な小さな問題がいくつかありました。
例えば次のような問題です。
- GenericsのなかにGenericsがあった場合Any型と判断されてしまう
- 同名・同引数名のメソッドが利用できない
- interfaceのデフォルト実装が利用できない
しかしどれもひと工夫で解決することができ、大きな問題とはなりませんでした(詳細についてはiOSDC2021で発表された iOSエンジニアがKMPで大規模アプリのロジック共通化をしてうまくできている話 を参照いただければと思います)。
また、Flutterなど他のクロスプラットフォーム技術と比べた場合、UIをそれぞれのOSで開発しなければいけない点はデメリットとも考えられそうです。
私たちはあえてUIを共通化しないことを選びましたが、実際のところ多くのUIが両OSで同じ見た目になっており、いっそのことFlutterで完全に共通化するように強制した方が良かったのではないかと思うこともあります。
これはUIを両OSで馴染みの技術を使って実装できることとのトレードオフなので、プロジェクトとしてどちらを取るべきかはプロジェクト開始前に慎重に検討することをお勧めします。
おわりに
結論として、我々の『ホットペッパーグルメ』アプリのフルリプレイスにおけるKMMの導入は非常に良い選択だったと思います。
致命的な問題もなく、KMMのメリットを十分に享受して開発できています。
良くも悪くもPresentation層は両OSで開発する必要があるため、人数が限られていたり、小規模だったり、UIまで共通化したいアプリ開発の場合はFlutterなどの方が良い場合もありそうですが、どのような場合でもKMMを利用すること自体が大きな負債になることはないように感じています。
本事例がKMM利用を検討している方の助けになれば幸いです。
現在リクルートでは多くのプロダクトで一緒に働くメンバーを募集しています。ご興味がある方はこちらをご覧ください。
著者紹介
飯島 彩輝
株式会社リクルート所属、『ホットペッパーグルメ』Androidエンジニア。 リプレイスプロジェクトではAndroidエンジニアチームのリーダーを務める。