はじめに
こんにちは。
株式会社アイスタイルで @cosme アプリのAndroidエンジニアをしている鈴木と申します。
以前、Compose Multiplatform(KMP)で未来を切り拓く: @cosmeアプリの取り組み事例紹介という記事を書かせていただきました。
あれから約2年が経過し、実際のプロジェクトで運用を続けてきた結果、Compose Multiplatformが@cosmeアプリ開発にどのような変化をもたらしたのかを紹介します。
導入初期と現在の運用方針
導入初期の取り組み
Compose Multiplatformがまだアルファ版だった2023年ごろ、まず店舗一覧画面に適用しました。
| iOS | Android |
|---|---|
![]() |
![]() |
このPoC(概念実証)を通じて、Compose Multiplatformで問題なく開発を進められることを確認し、本格的に実際の業務案件でも利用する運びとなりました。
注釈: 導入初期に適用した店舗一覧画面は、後にWebViewへ置き換わったため、現在はCompose Multiplatformを利用した画面ではありません。
現在の運用方針
検証の結果、アイスタイルでは、新規案件の画面のみ Compose Multiplatformを利用した開発を進めるという方針になりました。
新規画面や既存画面の一新(リプレイス):Compose Multiplatformを導入。
既存コードの改修:既存のコードを活かして対応。
この方針に基づき、現在の @cosme でのCompose Multiplatformの運用フローと、具体的な事例を紹介します。
導入事例の紹介
@cosme アプリでは、すでにいくつかの案件でCompose Multiplatformを利用して共通Viewが実装されています。主な事例を2つ紹介します。
1.RNA肌診断
カメラ撮影画面を除き、診断開始から診断結果や履歴を見るまでの一連の画面がCompose Multiplatformで作成されています。
| 診断トップ画面 | 生年月日画面 | 写真撮影説明画面 |
|---|---|---|
![]() |
![]() |
![]() |
| 診断中画面 | 診断結果画面 | 診断履歴画面 |
|---|---|---|
![]() |
![]() |
![]() |
大きな案件での適用でしたが、共通コードで問題なく実装を完了することができました。
検索
@cosme アプリの検索機能に関する画面群も、Compose Multiplatformで新規リプレイスされました。対象は、検索結果一覧、ブランドから探す、カテゴリから探す、絞り込み条件の各画面です。
| 検索結果一覧画面 | ブランドから探す画面 | カテゴリから探す画面 | 絞り込み条件画面 |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
この案件では、RNA肌診断での経験を活かし、チームメンバーがCompose Multiplatformの開発に慣れたことで、iOSエンジニアも主体的に共通コードでの開発を進められるようになりました。
複数の実案件で運用して見えたメリット・デメリット
メリットについて
1.工数の削減
最も大きな効果は工数の削減です。
AndroidとiOSでそれぞれにかかっていた工数が、共通コード化によってシンプルに減少しました。初期はKotlin未経験のメンバーへのキャッチアップに時間を要しましたが、チーム内でのナレッジシェアやペアプログラミング、AIの活用なども相まって、案件を重ねるごとに開発スピードが向上しています。
工数が減ったことで、ある程度スピードを持って開発に取り組めています。
2.仕様差分の減少
Viewまで同じコードで書くことで、OS間で認識の齟齬が生じる場面が大幅に減りました。
完全に全てを共通化しているわけではありませんが、コミュニケーションロスによる仕様差分の発生や、それに伴う手戻りが明らかに減少しています。OSごとの出し分けが必要な場合も、expect/actualを効果的に利用することで、共通コードの枠内で対応できています。
3.テストコードについて
Sharedモジュール(共通コード部分)に関しては、ViewModelまで単体テストを記述しています。
カバレッジ計測:Koverを利用し、ブランチカバレッジを計測。ViewModel/Domain層などのロジック部分を100%で運用。
運用ルール:共通部分の実装時にはテストコード記述を必須としています。プルリクエスト作成時にKoverによる計測結果が自動で表示される仕組みです。
ブランチが発生し得ない特殊なケースを除き、確実にテストケースを網羅するように徹底しています。
ブランチが発生し得ない特殊なケースとは、例えば「条件分岐(if/when/for/try-catchなど)を含まない」data classの定義が該当します。
data class TestDataModel(
val id: Long,
val name: String
)
このような場合は、次のように build.gradle.kts で除外指定を行うことで計測対象から除外としています。
kover {
reports {
filters {
excludes {
classes(
"your.package.path.TestDataModel*"
)
}
}
}
}
4.コミュニケーション機会の活発化
AndroidエンジニアもiOSエンジニアも同じようにKotlinを書く機会ができたため、Kotlinでの書き方はこういうものもあるといったようなコミュニケーションの機会が増えました。
プルリクエストでレビューをする際にもこういった書き方もありますというような形で、自然と知識共有が増えていると思います。
デメリットについて
1.各OS起因で起きる不具合に遭遇する
共通コードであっても、OS特有の表示差や挙動の違いから不具合に遭遇することがあります。
【事例1:テキスト上部の余白】 AndroidでのみTextコンポーネントに微妙な上部余白が発生し、iOSと表示がずれる現象がありました。
| 修正前 | 修正後 |
|---|---|
![]() |
![]() |
commonMainにて、expectでVerticalUpperTextSpacerを定義し、android/ios側ではactualでそれぞれ処理を分ける形で対応しました。
// commonMain
@Composable
internal expect fun VerticalUpperTextSpacer(size: Dp)
// androidMain
@Composable
internal actual fun VerticalUpperTextSpacer(size: Dp) =
Spacer(modifier = Modifier.size(if (size.value <= 0) 0.dp else size * 0.32f))
// iOSMain
@Composable
internal actual fun VerticalUpperTextSpacer(size: Dp) = Spacer(modifier = Modifier.size(size))
【事例2:フォントの違い】 AndroidとiOSでフォントが異なるため、Text ComposableのFontFamilyをデフォルトのまま利用すると、iOS側のフォントが変わってしまう問題がありました。
これには、commonMainでsystemFontFamilyをexpectで定義し、iOS側でSystemFont(この場合は「Hiragino Sans」)を指定する形で対応しました。
// commonMain
internal expect object FontProvider {
@OptIn(ExperimentalTextApi::class)
val systemFontFamily: FontFamily
}
// androidMain
internal actual object FontProvider {
@OptIn(ExperimentalTextApi::class)
actual val systemFontFamily: FontFamily
get() = FontFamily.Default
}
// iOSMain
internal actual object FontProvider {
@OptIn(ExperimentalTextApi::class)
actual val systemFontFamily: FontFamily
@Composable
get() = FontFamily(
SystemFont("Hiragino Sans"),
SystemFont("Hiragino Sans W1", FontWeight.W100),
SystemFont("Hiragino Sans W2", FontWeight.W200),
SystemFont("Hiragino Sans W3", FontWeight.W300),
SystemFont("Hiragino Sans W4", FontWeight.W400),
SystemFont("Hiragino Sans W5", FontWeight.W500),
SystemFont("Hiragino Sans W6", FontWeight.W600),
SystemFont("Hiragino Sans W7", FontWeight.W700),
SystemFont("Hiragino Sans W8", FontWeight.W800)
)
}
このように、共通コードを利用しつつも、OS特有の差異はexpect/actualで吸収することが重要です。
2.ライブラリがそれほど多くはない
アルファ版の頃に比べればCompose Multiplatform対応ライブラリは増えましたが、特定の高度な機能を実現したい場合、まだ自前での実装が必要になることがあります。
例えば、RNA肌診断の案件では、思ったようなグラフ描画系のライブラリが見つからず、最終的にCanvasを用いてグラフ部分を自前で実装しました。
| 肌状態グラフ | 肌年齢グラフ |
|---|---|
![]() |
![]() |
実装にあたっては、Compose MultiplatformでCanvasを用いたグラフ機能の実装方法についてを書かせていただきましたので、こちらで説明している内容を駆使すれば実現できると思います。
3.UITableViewCellとの相性が悪い
UITableViewCellに組み込もうとすると、一見すると表示がうまくいくように見えていたのですが、何度かスクロールをしているうちに表示されなくなる現象を確認しています。
部分的に置き換えを検討していたものの、UITableViewCell周りでiOSのみうまく表示できない問題があるため、現在は新規画面実装時にのみCompose Multiplatformを使っていくこととしています。
以前、Compose MultiplatformをUITableViewCellに組み込んでハマった話に詳細を記載させていただいております。
デメリットはいくつかまだ残っているものの、ライブラリ側も更新されているので徐々に改善されていくと思います。
困った時の対処法
なかなか困った場面にも遭遇してきたので、その時どうやって対応したか対処法についても紹介します。
AIに聞いて確かめるのも良いとは思います。
しかし、いくつかの情報から判断した方が良いと思いますので、困った時参考にしていたところをあげたいと思います。
公式の質問は、YouTrackに上がっています。何かがおかしいなと思った時は、こちらからまずは確認するのが良いです。
また、サンプルなどで確認したいときは、Kotlin Confのアプリのコードを参考にするのがおすすめです。何かこういうのがあればいいのに、というときにこちらを参考にすると実は参考になるコードが載っていたりします。
おわりに
@cosme アプリでのCompose Multiplatformに対する取り組みを2年間運用したことについての紹介でした。
日々新たな問題にあたりながらも、共通で開発できることはスピード感を持って取り組むことに大きな貢献をしています。
また進展がありましたら記事にしたいと思います。今後の展開に注目していただければ幸いです。















