こんにちは、エンジニアのはまぐちです。
普段はAumo(アウモ)というおでかけコミュニティサービスや観光DX SaaSの開発を行っています。
今回はAumoが提供するクライアントアプリの全面リニューアルについてご紹介します。
タイトルにある通り、このアプリのリニューアルではアプリの大部分を構築したのはエンジニアではなくデザイナーでした。AIを活用してiOS・Android両OSのアプリを約2ヶ月で作り直したのですが、AIエージェントを使って自律的に実装を進めてもらいました。
エンジニアである私が担当したのは、そのための土台づくりと環境整備です。技術選定とアーキテクチャを決め、AIエージェントが自律的にビルドや動作確認を実行し、繰り返せる状態を構築したうえで引き渡しました。
本記事では、プロジェクトの立ち上げから開発環境の引き渡しまでに、エンジニア側で取り組んだ内容を紹介します。デザイナー視点での実践やAI運用の工夫は、後編でデザイナー本人からご紹介する予定です。
リニューアル後のアプリ画面がこちらです。


Aumoアプリは、以下からダウンロードできます。
ぜひ新しくなったAumoアプリをお楽しみください。
プロジェクトの概要
なぜ全面リニューアルしたのか
もともとAumoのアプリは、iOSとAndroidそれぞれネイティブで実装されており、リポジトリも別々に管理されていました。専任のアプリエンジニアが不在だったこともあり、どちらも数年以上大きな更新が止まっていました。
その結果、両OSの間で実装や機能に差分が生じており、改修のたびに「どちらの挙動が現在の正しい仕様なのか」を両方のコードで突き合わせる必要がありました。プラットフォームのアップデートへの追従や依存ライブラリの更新も滞っており、最低限の変更のみでなんとかしのいでいる状態でした。当時のメモにも「両リポジトリは機能差分があるため、全機能を参照できるわけではありません」と残してありました。
Aumoは2026年4月にリブランディングを実施し、ロゴの刷新やサービス名表記の変更(「aumo」から「Aumo」へ)を行いました。
当初はリブランディングに合わせてアイコンやロゴを差し替える程度で済ませる想定でしたが、SwiftやXcodeのバージョンが古く、AndroidはまだしもiOSはビルドを通すだけでも一苦労という状況でした。
以前から、「Kotlin Multiplatform(KMP)を使って簡単にリニューアルをできないか」という構想は持っていました。ちょうどLLMやAIエージェントの性能が向上し、Android CLIをはじめとする周辺ツールも整ってきたタイミングだったこともあり、「AIエージェントを活用すれば効率よくリプレイスできるのでは」と考え、テスト的にプロジェクトを作成したことが始まりでした。
結果として、1週間ほどでプロジェクトのベースが構築でき、全体の6割程度まで形にすることができました。このスピード感でのアプリ構築で手応えを得られたため、部分的な改修ではなく、KMPによる全面リニューアルへ踏み切ることにしました。
あわせてビジネスサイドでアプリの位置づけ自体も見直し、従来のメディアから「おでかけや暮らしの体験を共有するSNS」へと大きく軸を移すことになりました。
2人体制と役割分担
このプロジェクトは、エンジニア1人とデザイナー1人の2人体制で進めました。私が担当したのは主に次の3点です。
- 技術調査と技術選定
- KMPプロジェクトのセットアップとベースの作り込み
- デザイナーがAIエージェントを使って開発できる環境の構築
開発環境の構築といっても、私ひとりで膨大な時間をかけたわけではありません。用意したのは、あくまで「ビルド、デバッグ、動作確認のループが自律して回る環境」までです。
開発開始後の細かな運用ルールやエージェントへのプロンプトは、デザイナーさんが実装を進めながら自ら構築し、磨き込んでいきました。実装期間中の私の役割は、定期的にコードを確認してアーキテクチャから外れていないかをチェックしたり、技術的なサポートや判断が必要な場面で相談に乗ったりする伴走が中心でした。
技術選定
なぜKotlin Multiplatformを選んだのか
クロスプラットフォーム技術を採用する大きなメリットのひとつに、ビジネスロジックを両OSで二重に実装する手間を省ける点があります。その中でも Kotlin Multiplatform(以下KMP)を選んだ理由は、UIの描画アプローチにあります。
代表的なクロスプラットフォーム技術である Flutter や React Native は、基本的にUIも含めてフレームワーク側で共通化するアプローチです。一方でKMPは、ロジックのみを共通化し、UIは各OSネイティブのフレームワークを使う構成を柔軟に選べます。
Aumoはおでかけや暮らしの体験を日常的に共有するSNSです。そのため、iOSやAndroidそれぞれの標準的なUIコンポーネントやジェスチャー操作と自然に調和させ、ユーザーが普段使い慣れている他のアプリと同じような手触りや操作感を大切にしたいと考えていました。
また、既存資産や知見が活かせることも理由のひとつです。旧アプリは Swift と Kotlin で書かれており、私自身もKotlinの知見がありました。KMPであれば言語としてモダンで採用実績も増えており、Android側の知見やコード資産を活かしやすいというメリットがありました。
結果として、コードの内訳は次のようになっています。
| 区分 | 言語 | 行数 | 割合 |
|---|---|---|---|
| 共通ロジック | Kotlin | 30,987 | 31.0% |
| iOS UI | Swift | 34,988 | 34.9% |
| Android UI | Kotlin | 34,139 | 34.1% |
今回はCompose Multiplatform(CMP)を採用せず、UIを各OSネイティブ(SwiftUI / Jetpack Compose)で個別に実装したため、行数ベースでの共通化割合は約3割にとどまります。ただし、iosApp と androidApp には ViewModel・Repository・UseCase を置いておらず、ビジネスロジックは共通モジュール側に集約されているため、ロジックの重複はありません。
KMPの採用事例や公式情報については、JetBrainsの公式ドキュメントやケーススタディにまとまっています。
採用した主なライブラリ構成は以下のとおりです。
| 領域 | 採用技術 |
|---|---|
| 言語 | Kotlin 2.4.10 |
| ビルド | AGP 9.1.0 / compileSdk 37 / minSdk 24 / targetSdk 36 |
| Android UI | Jetpack Compose(BOM 2026.08.00)+ Material 3 |
| iOS UI | SwiftUI |
| ネットワーク | Ktor 3.5.2 |
| DI | Koin 4.2.2 |
| ローカル保存 | Multiplatform Settings 1.3.0 |
| Kotlin↔Swift連携 | SKIE 0.10.14 |
| 画像 | Coil 3.5.0 |
| 地図 | Google Maps(両OS) |
| ログ | Napier 2.7.1 |
アーキテクチャと実装ルールの固定
ロジックを共通化する shared モジュールは、クリーンアーキテクチャの考え方をベースにした4層構成にしました。依存の向きを一方向に保ち、domain 層が他の層に依存しないルールとしています。
shared/src/commonMain/kotlin/jp/aumo/
├── domain/ … model / repository(interface) / usecase など
├── data/ … repository(impl) / remote / mapper / cache / storage
├── presentation/ … 画面ごとの State / Intent / ViewModel
└── di/ … Koin モジュール
画面まわりはMVI(Model-View-Intent)に統一し、1画面につき以下の3点セットを作成するルールとしました。
State… 画面の状態(Loading / Success / Error など)Intent… ユーザー操作やライフサイクルイベントViewModel…state: StateFlowを公開し、processIntent(intent)を受ける
現在この構成は28画面分あり、すべて同じ構造になっています。
クリーンアーキテクチャを厳格に適用しているわけではなく、UseCaseの多くはRepositoryの呼び出しをそのまま返す薄い実装です。それでも層を分けているのは、AIエージェントに実装させる際にコードの実装箇所を一意に決めるためです。「この処理はどこに書くべきか」で迷わせないことを優先しました。
UI層についても、iOSとAndroidで対称になる命名ルールを決めました。
| Stateful(ViewModelと接続) | Stateless(プレビュー可能) | |
|---|---|---|
| iOS | [Feature]View.swift | [Feature]ContentView.swift |
| Android | [Feature]Route.kt | [Feature]Screen.kt |
ViewModelと結合してよいのは最上位(Stateful側)だけに制限し、描画部分を状態から切り離すことで、UIプレビューやテストを容易にしています。
こうした実装ルールをあらかじめ整えておくことは、AIエージェントに実装を任せる上で効果的でした。既存の画面をベースに同じ構成の新しい画面を作成するときに指示がしやすく、精度の高い出力を得られます。画面ごとに異なるパターンのコードが生成されると、レビューやメンテナンスの負担が増えてしまいます。
KotlinのコードをSwiftから利用する
共通ロジックをSwiftから呼び出すためのブリッジには SKIE を採用しました。SKIEを導入することで、Kotlinの Flow がSwiftの AsyncSequence として、また sealed class がSwiftの enum として扱えるようになり、Swift側からの呼び出しがスムーズになります。
AIエージェントが自律的に開発できる環境をつくる
AIエージェントにアプリ開発を任せる場合、コードを生成させるだけでは開発サイクルは回りません。生成したコードがビルドできるか、アプリが正常に起動するか、そして意図した画面が表示されているかまでを、エージェント自身が確認できる必要があります。人間が毎回手元でビルドして画面を確認していては、AIエージェント主体での自律的な開発は進みません。
また、モバイルアプリの開発環境は、Xcodeの設定やAndroid SDK・エミュレータのセットアップ、JDKのバージョン管理など、前提知識や環境構築でのつまずきやすい点が多い領域です。モバイルエンジニアであれば経験を基に対応できますが、普段触らない人にとってはエラーがコード起因なのか環境起因なのかを切り分けるのも簡単ではありません。
そこで、「エージェント自身が自律的にビルド・デバッグ・確認のループを回せる手順」をリポジトリ内に整備しました。
- セットアップ手順: Xcode / Android SDK / JDK の準備、バージョンの固定とその理由
- 実行手順: ビルドからエミュレータ起動、画面確認までのコマンド列
- トラブルシューティング: 環境起因のエラーパターンと、コード修正で追わないケースの切り分け
開発はClaude Codeを主に使用していたため、.claude/skills/ 配下の実行スキルと、docs/skills/ 配下の技術手順書に整理して配置しています。
iOS側
iOS側では、Xcodeのセットアップ手順に加え、xcodebuild や xcrun simctl を使ったビルド・起動のコマンドを整備しました。
# 使用するシミュレータの UDID を名前から解決する
xcrun simctl list devices available | grep "$SIM_NAME ("
xcrun simctl boot $SIM_UDID
xcodebuild \
-project iosApp/iosApp.xcodeproj \
-scheme iosApp \
-configuration "Debug-Development" \
-destination "id=$SIM_UDID" \
-derivedDataPath /tmp/aumo-ios-build \
build
xcrun simctl install $SIM_UDID "$APP_PATH"
xcrun simctl launch $SIM_UDID jp.aumo.dev
xcrun simctl io $SIM_UDID screenshot /tmp/ios_screenshot.png手順書には「UDIDや .app のパスを直書きしない」ことを明記しました。シミュレータの再作成でUDIDは変わりますし、.app のパスもプロジェクト設定に依存します。固定値を書いてしまうと環境の変化で停止してしまうため、手順書には値そのものではなく「値の求め方」を記載するようにしました。
なお、Xcodeのビルドフェーズで :shared:embedAndSignAppleFrameworkForXcode が自動実行されるため、KMPのframeworkを手動でビルドする必要はありません。
Android側
Android側は、Android CLI(コマンドラインツール)を活用し、エミュレータの起動からビルド・インストールまでをコマンドで実行できるようにしました。
emulator -avd $AVD_NAME -no-snapshot-load &
adb wait-for-device
# boot_completed が "1" になるまで待機
until [ "$(adb shell getprop sys.boot_completed 2>/dev/null)" = "1" ]; do sleep 2; doneadb wait-for-device だけではOSの起動完了前に次の処理に進んでしまいインストールに失敗することがあるため、ブート完了(sys.boot_completed)を待つ条件を含めています。
ビルドとインストールは以下のように実行します。
JAVA_HOME=$(/usr/libexec/java_home -v 21) ./gradlew :androidApp:installDevelopDebug
adb shell am start -n jp.aumo.develop/jp.aumo.MainActivityAGPとの互換性を保つために JAVA_HOME を固定しています。また、AVDの作成にはJava 21で動作する cmdline-tools;latest が必要な点なども、トラブルシューティングとして手順書に含めました。
画面確認はUI階層で行う
アプリ起動後の画面確認には、MCP(Model Context Protocol)サーバーを活用しました。iOSは XcodeBuildMCP、Androidは android-mcp-server を使用し、以下の流れで検証を行います。
- UI階層を構造化データとして取得する(iOS:
snapshot_ui/ Android:get_ui_tree) - 取得した要素を指定してタップやテキスト入力を行う
- スクリーンショットを撮影して表示を確認する
画面のピクセル座標を推測してタップする方法は避けるよう手順書に記載しました。座標指定はレイアウトの変化でずれてもタップ処理自体は成功してしまうため、誤動作を見落としやすくなります。UI階層から要素を指定して操作すれば、対象の要素が存在しない時点でエラーを検知できます。
また、「タイムライン画面で3つのタブが表示されていること」「ローディングからコンテンツ表示に切り替わっていること」など、確認完了とみなす具体的な表示条件も明記しました。
ビルドの競合と環境要因の切り分け
環境構築で特に注意が必要だったのが、iOSとAndroidのビルドを並行して実行すると shared のビルド成果物が破損する問題です。
両OSのビルドが同時に走ると、中間生成物の競合により、コードに問題がないにもかかわらずコンパイルエラーが発生することがありました。エージェントがこのエラーメッセージをそのまま受け取って正しいコードを修正してしまわないよう、「両OSのビルドは必ず直列に実行する」「身に覚えのないエラーが出た場合はまず :shared:clean を実行する」といった運用ルールを手順書に記載しました。
また、社内ネットワークやプロキシに起因する通信エラーなど、コード起因ではない問題については環境要因として扱い、コード修正で追わないことを明記し、不要なコード変更を防ぐようにしました。
既存アプリのアップデートとして提供する
アプリを全面的にリニューアルする際、リリース方法としては既存アプリのアップデートとして配信する方法と、別アプリとしてストア上に新規公開する方法があるかと思います。
どちらのアプローチにも実際の事例があり、たとえば航空会社のアプリが同一アプリのまま全面リニューアルを果たした例もあれば、あるカーシェアサービスが新アプリを別途リリースして移行を促した例もあります。
今回は既存アプリのアップデートを選択しました。ユーザーに再インストールの負担をかけたくなかったことに加え、技術的にも新規アプリに分ける必要がなかったためです。リポジトリやコードベースは完全に刷新しつつ、ストア上のアプリとしてはシームレスにアップデートができる形をとりました。
ローカルデータの保存先とキーを引き継ぐ
既存アプリのアップデートとして配信するため、アプリの識別子(iOSの CFBundleIdentifier や Androidの applicationId、および開発用・QA用の環境suffix)は旧アプリのものをそのまま引き継ぎました。
ただ、識別子を揃えただけでは不十分です。端末内に保存されているローカルデータが引き継げなければ、既存ユーザーにとってはアップデートした途端に勝手にログアウトされた初期状態に見えてしまいます。そこで、データのキー名も旧アプリと一致させました。
たとえば Android の SharedPreferences は、ファイル名を旧アプリと同じものを指定しています。
single<Settings> {
val context: Context = get()
val sharedPreferences = context.getSharedPreferences("examplepref", Context.MODE_PRIVATE)
SharedPreferencesSettings(sharedPreferences)
}保存するキー名についても、KMPの expect / actual 機構を活用してプラットフォームごとに旧アプリと同じキーを定義しました。
// iOS側
actual object SessionKeys {
actual val ACCESS_TOKEN: String = "EXAMPLE_ACCESS_TOKEN_KEY"
actual val REFRESH_TOKEN: String = "EXAMPLE_REFRESH_TOKEN_KEY"
actual val GRADE: String = "EXAMPLE_GRADE_KEY"
// ...
}このiOS側のキー名は、旧iOSアプリで使用していた定義からそのまま引き継いだものです。AndroidとiOSでキーの命名規則が異なっているのは、旧アプリ時代にOS別々で開発されていた名残ですが、ここを綺麗に統一してしまうと既存ユーザーのログイン情報が飛んでしまうため、新アプリでもその差分をあえてそのまま残しています。
セキュリティ要件との折り合いをつける
一方で、旧アプリの仕様をすべてそのまま踏襲したわけではありません。セキュリティ向上のため、アクセストークンやリフレッシュトークンの保存先は、Androidでは EncryptedSharedPreferences へ、iOSでは Keychain へ移行しました。
ただし、暗号化ストレージの導入には運用上の注意点があります。端末のKeystoreが破損した場合やOSのバックアップから復元された場合などに、暗号化領域の初期化処理で例外が発生することがあります。セッション情報はアプリ起動直後に必ず読み込むため、対策を入れておかないと起動するたびにクラッシュを繰り返し、アプリを再インストールする以外に復旧手段がなくなってしまいます。
そこで、暗号化ストレージの初期化に失敗した場合は破損した設定ファイルを安全に削除し、1回だけストレージを再生成するリカバリ処理を組み込みました。この場合トークンは破棄されるためユーザーは再ログインを求められますが、アプリがクラッシュし続けて開けなくなる事態に比べれば影響は最小限に抑えられます。
※ androidx.security:security-crypto は 1.1.0 でAPI全体が非推奨となり、プラットフォーム標準のAPIやAndroid Keystoreの直接利用への移行が推奨されています。
引き渡し後の開発
テストプロジェクトでリニューアルの手応えを得たあと、マルチ環境の設定やアーキテクチャのルールを整備し、2026年6月にデザイナーさんへプロジェクトを引き渡しました。
実装期間は約2ヶ月間で、2026年8月末に v2.0.0 として無事ストアリリースを迎えました。画面数は28画面、共通ロジックの単体テストは1,694個という規模です。総コミット数861件のうち、約95%(816件)がデザイナーさんによるもので、私のコミットは初期構築と環境整備を中心とした45件(約5%)でした。
おわりに
今回エンジニアとして担当したのは、直接コードを書くことではなく、AIエージェントが自律的に開発を進められる環境とルールを整えることでした。
人間であれば、コードの問題ではなく環境の問題かもしれない、と立ち止まれる場面でも、AIエージェントはエラーメッセージの通りにコードのバグだと判断し、本来正しいコードまで修正してしまうことがあります。そのため、手順や前提条件を曖昧にせず、「何ができれば成功か」という達成条件まで明確に言語化しておくことが重要です。
実装そのものはAIエージェントに任せられる時代になってきましたが、そのエージェントが最大のパフォーマンスを発揮できる「土台」を設計し、整えること、それこそがこれからのAI駆動開発においてエンジニアが発揮すべき役割なのだと実感したプロジェクトでした。
余談:Swiftで両OSを開発する試み
今回は共通ロジックに Kotlin を採用しましたが、最近では異なるアプローチの試みも見られます。
DroidKaigi 2026 では、「Swift が Android で動く仕組み ー Swift SDK for Android と KMP」というセッションがありました(STORES の Yu Takahashi さん)。Swift 6.3 でリリースされた Swift SDK for Android を使い、SwiftコードをコンパイルしてJNI経由でAndroidアプリから動かすという内容です。
UIフレームワークやAPIの制約、JNI経由のオーバーヘッドなど考慮すべき点は多いものの(詳細は発表スライドで解説されています)、Swiftを共通言語として扱うことについて技術的に探求されています。現時点ですぐにプロダクトに取り込むことは難しいと思いますが、様々なマルチプラットフォームの可能性を試してみるのも面白そうです。
後編では、実際にコードの大半を実装したデザイナーさんから、非エンジニア視点で直面した課題や、AIエージェントを自律的に動かすための工夫などを紹介する予定です。