こんにちは! 技術本部 Sansan Engineering Unit Mobile Applicationグループの桑原です。
この度、Sansanモバイルアプリでは開発スピードを加速させるため、実プロダクトにKotlin Multiplatform(以下、KMP)を導入しました!
まずは1画面の導入から始めましたが、今後は既存の画面も順次KMPに移行していく予定です。
この記事では、Androidエンジニアの視点から、SansanアプリにKMPを導入した背景やその成果、さらにKMPの設計についても簡単に紹介します。
KMPの導入背景と成果
導入した理由
KMPを導入した理由は、先述したとおり開発生産性の向上を目指すためです。現在より開発生産性を上げるために、私たちの開発チームは以下の課題に直面していました。
- OS間の仕様調整コストの増大:既存機能の改修時にエラー処理などのエッジケースの挙動が異なるなど、OS間で仕様差分が頻繁に発生していました。また、新規機能開発でも、OSごとに仕様調整のためのコミュニケーションが必要であり、その結果コストが発生していました。
- 同一仕様のプログラムを2回作成する非効率性:ビジネスロジック以下の共通化が進んでおらず、同じ仕様のプログラムをOSごとに作る必要がありました。
これらの問題を解決する手段としてKMPを導入し、開発生産性の向上を図りました。
導入で感じたメリット
- コスト削減
- 仕様差異の解消
- KMP化した部分は、OS間での仕様差異が解消されました。これにより、OSごとの仕様調整や、それに伴うコミュニケーションが不要となり、業務の効率が向上しました。
- 保守運用の効率化
- KMP内部にロジックが閉じ、画面のプロパティや状態が共通化されたことで、インターフェースの変更がない限り、KMPの修正だけで両OSの挙動を統一できるようになりました。これによりロジックのバグ修正が一箇所で済むため、修正作業がシンプルかつ迅速に行え、開発の安定性が向上しました。
- 仕様差異の解消
- リソース効率の最大化
- 現在、チーム全体で互いのプラットフォームを開発できるようにする取り組みが始まっています。簡単なUI実装ができるスキルが身につけば、iOS、Android、KMPの各プラットフォームにリソースを柔軟にアサインできるようになり、リソース配分が最適化され、開発効率の向上が期待されています。
- iOS、Androidエンジニア間での交流が活発化
- 以前は、iOSチームとAndroidチームは完全に別々で作業をしており、ほとんど会話することがありませんでした。しかし、効率性の向上や開発プロセスの改善を目指し、混合チームとして合同スクラムチームが編成されました。その結果、毎日交流するようになり、チーム間のコミュニケーションが活発化しました。
- KMP部分の実装は、iOSとAndroidの担当者がそれぞれレビューを行うことで、開発の質を向上させるための活発な議論が行われています。
導入で感じたデメリット
- iOS側の負担増加
- 互換性の問題で型が期待通りにならず頭を悩ますことがちらほら
- KMPをiOS向けにビルドした結果、Xcodeの型認識がうまく機能せず、コード補完が動作しないことが頻発しています。現状はiOS側でtypealiasを使って型名を再定義しています。
- KotlinからSwiftへの直接的なエクスポート化が実現すれば解消する可能性があるので、今後のKotlinバージョンアップに期待しています。
- キャッチアップが必要
- 互換性の問題で型が期待通りにならず頭を悩ますことがちらほら
- サブモジュール管理によるOS側の負担
- KMPのリポジトリとiOS、Androidのリポジトリを分けて管理しているため、例えばKMP側でプロパティを追加した場合、iOSやAndroid側でその実装が行われていない状態でサブモジュールをアップデートすると、コンパイルエラーが発生することがあります。このため、各OSのエンジニアが気軽に開発のベースブランチであるdevelopを参照できないという問題が生じています。
- 現在、この問題を解決するために、モノレポに移行する案や、運用ルールで防ぐ方法など、改善策を検討中です。
- ネイティブコードからKMPへの移行コスト
- KMPの導入によりOS間の仕様調整コストは減少していますが、既存の完全ネイティブコードをKMPに書き直す際には、追加のコストが発生します。この作業は避けられないため、初期の開発フェーズでは、特にコスト負担が大きくなる傾向があります。
- 一方で、KMP導入後の新規機能開発や、今後増えていくKMP画面の改修では、仕様調整の手間が軽減されることで、開発効率の向上が期待されます。導入前の試算では、KMP導入により全体的な開発効率が11%向上し、基盤やCI/CD環境が整備されれば、最大22%の向上が見込まれていました。しかし、現時点では開発環境が完全には整っておらず、この効果がまだ十分に発揮されていない状況です。環境整備が進むことで、効率の向上が期待されています。
KMPの基本設計
AndroidでのKMPモジュールの取り込み方法
「git submodule + composite build」 の方式を採用しています。
Sansan-AndroidからKMPモジュールをライブラリのようにimplementationして利用しています。Composite buildでもビルドキャッシュが有効なので、コードの変更がなければキャッシュが利用され、ビルド時間に大きな影響はありません。
他の案も検討しましたが、いずれもMavenを介する方法でした。KMPのコード変更のたびにMavenリポジトリに発行し、依存関係を更新する必要があるため、開発体験が悪くなる可能性があり、見送りました。
採用したアーキテクチャ
状態管理のしやすさとコードの複雑性の低さを保つため、単方向データフローのアーキテクチャを採用しています。具体的には、すでにAndroidチームで採用しているFluxアーキテクチャを利用しています。
Fluxアーキテクチャは、画面の初期化時またはユーザ操作時にActionを生成し、Dispatcherが適切なStoreにActionを送信し、StoreではActionに応じて次の状態(State)を生成し、Viewはその状態(State)に従い画面を描画します。
KMPでFluxのView以外を実装し、View部分はiOS/Androidのネイティブで実装・描画します。次の図のようにユーザ操作でKMPで記述するAction生成コードを実行し、その結果の状態をKMPから受け取り描画します。
KMPの内部は次のような構成となっています。各クラスの役割解説は長くなるため、今回は割愛します。
KMPモジュールの構成
次のようなマルチモジュール構成で開発しています。
- sharedモジュール
- 各種モジュールを1つにまとめ、各OSで必要な処理を実装するモジュールです。
- featureディレクトリ
- 機能ごとのモジュール群がこのディレクトリに含まれます。
- coreディレクトリ
- アプリケーションの基盤となるモジュール群がこのディレクトリに含まれます。fluxやmodel、local(OSネイティブとのブリッジ)など、データモデルや共通処理を提供するモジュールが含まれています。
- dataディレクトリ
- データの取得や保存を担当するモジュール群がこのディレクトリに含まれます。Repositoryの実装やネットワークアクセス、ローカルデータベースの操作を行うモジュールが含まれています。
エラーハンドリング
エラーハンドリングには、各画面で行う必要がある一般エラーハンドリングと、全画面で統一して行う必要がある共通エラーハンドリングの2種類に分類しています。
- 一般エラーハンドリング
- 例:データが見つからない場合や通信ができなかった場合のエラー処理。
- RemoteDataSourceにてHttpStatusCodeを元に分類し、エラー内容に応じてStateまたはEventとしてViewに通知します。
- 共通エラーハンドリング
- 例:認証エラーによる強制ログアウトなど。
- KtorのCustom Pluginsを用いてハンドリングしており、HttpClient利用側で共通エラー処理を一切意識しなくて良い設計になっています。
テスト
Sansanアプリでは、ロジック部分に対してユニットテストを実施しています。
使用している主なライブラリは以下の通りです。
- kotlin.test: Kotlinの標準テストライブラリ
- MocKMP: モックライブラリ。ただし、interface以外はMock化できないため、巨大なdata classを実インスタンス化する必要があり、その点が難点です。
KMPではKotlin/Nativeに対応したライブラリしか使用できないため、JUnit、AssertJ、MockKなど、Android開発で一般的に使われるライブラリは利用できません。
また、KoTestの導入も検討しましたが、共通ロジックのテストがIDEから個別実行できない(コマンドラインでは実行できるが全テストになってしまい、開発中の取り回しが悪い)ため見送りました。
このため、複数パターンのテストを、Theoryなどを用いて効率的に書けず、1つずつテストコードを書かなければならないのが現状です。
現在、テストに関する改善タスクが積まれており、今後さらに検討していく予定です。
KMPで使用しているその他ライブラリ
- moko-mvvm
- AndroidでActivity再生成時にデータを保持するために使用しています。
- expect/actualの仕組みにより、AndroidではAACのViewModel、iOSでは通常のクラスが適用されます。
- Android: AACのViewModelのライフサイクルに対応
- iOS: Viewのライフサイクルに対応
- Koin
- KMPに対応したKotlin用のDIフレームワークです。
- Ktor
- JetBrains社が提供しているKotlinの純正Webアプリケーションフレームワークで、API通信時のHTTPクライアントとして利用しています。
- Coroutines
- Kotlinで非同期プログラミングを簡単に行うためのライブラリです。
- Napier
- KMP用のサードパーティーのログライブラリです。
- SKIE
- iOS側で自然なSwiftコードでKotlin Multiplatformのコードを扱えるようにするライブラリです。KMPがKotlinをObjective-Cのライブラリに変換する過程で、Enumの網羅性担保などSwiftのメリットが損なわれてしまう弊害を解消してくれます。
最後に
Kotlin Multiplatform(KMP)の導入は、Sansanアプリの開発プロセスに多くのメリットをもたらしました。共通コードの利用でOS間の仕様差異が解消され、修正の迅速化や開発コストの削減が期待されています。また、エンジニア間の交流も活発化し、リソース配分の柔軟性も向上しました。
しかし、導入初期の段階であるため、手探りでの開発が続いており、いくつかの課題も依然として存在しています。これらの課題に取り組みながら、今後も試行錯誤を重ね、KMPの活用を進めて、より効率的で高品質なアプリ開発を目指します。
また、共にSansan / Eightのモバイルアプリ開発を進めていく仲間を募集中です!選考評価なしで現場のエンジニアのリアルな声が聞けるカジュアル面談もありますので、ご興味のある方はぜひ面談だけでもお越しいただけたら幸いです。