Kotlin Multiplatform Projectについて話すこと・話さないこと

Yasuhiro Shimizu氏(以下、Shimizu):それでは「ドットマネーでKotlin Multiplatform Projectを導入してみて」というタイトルで15分ほど話します。自己紹介です。清水泰博と言います。ドットマネーというCAの子会社でAndroidアプリを書いたり、Webフロントを書いたりしています。

ソーシャルアカウントはTwitter、GitHub、書いてあるとおりです。コミュニティ活動では「AndroidDagashi」というAndroidエンジニア向けのニュースまとめサイト的なものを運営しています。

今日話すことについてです。Android1人、iOS1人のチームでKotlin Multiplatform Project(以下、KMP)を採用して新規アプリを作った中で、コードをどれくらい共通化できたのか、どんなアーキテクチャを採用したのか。あとは使ってみた中でのメリットデメリットについてを、Androidエンジニアの視点から軽く話していこうと思います。

基本的なことは話しません。技術の詳細についても、時間がないので話しません。気になった点があれば、ぜひ質問をお願いします。

ドットマネーアプリのおさらいとコード内訳

まずは前提知識として、アプリについてざっくり説明します。ドットマネーはポイントを管理したり、交換するサービスです。例えばクレジットカードなど、いろいろなポイントサイトのポイントをドットマネーでまとめて、Amazonギフト券とかdポイントなどの別ポイントに交換する機能を提供しています。

アプリのAPIはGraphQLを使っていて、参照系……JSONを読み込んで表示するのがメインです。更新系もありますが、ごく一部です。参照系ばかりのシンプルなアプリになっています。

コードの内訳です。これはGitHubから持ってきたものですが、Kotlinがだいたい48パーセントを占めています。これだとノイズが多いので、もう少し条件を絞って数字を出してみます。

Objective-CやJavaは、社内のいにしえのライブラリなので、除外します。ほかにもレイアウトファイルなどを除いて、純粋にKotlinとSwiftのコードのみで比較してみると、こんな感じです。

全体に対してKMPのコードが68パーセント、Android、iOS固有のコードがそれぞれ16パーセントです。これはiOS、Androidを合わせた全体に対しての数字なので、各OSのみで計算すると、8割近くが共通コードになっています。

アーキテクチャについて

アーキテクチャについて、ざっくり説明します。リポジトリ構成はモノレポになっています。KMPのコードとAndroidのコード、iOSのコードを1つのレポジトリで管理しています。ディレクトリ構成は右の画像のとおりです。下線の引いてあるディレクトリと、その中がKMPのモジュールになっています。

モノレポを採用した理由はいくつかありますが、一番大きいのは共通コードの読み込み方法を考えたくないからです。レポジトリを分けてしまうと、サブモジュールで読み込むことや、プライベートのmavenレポジトリを作ることなど、考えなきゃいけないことが多いので、一番シンプルな方法を取りました。

CI(Continuous Integration)はBitriseを使っています。iOS、AndroidでそれぞれCIの設定をすると、素直に動かせました。PR(Pull Request)を投げると、iOSのbuildとAndroidのbuildが同時並行で走るような状態になっています。Androidのほうがbuildにかかる時間は少ないので、Android側でktlintやlint系は実施しています。

コードのアーキテクチャについてです。よく見かけるようなレイヤードアーキテクチャを採用しています。気になる点だと思いますが、プレゼンテーション層から下はすべて共通コードです。ViewModelやユースケース、レポジトリなどは全部共通、Kotlinで書いています。

逆に言うと、OS固有のコードは基本的にアクティビティやVIewController、Viewだけになっています。

データ層のみKMPにするなど、いろいろ選択肢はありましたが、このようにしたのは、せっかくの新規アプリなので、どこまでできるか挑戦したかったのが大きな理由です。あとリソースが少なく、それぞれ1人ずつしかいないというのもありました。

プレゼンテーション層のデザインパターンとしては、MVI(Model-View-Intent)を採用しています。単方向データフローでImmutableなStateがあって、というところが特徴です。

ViewModelのインターフェイス

ViewModelのI/F、インターフェイスですが、だいたいこんな感じになっています。いくつかメソッドや変数がありますが、気にする必要があるのは、Viewへの出力となるこの部分です。statesとeffectsという2つのFlowが定義されていますが、これがViewへの出力になっています。

ただCoroutineのFlowは、iOSから直接使えないので、ここをどうにかする必要があります。どうにかする方法はシンプルです。専用のラッパークラスを用意して、Flowをコールバックに変換するだけです。

KMPからは、このラッパークラスをiOS向けのインターフェイスとして公開します。iOS側ではこれをそのまま使うこともできるし、RxSwiftのObservableに変換するラッパークラスを作ったり、いろいろできます。

「各OS向けのライブラリを使いたい」「OS固有の機能を共通側から使いたい」という場合も、もちろんあります。例としてFirebaseAnalyticsを挙げていますが、このようなコードを共通側から使えるようにするには、主に2つ方法があります。

1つはexpect/actuialという、KMPの言語機能を使う方法。もう1つは普通のAndroidアプリやiOSアプリでもよく見かけると思いますが、インターフェイスを定義して、それぞれの実装クラスを用意して、DIをするやり方です。

ドットマネーではexpect/actuialを使うとテストがちょっとしづらいので、こちらの利用は最小限にとどめ、基本はインターフェイスで書き分けてDI(Dependency Injection)するやり方を取っています。

KMPライブラリ

利用しているKMPのライブラリはこんな感じです。DIにはKodeinを使っています。非同期処理周りはkotlinx.coroutinesとCoroutineWorkerを利用しています。通信周りはhttpクライアントにKtorを使っています。JASONへのシリアライズとデシリアライズは、kotlinx.serializationです。

