🎢

iOS/Android両方のアプリを作るならCompose Multiplatformがベストだと思う

2024/12/06に公開3

こんにちは!sugitaniと申します。

Black Cat CarnivalというSNSアプリを開発しています。現在ベータテスト中です。

本稿はBlack Cat CarnivalでのCompose Multiplatformの採用事例の紹介と、それを例にとったCompose Multiplatformのオススメを行います。

"Compose Multiplatformがベストだと思う"なんてたいそうなこと言ってるけど、言うほど開発のことわかっているのかい?

…と思われるかもしれないので経歴をご紹介させていただきます

  • これまで主に開発してきたもの ⇒ ニコニコ生放送 / GANMA! / SUGAR
  • iOS開発経験10年超 - GANMA!でSwiftベータ版時代から利用、現在も日常的に開発中。UIKitはもちろんSwiftUIも実戦で利用中。Swift6ガチ対応の難しさに血涙を流している
  • Android開発経験10年超 - 同様にGANMA!時代から開始、現在も日常的に開発中。Jetpack Composeもバリバリ利用

iOS/Androidのどちらに関しても経験は十分だとおもいます。

前提: Compose Multiplatformとは?

Android開発で使われているJetpack Composeは、iOS開発でいうSwiftUIに相当する宣言的UIフレームワークです。Compose MultiplatformはKotlin Multiplatform(JetBrains社がコア)とJetpack Compose(Googleがコア)をベースに、Jetbrains社が開発しているものです

Android/iOS/デスクトップ/Web(wasm)で動くアプリを作ることができます。

どれぐらい使い物になるものが出来る?

百聞は一見にしかず、ということでiOS限定で申し訳ないのですが、ベータ版のBlack Cat Carnivalをお試しください

https://bcc.cc

  1. TestFlightをインストールする(コード入力は求められるが無視する)
  2. https://testflight.apple.com/join/qZc99ykx を開いて TestFlightで表示 を押す

で簡単にテスト参加できます。

Androidをよく知っている方ならAndroidを感じるかもしれませんが、おそらく普通のネイティブアプリに見えるとおもいます。実戦利用可能レベルでヌルヌルさくさく動きます。

ちなみにアプリのiOSアプリの配布サイズは34MBです。極端に大きくなったりはしません。

ベストだと思う理由の前提

FlutterやReact NativeなどマルチプラットフォームフレームワークにとってAppleやGoogleの開発プラットフォームは地面といえます。しかしAppleもGoogleも自社プラットフォームの機能強化を絶え間なく行っているので地殻変動が頻繁に発生します。

フレームワークの開発コミュニティは地殻変動に対応するために開発を続けなければいけませんが、そのコストは膨大で、何らかの理由でコストが十分払えなくなってくると問題が徐々に増えていくことになります。

実際Flutterですらコアチームへの兵站は十分ではないようで、あり方を模索することになっています。

Compose Multiplatformは少し特殊で、Androidの基本開発環境であるComposeの活用です。Compose自体が廃れる可能性は5年程度であれば無いと思われますので、万が一Compose Multiplatformプロジェクトが放棄されたとしてもAndroid資産として活用することは容易でしょう。

iOS対応部に関しても問題ない品質が出せることが確認できたので、多くの場面でCompose Multiplatformを選択にいれるのがよいのではないか?と思います。

他にも存在がGoogleやJetBrainsの思惑と一致しやすいのではないかとか、iOS側からみるとただのObj-Cのライブラリでしかないので地殻変動から逃れやすいのではないか(UnityやUnreal Engineもそう)、などもあります。

iOS+Android開発 vs Compose Multiplatform

上記の理由から、Compose Multiplatform(とゲーム系)以外のマルチプラットフォームフレームワークには否定的です。

何かを作る際、iOS開発チームとAndroid開発チームを持つべきか、Compose Multiplatformで開発するか、の二択が現実的な選択肢ではないかとおもいます。

理想はもちろんiOS開発チームとAndroid開発チームをもち、地殻変動に対応できるスキルを会社としてキープすることです。仮にCompose Multiplatformを採用しました、iOS部分が腐ったのでどうにかいないといけないです、という事態が発生したとき対応できなければ厳しいことになるでしょう。

ただ、同じ機能を2回、2チームで開発するというのは恐ろしいコストです。年単位の人件費で考えると2500万が5000万になる、みたいな話です。フットワーク的にもよろしくないですし、理想をとれない場面は多いでしょう。

