2025年6月〜8月にかけて実施した「ZonePane の iOS 版開発」における技術的な知見をまとめたものです。
レガシーな技術を比較的多く含むAndroidアプリの Kotlin Multiplatform / Compose Multiplatform (CMP) 構成への移行を行い、iOSへの移植を行った記録です。
備忘録的な側面もありますが、特にレガシーなAndroidアプリのKMP/CMP移行を検討されている方には、具体的な課題と解決策のヒントを提供できるかと思います。
AI(Cursor, Claude Code)も活用しましたが、特に大規模なリファクタリングや移行作業において、その効果を実感しました。
2010年に開発を開始したTwitter用アプリTwitPaneをベースとし、Mastodonに対応したアプリとしてZonePaneの開発が始まりました。2023年2月頃です。
その後、Misskey、Blueskyにも順次対応していきました。
アプリの構成
技術的なポイントとして、TwitPaneと同じソースコードを共有しながら複数のアプリを開発していることがあげられます。
具体的には1つのプロジェクト内に下記のアプリが存在します。
アプリ名 | 対応 |
---|---|
ZonePane (ぞーぺん) | Bluesky、Mastodon、Misskeyに対応 |
BluePane | Blueskyに対応 |
TwitPane無料版 | Bluesky、Mastodon、Misskeyに対応、Twitter対応(APIが利用不可のため実質非対応) |
TwitPane有料版 | Bluesky、Mastodon、Misskeyに対応、Twitter対応(APIが利用不可のため実質非対応) |
ついぺんリサーチR | X閲覧専用 |
たいぺん | タイッツー対応 |
ビルドは下記のコマンドでAndroid版を生成できるようにしてあります。
Android版のZonePaneは Mastodon, Misskey, Bluesky に対応していますが、iOS版は2025年8月末時点では Bluesky にのみ対応しています。
(今後 MastodonとMisskey にも対応していく予定です)
なぜ Bluesky から対応したのか
iOS版を作るための技術として Compose Multiplatform (CMP) を前提としていますが、Android版は Mastodon と Misskey が Android View (RecyclerView) で、Bluesky が Compose (LazyColumn) で実装されており、CMP に一番近い構成で実装されている Bluesky が iOS に最短距離で対応ができる可能性が高いことから、まずは Bluesky から対応することとしました。
▼Android版の現状
Mastodon : AndroidView+RecyclerView
Misskey : AndroidView+RecyclerView
Bluesky : Jetpack Compose+LazyColumn
▼iOS に対応するためには、、、
AndroidView(RecyclerView) -> Jetpack Compose(LazyColumn) -> Compose Multiplatform(LazyColumn)
という移行作業が必要
とはいえ Bluesky 部分も Compose 実装なのは「タイムラインの描画のみ」で、その周辺の「タップしたときのメニュー」や「いいねやリポスト等の確認画面」、アプリ自体のメイン画面や設定画面、投稿画面なども Android View で作られていることから、大幅に作り込む必要があります(ありました)。
いわば、フルCompose版のAndroidアプリを作り、それをSwiftからライブラリとして呼び出してiOS版を作るようなイメージです。ほとんど作り直しですね。
レイヤー構成
Android View (RecyclerView) + Compose (LazyColumn)
ViewPager + Fragment
ViewModel
Presenter
UseCase
Repository
domain
common
モジュール構成
common, domain が基本にあって、その上に shared/core, shared/ui_core, shared/core_compose などが載り、各種機能モジュール、ライブラリ、アプリが載る構成ですが、15年も維持しているアプリなのでまあカオスですね。。
CMP/iOS対応で core -> core+android_core や domain -> domain+android_domain のように Android 専用のモジュールが増えたりしていて、改めて整理する必要性を感じます。
ViewPager に載せる Fragment をアプリ内で「PagerFragment」と呼んでおり、これをまとめたモジュールを pf_bsky
のように pf_*
と称しています。
下記にモジュール構成の詳細を アプリ内のREADME.md より抜粋すて記載しましたが、長いので折りたたんでおきます。
モジュール構成の詳細(README.mdより抜粋)
アプリ一覧 (app_*)
アプリ名 | パッケージ名 | 対応サービス | KMP/CMP対応 | ファイル数 | 説明 |
---|---|---|---|---|---|
app_free | com.twitpane | Twitter, Bluesky, Mastodon, Misskey | Android | [11] | TwitPane無料版 |
app_premium | com.twitpane.premium | Twitter, Bluesky, Mastodon, Misskey | Android | [9] | TwitPane有料版 |
app_research | com.twitpane.search | Android | [8] | ついぺんリサーチR | |
app_zonepane | com.zonepane | Bluesky, Mastodon, Misskey | Android | [9] | ZonePane(ぞーぺん) |
app_taipane | com.taipane | タイッツー | Android | [8] | たいぺん |
app_bluepane | net.bluepane | Bluesky | Android | [8] | BluePane (Bluesky専用) |
app_zonepane_cmp | com.zonepane.cmp | Bluesky, Mastodon, Misskey | KMP/CMP | [6/12/9] | ZonePane CMP版 (開発中) |
iosApp (ZonePane for iOS) | – | Bluesky, Mastodon, Misskey | KMP/CMP | – | iOS版 (SwiftUI + CMP) |
モジュール一覧(アプリ用ライブラリ、Featureモジュール) (modules/*)
モジュール名 | 対応サービス | KMP/CMP対応 | ファイル数 | 説明 |
---|---|---|---|---|
▼コア機能 | ||||
app_base | 全サービス | Android | [3] | 各アプリの共通ベース |
android_common | 全サービス | Android | – | Android共通処理 |
android_core | 全サービス | Android | [93] | Android共通コア機能 |
android_db_zstd | 全サービス | Android | [1] | データベース用Zstd圧縮 |
android_domain | 全サービス | Android | – | Androidドメイン層 |
android_main | 全サービス | Android | [82] | メイン機能 |
android_shared_api | 全サービス | Android | [28] | Android共有API |
android_shared_impl | 全サービス | Android+Compose | [1] | Android共有実装 |
cmp_main | 全サービス | KMP/CMP | [2/14/2] | メイン機能 (CMP版) |
▼認証・アカウント関連 | ||||
login_bluesky | Bluesky | KMP対応 | [5/4/0] | Blueskyログイン |
login_common | 全サービス | KMP対応 | [3/8/0] | ログイン共通処理 |
login_fediverse_bluesky | Mastodon, Misskey, Bluesky | Android | [27] | Fediverse+Blueskyログイン |
login_ob_bluepane | – | Android+Compose | [3] | BluePane用オンボーディング |
login_ob_research | – | Android+Compose | [3] | Research用オンボーディング |
login_ob_taipane | – | Android+Compose | [6] | TaiPane用オンボーディング |
login_ob_twitpane | – | Android+Compose | [3] | TwitPane用オンボーディング |
login_ob_zonepane | – | KMP対応 | [1/2/0] | ZonePane用オンボーディング |
login_ob_zonepane_cmp | – | KMP対応 | [0/2/0] | ZonePane CMP用オンボーディング |
login_tw | Android | [7] | Twitterログイン | |
▼ページャフラグメント系 (pf_*) | ||||
pf_api | 全サービス | KMP対応 | [19/2/0] | プラットフォームAPI |
pf_bsky | Bluesky | KMP対応 | [63/111/3] | Blueskyプラットフォーム |
pf_core | 全サービス | KMP対応 | [26/24/0] | プラットフォームコア |
pf_fediverse | Mastodon, Misskey | Android | [290] | Fediverseプラットフォーム |
pf_legacy_timeline | Twitter, Mastodon, Misskey | Android | [26] | レガシータイムライン |
pf_tai | タイッツー | Android+Compose | [52] | タイッツープラットフォーム |
pf_tw | Android | [172] | Twitterプラットフォーム | |
▼投稿機能 (publish_*) | ||||
publish_bsky | Bluesky | KMP/CMP対応 | [12/14/1] | Bluesky投稿 |
publish_common | 全サービス | KMP/CMP対応 | [38/6/2] | 投稿共通処理 |
publish_cross | 全サービス | Android+Compose | [18] | クロスポスト |
publish_fediverse | Mastodon, Misskey | Android+Compose | [23] | Fediverse投稿 |
publish_taittsuu | タイッツー | Android+Compose | [10] | タイッツー投稿 |
publish_tw | Android | [21] | Twitter投稿 | |
▼プロフィール編集 (profile_edit_*) | ||||
profile_edit_bsky | Bluesky | KMP/CMP対応 | [2/2/1] | Blueskyプロフィール編集 |
profile_edit_mky | Misskey | Android | [3] | Misskeyプロフィール編集 |
profile_edit_mst | Mastodon | Android | [3] | Mastodonプロフィール編集 |
profile_edit_tw | Android | [2] | Twitterプロフィール編集 | |
▼UI・表示関連 | ||||
custom_emoji_picker | Mastodon, Misskey | Android | [26] | カスタム絵文字ピッカー |
gallery | 全サービス | Android | [4] | ギャラリー |
icon_api | 全サービス | Android | [8] | アイコンAPI |
icon_impl | 全サービス | Android | [6] | アイコン実装 |
image_crop | 全サービス | Android+Compose | [3] | 画像クロップ |
imageviewer | 全サービス | Android | [6] | 画像ビューア |
movieplayer | 全サービス | Android+Compose | [12] | 動画プレイヤー |
tab_edit | Twitter, Mastodon, Misskey | KMP/CMP対応 | [14/9/0] | タブ編集 |
timeline_renderer_api | 全サービス | Android | [6] | タイムライン描画API |
timeline_renderer_impl | Twitter, Mastodon, Misskey | Android+Compose | [51] | タイムライン描画実装 |
timeline_repository | Twitter, Mastodon, Misskey | KMP対応 | [57/12/0] | タイムラインリポジトリ |
▼ユースケース | ||||
(割愛) | ||||
▼ジョブ管理 | ||||
(割愛) | ||||
▼その他機能 | ||||
(割愛) |
共有モジュール一覧 (shared/*)
モジュール名 | 対応サービス | KMP/CMP対応 | ファイル数 | 説明 |
---|---|---|---|---|
auth_api | 全サービス | KMP対応 | [1/5/0] | 認証API |
auth_impl | 全サービス | KMP対応 | [0/2/0] | 認証実装 |
billing_repository_api | 全サービス | Android | [2] | 課金リポジトリAPI |
billing_repository_impl | 全サービス | Android | [8] | 課金リポジトリ実装 |
common | 全サービス | KMP/CMP対応 | [50/26/12] | 共通処理 |
config_api | 全サービス | KMP対応 | [5/1/0] | 設定API |
config_impl | 全サービス | KMP/CMP対応 | [58/30/0] | 設定実装 |
core | 全サービス | KMP/CMP対応 | [19/84/10] | コア機能 |
core_bsky | Bluesky | KMP対応 | [14/51/0] | Blueskyコア機能 |
core_compose | 全サービス | KMP/CMP対応 | [28/86/6] | Compose機能 |
core_fediverse | Mastodon, Misskey | Android | [107] | Fediverseコア機能 |
core_taittsuu | タイッツー | Android | [54] | タイッツーコア機能 |
core_tw | Android | [90] | Twitterコア機能 | |
db_api | 全サービス | KMP対応 | [4/31/0] | データベースAPI |
db_impl | 全サービス | KMP対応 | [11/13/2] | データベース実装 |
domain | 全サービス | KMP/CMP対応 | [16/67/2] | ドメイン層 |
domain_bsky | Bluesky | KMP対応 | [0/5/0] | Blueskyドメイン |
domain_fediverse | Mastodon, Misskey | KMP対応 | [5/4/0] | Fediverseドメイン |
domain_tw | Android | [6] | Twitterドメイン | |
emoji_api | 全サービス | KMP対応 | [5/4/0] | 絵文字API |
emoji_impl | 全サービス | KMP対応 | [6/1/0] | 絵文字実装 |
mediaurldispatcher_api | 全サービス | KMP対応 | [1/4/0] | メディアURLディスパッチャAPI |
mediaurldispatcher_impl | 全サービス | Android | [28] | メディアURLディスパッチャ実装 |
ui_core | 全サービス | KMP/CMP対応 | [74/33/0] | UIコア |
unicode_emoji_picker | 全サービス | KMP/CMP対応 | [2/4/0] | Unicode絵文字ピッカー |
「ファイル数」のフォーマット説明:
- KMP対応モジュール: [Android/Common/iOS] = [androidMain/commonMain/iosMain ディレクトリの.ktファイル数]
- KMP非対応モジュール: [ファイル数] = [src/main/kotlin または src/main/java ディレクトリの.ktファイル数]
- KMP対応の判定基準: commonMainディレクトリに.ktファイルが存在する場合
各アプリの機能と移行・開発作業
Bluesky を対象に、何を移行し、何を作ったのかを整理してみます。
機能や構成要素 | 作業 | 備考 |
---|---|---|
アプリのメインフレーム | CMP, Compose Navigation で新規作成 | |
サイドナビゲーション | CMP 移行 | ほぼ作り直し、Android版と統合 |
オンボーディング、ログイン | CMP 移行 | わりとスムーズに移行できた |
設定画面 | CMP 移行 | ほぼ作り直し、Android版もCompose化 |
Bluesky のタイムライン | CMP 移行 | 描画だけはComposeなので同じだけどViewModelの移行が超大変だった |
Bluesky の通知 | CMP 移行 | 実装上はタイムラインと共通なので同時に作業を実施 |
Bluesky のリスト、フィード、プロフィールなど | CMP 移行 | それぞれ割と独立していてスムーズに進行した |
DB | KMP移行 | SQLDelight に移行、しんどい |
設定 | KMP移行 | Preferenceの移行、作り込むだけなので割とスムーズだった |
テーマ管理システム | KMP移行 | 設定さえできていればいいので割とスムーズだった |
技術スタック
Android版 | CMP/KMP | |
---|---|---|
DI | Koin | Koin |
DB | SQLite | SQLDelight |
設定値の保存 | Preferences | Preferences+NSUserDefaultsで独自実装 |
画面遷移 | Activity + Fragment (設定画面のみ Jetpack Navigation) | Navigation Compose |
通信 | OkHttp | ktor |
JSON解析 | Gson | Kotlin Serialization |
APIライブラリ Bluesky | kbsky | kbsky |
APIライブラリ Mastodon | mastodon4j | mastodon4j(自力でKMP対応) |
APIライブラリ Misskey | misskey4j | kmisskey |
開発期間
iOS版の開発ということはXcodeでのビルドにMacが必要なわけですが、実は私自身Macの初心者でした。というわけで5月末にクロスポスト機能が完成間近のタイミングでMac mini(M4)を発注するところからのスタートでした。
※2010年代にMacBook Airを購入したこともありましたが数日触れただけでほとんど使わずに売り払いました。。
あ、基本的に土日も作業しています。途中のジョインアライブ、RSRの期間(計4日間)以外はほぼ休みなしです。
期間(2025年) | やったこと | 備考 |
---|---|---|
▼6月 | 基盤整備、KMP/CMP導入 | |
6/2 – 6/5 (4日間) | Mac環境構築 | Mac miniのキッティングから開発環境構築まで |
6/6 – 6/7 (2日間) | build-logic, ConventionPlugin導入 | これをやらないとKMPのビルドすらできない |
6/8 – 6/11 (4日間) | KMP導入 | Appleデベロッパー登録もこの期間に。 |
6/12 – 6/14 (3日間) | CMP版試作 | フルComposeとCompose Navigationのお試し |
6/15 – 6/16 (2日間) | Universal Post 表示対応 | iOS実機で擬似的なポスト(※)を表示する。(※ 設定画面のプレビュー等で使用しているもの) |
6/17 – 6/18 (2日間) | Blueskyログイン | ログインのロジックを実行する |
6/19 | タイトルバー | |
6/20 | タブ、Deck試作 | |
6/21 – 6/23 (3日間) | ViewModel, UIState, Effect, Listener の KMP 対応 | |
6/24 – 6/26 (3日間) | 検索ロジックのViewModel | |
6/27 – 6/28 (2日間) | その他のBlueskyタイムライン等のViewModelのKMP対応 | |
6/29 | Bluesky タイムライン Compose の CMP 移行 | Compose の CMP 対応 |
6/30 | Coordinator 導入 | Bluesky タイムラインは複雑なので「タブ」を管理する Coordinator を導入。Fragment 相当 |
▼7月 | DB対応、Bluesky機能作り込み | |
7/1 – 7/3 (3日間) | アカウント一覧、DBのKMP対応 | SQLDelight 移行、TestFlight版配布も開始。 |
7/4 | 投稿 | テキストのみのシンプルな実装 |
7/5 | いいね、リポスト、画像ビューア | |
7/6 – 7/8 | サイドナビゲーション(NavidgationDrawer)、DBマイグレーション、新通知対応 | |
7/9 | プロフィール、画面遷移 | |
7/10 – 7/14 (5日間) | 設定 | ボリュームがありしんどかった |
7/15 – 7/16 (2日間) | タブのカスタマイズ、動画プレイヤー | |
7/17 | 画像投稿対応 | |
7/18 | Mastodon4J の KMP 対応 | 突然のMastodon対応 |
7/19 – 7/20 (2日間) | ジョインアライブのためお休み | |
7/21 | いいね、リポスト確認 | |
7/22 – 7/25 | タップメニュー | 表示、前後検索、カラーラベル、ユーザーサブメニューも |
7/26 | リスト一覧 | |
7/27 | フィード一覧 | |
7/28 | リスト作成・編集 | |
7/30 | プロフィール | |
7/31 – 8/1 (2日間) | タブの追加 | メインのViewModel+Screenを動的なタブ追加対応にする。しんどかった |
▼8月 | 低優先度機能の作り込み、審査対応 | |
8/2 | チャット、Unicode絵文字Picker | |
8/3 | 広告導入 | |
8/4 – 8/5 (2日間) | オンボーディング | |
8/6 – 8/7 (2日間) | ミュート設定 | |
8/8 | 通知のタップメニュー | |
8/9 | Android版の作業 | |
8/10 | 不具合修正など | |
8/11 | オンボーディング | |
8/12 | 返信制限対応、不具合修正 | |
8/13 | 体調不良で休み | |
8/14 | アカウントの並べ替え対応 | |
8/15 – 8/16 (2日間) | RSRのためお休み | |
8/17 | 画像縮小対応 | |
8/18 – 8/19 (2日間) | プロフィール編集 | |
8/20 | リロードボタン長押し対応 | |
8/21 | ダークテーマの動的切り替え対応、テーマとデザイン設定 | |
8/22 – 8/23 (2日間) | タブの長押しメニュー、画面遷移作り直し | |
8/24 – 8/25 (2日間) | 審査提出、タップメニューのショートカット | |
8/26 – 8/27 (2日間) | リジェクト対応、Android版のEdgeToEdge対応 | |
8/28 | リジェクト対応 | |
8/29 | 審査完了、App Store で公開! | |
8/30 | Android版不具合修正など、この記事を書く |
expect/actual一覧
expect/actual で iOS と Android の実装を分離するのが定番です。
expect/actual で実装した部分を一覧化することで 2025年夏時点における iOS/Android の実装分離ポイントが見えてくるかも。
※下記以外に、DIで実装を分離しているパターンなどもあります。
概要 | |
---|---|
▼メイン | |
fun createImageLoader() | ImageLoader (Coil3) |
fun platformModule() | Koinの実装分離 |
fun PlatformBackHandler() | 戻る制御(Androidのみ) |
class TrackingPermissionManager() | iOS の Tracking 関連のブリッジ |
▼タブ関連 | |
fun executeMigration() | タブデータの移行処理、旧バージョン分はiOS無関係なのでAndroid実装のままにするためexpectで分離 |
▼Compose (pf_bsky) | |
fun openMediaForBluesky() | 画像ビューア・動画プレイヤー表示 |
fun showBsMediaUrlSubMenu() | メディアサブメニュー表示(Android版のみなので分離) |
fun showImportDesignConfirmDialog() | デザインインポートダイアログ(Android版のみなので分離) |
fun showDesignConfigDialogPresenter() | 長押しメニューのデザイン設定項目(Android版のみなので分離) |
▼プロフィール編集 | |
fun saveImageFromUri() | 画像保存 |
suspend fun uploadBlobIn() | 画像アップロード |
▼投稿 | |
BlueskyPublishDelegate.convertVideoOrImagesToEmbed() | 画像・動画変換 |
fun BlueskyPublishDelegate.downloadAndUploadLinkThumbnail() | リンクサムネイル |
fun BlueskyPublishDelegate.downloadLinkThumbnail() | サムネイルダウンロード |
fun BlueskyPublishDelegate.uploadMedias() | メディア投稿 |
fun BlueskyPublishDelegate.uploadVideo() | 動画投稿(未対応) |
fun BlueskyPublishDelegate.uploadImage() | 画像投稿 |
▼投稿共通(publish_common) | |
expect class ImagePicker() | 画像ピッカー |
expect class MediaPermissionManager() | アクセス権限(Android用) |
▼共通(common) | |
expect class LruCache | iOSのLruCacheがないので独自実装 |
expect interface PrefUtil2Delegate | 設定 |
expect interface PrefUtil2EditorDelegate | 設定保存 |
expect object CodePointUtil | コードポイント処理(性能重視のためネイティブ実装) |
expect val Long.toCommaSeparatedString: String | カンマ区切り文字列生成 |
expect object MyLog | ログ出力 |
expect object PlatformBackupManager | BackupManager はAndroidのみ |
expect val MatchGroup.rangeCompat: IntRange | MatchGroup.rangeの代用 |
expect object StringUtil2 | 正規表現の互換用 |
▼共通(core) | |
expect object PlatformUtil | isAndroidやpackageName,shareやclipboard管理などいろいろ |
expect class ClipboardManager | クリップボード |
expect object TPFontPathConfig | |
expect object SystemThemeDetector | ダークモード検出 |
expect class GetAppInfoUseCase() | アプリバージョン |
expect object IOUtil2 | ファイルI/O |
expect object NetworkUtil | コネクション確認など |
expect class OfficialAppLauncher() | 公式アプリ起動 |
expect object StorageUtil2 | 内部ストレージ等の操作 |
expect fun String.Companion.formatCompat | String.format の互換実装 |
▼Compose (core_compose) | |
expect fun BlurAsyncImage | Blur表示付きImage |
expect fun ImageRequest.Builder.enableAnimation | アニメーション制御 |
expect object TPComposeFontProvider | 外部フォント指定 |
▼DB (db_impl) | |
expect object SqlDriverFactory | SQLDelight |
expect object ZstdCompressor | Zstd圧縮対応 |
▼ドメイン (domain) | |
expect fun applyPlatformLocale など | Locale設定 |
expect object PlatformTPDateTimeUtil2 | 日付・時刻の整形 |
しんどかったこと
改めて振り返ってみて CMP / iOS 対応でしんどかったことをざっくりと書いてみます。
Mac導入
そもそもCmdキー主体の操作に慣れるまでしんどかった。
AndroidStudioはWindowsに合わせてCtrlキー主体のキー設定にカスタマイズして使い始めた。
Karabiner-Elementsも導入したり。
この3ヶ月のiOS版開発で、私自身のメインPC/開発環境も Windows11 から Mac に変化し、1995年から約30年間使い続けてきたWindowsとおわかれするという大変革となりました。まあWindowsマシン自体は経理とかでどうしても必要なので残してありますが(Macの)WindowsApp経由でリモートアクセスする使い方になっています。
build-logic / KMP 導入環境
まず最初にこれがしんどかった。
applicationやライブラリモジュール用の共通部分を buildSrc 配下に書いていて、groovyファイルのapplyで共通化してたんだけど、これがあることで KMP のプラグインが導入できなかった。
そこで(泣きながら)全部 Convention Plugin 化して対応した。
アイコン (フォントベースのアイコン -> Material Icon ベースに変更)
メニュー等に使用するアイコンがフォントファイルベースの https://github.com/takke/IconicDroid を使っていたんだけど、当然これはCMPで使えないので Material Icon のファイルを取り込んで使えるようにした。
地味にこの環境を整備するだけで半日くらいかかった。
文字列リソース
TwitPane時代から複数言語対応を前提にしていたので、KMP/CMP対応部分は Compose Resources を利用した文字列リソース参照に書き換え。
Android用の大量のコードがあるのでこれと2重管理になるのを避けるために、Compose Resources 用の文字列リソースを Android 用にコピーするタスクを作って管理するようにしたり。
文字列、正規表現
MatchGroup.range が ios, android にはあるけど他のプラットフォームにはなくてIDE上エラーになるのであえて expect/actual で回避した。
日付時刻の扱い
iOS 版だけなぜか 2055 年になったりした。instant.toNSDate() 等で回避。
と、その前に java.util.Date が使えないのでまず Instant が必要で、かつ開発期間中に Instant の移行(kotlinx.datetime.Instant -> kotlin.time.Instant)もあったりした。
AnnotatedString.fromHtml
がない
気合いで実装。
(利用するタグの種類が a と br 程度だったので簡単なパーサを書いて AnnotatedString を構築した)
LinkedList -> ArrayList (mutableListOf())
ポストの表現にLinkedListを使っていたけどKMPにはないのでArrayListで。性能的にはまあ十分かな。
動画再生
https://github.com/Chaintech-Network/ComposeMultiplatformMediaPlayer を使わせてもらいました。
画像ビューア
https://github.com/usuiat/Zoomable を使わせてもらいました。
設定画面が全部View
気合いで作り直し。
ざっくり1つの設定だけ Claude Code に移行させて、自前で細かく修正してリファレンス実装を1つ作り、それと同様に Claude Code に実装してもらう、という流れで PLAN の md ファイルを作って・・・、まあ 2025年8月時点で Claude Code にお願いするときの定番の流れで。
DB層の作り直し
気合いで SQLDelight 化。
こちらも設定画面と同様に Claude Code とペアプロ。
ViewModel
Effect で個別の実装を Fragment/CMP で分離したり、Listener を最初は Fragment/CMP 用で大きく2つ用意してそれぞれが動作する状態を維持しながら徐々に統合していったり、、、
両対応にするための実装箇所をどこにすべきかはけっこう混乱するのでクラス図を用意したりして、
(ViewPagerに載せる)Fragmentを前提としたComposeのScreen+ViewModelを ViewPager/HorizontalPager 両対応に作り直すということを繰り返しました。
アプリ内の独自テーマ対応
iOSのダークテーマの扱いが全然分からなくて苦労した。
アプリ内の独自テーマシステムは結果的にはKMPに移行するだけ(ほぼディレクトリ移動のみ)だったんだけど、これが書き換わったりライト系・ダーク系テーマの切り替えをアプリ全体に反映するためにはどうするのか、、結局アプリの Compose 全体を key {} で囲むようなダサい実装になったんだけど、試行錯誤がしんどかった。
App Store 審査
審査に出したらいきなり4件も指摘されてびっくりして泣いちゃいましたが、いずれも真っ当な指摘だったので淡々と修正し、結果的には1往復で審査完了しました。
指摘されたのは、
・ストアの掲載内容
・アプリ内の機能について(2件)
・広告関連(IDFA関連)
でした。
15年ほどかけて作ったAndroidアプリ(の一部)をiOS対応したわけですが、こう振り返ってみてもやっぱり1人で3ヶ月でやる量じゃないと思います。
AI時代のスピード感は人間が疲弊するので用法用量を守って正しく使いたいものです。
(・・・あ、APIの上限のことじゃないよ。。)
AndroidアプリのCMP対応を考えている方の参考になれば幸いです。
さて、Mastodon対応はどうやろうかな・・・。
Views: 1