APIがGraphQLなので、GraphQLドキュメントからkotlinx.serialization用のデータクラスを出力するための自家製ライブラリはkgqlを使って作っています。

ローカルキャッシュ周りはSQLDelightというSQLiteのラッパーを使っていて、あとはmultiplatform-settingsというShared PreferencesとUserDefaultsのラッパーも一部利用しています。開発環境によって変わる定数、APIのエンドポイントなどは自家製のBuildKonfigというライブラリを使っています。あとログはNapierを使っています。

使って感じた5つのメリット

続きまして、実際に使っている中で感じたメリットを説明します。使い慣れたKotlinで共通コードが書けるところ、Androidエンジニアにとって一番大きいのはこれかなと思います。iOSエンジニアからは新言語なので、あんまりメリットにはならないかもしれません。

工数削減も大きなメリットでした。View以外は同じものを使いまわせます。例えばViewModel以下の実装に2日、Viewの実装に1日それぞれのプラットフォームでかかっていたとすると、ViewModel以下は使いまわせるので、6日かかっていたはずのものが4人日で実装できる感じになるわけです。

実際に開発していてすごく感動したんですが、自分がAndroid側のViewを作っているときにiOS側の人が別画面のViewModelを作っていて。Androidの実装が終わったときにそれぞれAndroid、iOS1人ずつしかいないのに、もう次の画面のViewModelができていて、すぐに画面の実装に取り掛かれるということがありました。

用語や仕様に差が生まれないのもメリットです。画面名が異なる、モデルクラスの名前が異なるなどはないので、余計なコミュニケーションコストが減らせます。ビジネス的にクリティカルな部分は実装の仕方次第ですが、基本的にすべて共通にできるので、「Androidでは実装していないです」とか、そのようなことが発生しづらくなります。

これは考え方によってはデメリットかもしれませんが、KMPでは基本的にViewを各プラットフォームで書くことになるので、それぞれの特性やデザインガイドラインを意識したUIを、最適な方法で作れます。

Jetpack Compose使ったり、Swift UI使ったり、そういうことも可能です。最新OSの機能も自由に使えます。将来的にはComposeがiOSに対応してUI部分も一緒に書ける、そんな未来もあるかもしれません。

ドットマネーアプリではできるだけたくさんのコードを共通化することを目標に開発しましたが、もちろん、プロジェクトのニーズに応じて導入する範囲を選べます。例えば「リモートAPIクライアントだけ共通にしよう」「レポジトリ層以下、リモートとローカルキャッシュだけ共通にしよう」、そういうこともできます。

実際、サイバーエージェント内部でもリモートAPIクライアントの共通化を目標に、KMPの導入を検討しているプロジェクトが複数存在します。そのようなときに気になるのはアプリコードからの使い方だと思いますが、KMPのコードはアプリからの見え方はライブラリと特に変わらないので、自然なかたちで導入できます。

Androidであればjarやaarのライブラリに見えますし、iOSからであれば、ただのフレームワークです。そのため、それぞれのOSの仕様に則って普通にインポートして使えます。

4つのデメリットと注意点

続いて、デメリットです。一番大きいのはやはり学習コストだと思います。iOSの人にKotlinを学んでもらう必要があります。いくらKotlinとはいっても、Kotlin/Nativeにはいろいろ独自の仕様があるので、Androidをやっている人でも苦戦することがあります。

一番顕著なのがスレッド周りで、なにも考えずに変数をスレッドをまたいで使おうとすると、Exceptionを吐いてクラッシュすることはよくあります。ただ、iOSで動けばAndroidでは基本は動くので、テストを書いてKotlin/Nativeで実行しながら開発すると、だいたいうまく動かせます。

あと日本語の資料が少ないのも、人によっては大きな障害になるかと思います。幸い、英語のコミュニティはそこそこに活発なので英語の資料はけっこうありますし、Kotlinの公式Slackもそこそこ活発に動いています。

これけっこう忘れがちですが、実はまだα版です。そのため、破壊的変更もたまにあります。最近ドットマネーで引っかかって今も苦戦しているのが、Kotlin1.4からKtorがマルチスレッド対応のCoroutine必須になってしまったところです。粛々とやっていくしかありません。

将来的にKotlin/Nativeのメモリ管理モデルが完全にリプレースされることになっているので、そのあたりも注意が必要です。あとはよくわかりませんが、IDEが真っ赤になることもたまにあります。

あと、ライブラリが少ないのもあります。これは、考え方によってはコントリビュートのチャンスです。私自身も3つくらい、ライブラリを書いて公開しています。

KMPは自信をもって「使ってよかった」と言える

まとめです。KMPを使ってよかったかと言うと、これは自信を持ってイエスと言えます。コードの共通化を無理なく使いやすい言語でできているし、工数の削減もできました。ビジネスロジックのみ共有したい場合には、有効な選択肢になると考えています。普通のライブラリに見えるので、既存プロジェクトへの導入という観点からしたら容易です。

オススメできるかの観点だと、ちょっと条件付きでイエスになります。先ほどちょっと述べましたが、まだ資料が少ないので、ある程度自分でコード読んだり、英語の記事を読んだり、そういうことをする必要があります。

経験の浅い人が多いチームでKMPでやりたいとなった場合は、もしかしたらもうちょっと安定するのを待ったほうがいいかもしれません。選択肢として選べるのであればFlutterのほうがリソースは多いし、日本人コミュニティも大きいのでいいかもしれません。あるいは、普通に実装して経験を積んでいくのも選択肢としてあると思います。

以上です。ご清聴ありがとうございました。