正直、iOS開発ができてAndroid開発が習得できない(またはその逆)というのは無いと思うので、両方できる1チームで2回実装、という形もあるかもしれません

…が、それだったらCompose Multiplatformで1チーム1回実装でいいじゃない?と思うのです。

まとめ

  1. マルチプラットフォームフレームワークは地殻変動と付き合っていく必要がある
  2. 地殻変動に対応するコストが払えないと問題が発生するし、実際Flutterですらコストが払いきれない問題が発生している
  3. Compose MultiplatformはAndroid方面だけは盤石で、万が一放棄されてもAndroid資産として残る
  4. iOS対応も問題ない品質が出せる今であれば、Compose Multiplatformの採用は検討に値するはずだ
  5. 理想はiOSもAndroidもフル開発だがコスト高い
  6. 半分は堅牢であるCompose Multiplatformはいい位置では?

Compose Multiplatformの開発体験につらさはないのか?

Android側の開発は完全に普段のAndroid開発と同じです。ビルドも早くAndroid Studioも快適に動きます。Android対象に関しては困ることは少ないでしょう。

iOS側は大きめの難点があります。Kotlin Multiplatform部(Compose Multiplatform含めて、Kotlin MultiplatformによりObj-cライブラリになる共通部分)のコンパイルがかなり遅いです。

一工夫が必要になります。

一工夫の例

逆に、共通部に変更がなければビルドキャッシュが利き、普通のビルド速度になります。乱暴なのですがKotlin部分でinterfaceを定義、 Swift部で注入とするといい感じになります。

具体例を記載します

1. interfaceだけ決めてしまう

Envitonment.ios.kt
actual fun isDebugBuild(): Boolean = currentDebugBuild ?: true

var currentDebugBuild: Boolean? = null

2. Swift側からinjectする

iOSApp.swift
@main
struct iOSApp: App {
    // register app delegate for Firebase setup
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

    init() {
        #if DEBUG
            Environment_iosKt.currentDebugBuild = true
        #else
            Environment_iosKt.currentDebugBuild = false
        #endif}

この例ではBooleanを注入の例にしていますが、関数でも問題なく注入できます。

Kotlin側でiOS専用処理も普通に書けるのですが、個人的にはiOSの処理は素直にSwift+Xcodeで書いて注入してしまうのがオススメです。

以上です

この記事は「お願い!みんなBlack Cat Carnivalのベータテストに加わって!(やりたい検証があるんです)」をモチベーションとして書いたものですが、考え方としては正直なものです。

もっと贅沢を言えば、Swiftは6でStrict concurrency checkingというメモリが複数スレッドから触られるのを絶対防ぐマンが導入されておりKotlinと比較して言語としての正しさは頭一つ抜けたと考えています。 Swift⇒Androidを目指しているSkipというものがありますが、Appleとしてこれをやってくれるとたぶんベストなんだけどなー…?などと考えています。

この記事が、どなたかのお役に立てれば幸いです。

お読みいただきありがとうございました

よろしければ昨日書いたこちらもどうぞ

https://zenn.dev/bcc/articles/bd4aef508d83b5

Black Cat Carnival

Discussion

utamoriutamori

とても参考になりました!
iOSのほうのFlavor分けってどうしたらいいんでしょうか。ステージング、本番など。

sugitanisugitani

Xcode側の操作でprodのビルドターゲットを追加します。元かあるターゲットをdevとします
Build Settings > Active Compilation Confitionsで

dev + debug = "DEBUG"
dev = ""
prod + debug = "PROD DEBUG"
prod = "PROD"

とします

それからKotlin側で

Envitonment.ios.kt
// ProductFlavorはexpect側で定義
actual fun getProductFlavor(): ProductFlavor = currentFlavor ?: ProductFlavor.Local
var currentFlavor: ProductFlavor? = null
actual fun isDebugBuild(): Boolean = currentDebugBuild ?: true
var currentDebugBuild: Boolean? = null

としてから、Swift側でアプリ起動時に

iOSApp.swift
@main
struct iOSApp: App {
    // register app delegate for Firebase setup
    @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

    init() {
        #if PROD
            Environment_iosKt.currentFlavor = ProductFlavor.production
        #else
            Environment_iosKt.currentFlavor = ProductFlavor.local
        #endif
        
        #if DEBUG
            Environment_iosKt.currentDebugBuild = true
        #else
            Environment_iosKt.currentDebugBuild = false
        #endif
        

として注入します

utamoriutamori

丁寧にありがとうございます!actual expect便利ですね!