Google I/O 2026 で発表された Android Studio の移行エージェントは、React Native や iOS、Web のコードを解析してネイティブ Kotlin の Android アプリへ自動移行するという、かなり踏み込んだ機能です。発表記事を読んで私が最初に考えたのは「いま移すべきか」ではなく、「いま運用している Expo アプリは、そもそも移せる構造になっているだろうか」という点でした。
Rork が生成するのは Expo(React Native)ベースのアプリです。つまり Rork で作ったアプリには、将来「ネイティブ Kotlin へ自動移行する」という選択肢が新しく加わったことになります。ただし、自動移行が現実的に機能するかどうかは、エージェントの賢さだけでなく、移行元のコードがどれだけ「移しやすい形」をしているかに大きく左右されます。私自身、個人開発で Expo ベースの検証アプリとネイティブ Android アプリの両方を並行運用している立場から、この発表を機にコード構造を見直した内容を整理します。
移行エージェントは何をしてくれるのか
まず発表内容の事実関係を押さえておきます。Google の開発者ブログによると、Android Studio の移行エージェント(プレビュー)は次のような動きをします。
既存の React Native・iOS・Web アプリのコードベースを解析する
画面構成・ビジネスロジック・データフローを把握する
それらをネイティブ Kotlin + Jetpack Compose の Android プロジェクトとして再構築する
注意したいのは、これが「変換ツール」ではなく「エージェント」だという点です。一行ずつ機械的にトランスパイルするのではなく、AI がコードの意図を読み取って書き直す方式です。したがって出力の品質は、入力側のコードの意図がどれだけ読み取りやすいかに依存します。意図が UI とロジックの間に散らばったコードは、人間にとってもエージェントにとっても読み解きが難しいのです。
なお、現時点ではプレビュー段階の機能であり、挙動の詳細は今後変わる可能性があります。ここでの主眼は「エージェントの使い方」ではなく、「エージェントが来ても困らないコードの作り方」に置きます。
なぜ「いま移行しない」と判断したのか
結論から書くと、私は運用中の Expo アプリを当面移行しない判断をしました。理由は三つあります。
第一に、OTA 更新を手放すコストが大きいことです。Expo の EAS Update を使うと、JS レイヤーの修正はストア審査を経ずに配信できます。軽微な文言修正やロジックのバグ修正を当日中に届けられる運用は、個人開発において想像以上に効きます。Kotlin ネイティブに移行した瞬間、すべての修正がストア審査待ちになります。
第二に、コードベースが二つに割れることです。移行エージェントが面倒を見てくれるのは Android だけです。iOS 版を維持するなら、React Native 版(iOS 用)と Kotlin 版(Android 用)の二重管理が始まります。片方だけのつもりで移行すると、機能追加のたびに二倍の作業が待っています。
第三に、移行は「いつでもできる側」に回ったことです。エージェントの登場で、移行のコストは今後下がり続けると見ています。急いで移る理由がない限り、待つほど条件は良くなります。
それでも移行が視野に入るのは、Android 固有の深い統合(ウィジェット、特殊なバックグラウンド処理、最新 OS 機能への即応)が収益に直結するアプリです。この判断軸は、Apple 側で Rork Max への移行を考えるときとほとんど同じ構造をしています。Rork で出したアプリを Max へ移すのはいつか — 実績データで決める段階移行の基準 で書いた「実績データが移行コストを正当化したときだけ動く」という基準は、Kotlin 移行にもそのまま使えます。
移行可能性を決めるのは UI ではなくロジックの置き場所
見直しを始める前に、何が移行の障害になるかを整理しておきます。経験上、移行(あるいは大規模リファクタリング)で本当に苦労するのは UI ではありません。UI は作り直しが効きます。苦労するのは、次の二つです。
ビジネスロジックが UI コンポーネントの中に埋まっている : お気に入りの管理、課金状態の判定、表示条件の計算などが useEffect や onPress ハンドラの中に直接書かれていると、エージェントは「これは画面の都合か、仕様か」を判別できません
ネイティブモジュールへの依存が無自覚に増えている : JS だけで完結するパッケージと、ネイティブコードを含むパッケージでは移行時の重みがまったく違います。後者は Kotlin 側で代替実装を探すか書き起こす必要があります
逆に言えば、この二つを管理できていれば、移行エージェントに渡しても、Rork Max で作り直しても、あるいは手作業で移行しても、コストは大きく下がります。移行可能性とは特定ツールへの対応ではなく、コードの分離度の問題です。
実践 1: ネイティブ依存を棚卸しするスクリプト
最初にやるべきは現状把握です。package.json の依存のうち、どれがネイティブコードを含むのかを機械的に分類します。node_modules 内に android / ios ディレクトリを持つパッケージはネイティブコードを含む、という性質を使った簡単なスクリプトです。
// audit-native-deps.mjs
// package.json の依存を「JSのみ / Expo管理ネイティブ / サードパーティネイティブ」に分類する
import { readFileSync, existsSync } from "node:fs" ;
import { join } from "node:path" ;
const pkg = JSON . parse ( readFileSync ( "package.json" , "utf8" ));
const deps = Object. keys (pkg.dependencies ?? {});
const result = { jsOnly: [], expoManaged: [], thirdPartyNative: [] };
for ( const name of deps) {
const dir = join ( "node_modules" , name);
if ( ! existsSync (dir)) continue ;
const hasNative =
existsSync ( join (dir, "android" )) || existsSync ( join (dir, "ios" ));
if ( ! hasNative) {
result.jsOnly. push (name);
} else if (name. startsWith ( "expo" )) {
result.expoManaged. push (name);
} else {
result.thirdPartyNative. push (name);
}
}
console. log ( `JS のみ: ${ result . jsOnly . length } 件` );
console. log ( `Expo 管理ネイティブ: ${ result . expoManaged . length } 件` );
console. log ( `サードパーティネイティブ: ${ result . thirdPartyNative . length } 件` );
for ( const n of result.thirdPartyNative) {
console. log ( ` ⚠ ${ n } — 移行時に Kotlin 側の代替実装を要検討` );
}
node audit-native-deps.mjs で実行すると、次のような出力になります。
JS のみ: 24 件
Expo 管理ネイティブ: 9 件
サードパーティネイティブ: 5 件
⚠ react-native-mmkv — 移行時に Kotlin 側の代替実装を要検討
⚠ react-native-google-mobile-ads — 移行時に Kotlin 側の代替実装を要検討
...
私の検証用 Expo アプリで回したところ、依存 38 件のうちサードパーティネイティブが 5 件でした。このうち広告 SDK とストレージは Kotlin 側に確立した対応物(AdMob の Android SDK、Jetpack DataStore)があるため、実質的な懸念は残り 1〜2 件に絞れました。「なんとなく不安」だった移行コストが、リストを眺めるだけで「この 2 件をどうするか」という具体的な問いに変わります。この心理的な効果が棚卸しの一番の収穫です。
なお Expo 管理のパッケージ(expo-av、expo-file-system など)は、Android 側の標準 API への対応が比較的素直なので、サードパーティネイティブとは分けて数えるのが実態に合います。
実践 2: ビジネスロジックを「コア層」として独立させる
次に、移行時にそのまま持ち出したいロジックを、React にも React Native にも依存しない純粋な TypeScript に切り出します。お気に入り管理を例にします。
// src/core/favorites.ts — UI にもストレージ実装にも依存しないコア層
export interface FavoriteStore {
load () : Promise < string []>;
save ( ids : string []) : Promise < void >;
}
export class FavoritesService {
constructor ( private store : FavoriteStore ) {}
async toggle ( id : string ) : Promise < string []> {
const current = await this .store. load ();
const next = current. includes (id)
? current. filter (( x ) => x !== id)
: [ ... current, id];
await this .store. save (next);
return next;
}
async isFavorite ( id : string ) : Promise < boolean > {
return ( await this .store. load ()). includes (id);
}
}
ストレージの実装はアダプタとして React Native 側に置きます。
// src/adapters/mmkvFavoriteStore.ts — RN 依存はこのファイルに閉じ込める
import { MMKV } from "react-native-mmkv" ;
import type { FavoriteStore } from "../core/favorites" ;
const storage = new MMKV ();
export const mmkvFavoriteStore : FavoriteStore = {
async load () {
return JSON . parse (storage. getString ( "favorites" ) ?? "[]" );
},
async save ( ids ) {
storage. set ( "favorites" , JSON . stringify (ids));
},
};
なぜこの形にするのか。FavoritesService は interface と標準の制御構文しか使っていないため、Kotlin への書き換えがほぼ機械的に済むからです。interface は Kotlin の interface に、クラスはクラスに、Promise は suspend 関数に対応します。移行エージェントにとっても、このファイルは「仕様そのもの」として読めます。一方、MMKV への依存はアダプタ 1 ファイルに閉じているので、Kotlin 側では DataStore 実装に差し替えるだけです。
Rork でアプリを生成するときも、この構造はプロンプトで誘導できます。「お気に入りやダウンロード履歴などの状態管理ロジックは、UI コンポーネントから分離した純粋な TypeScript のサービスクラスとして実装してください。ストレージへのアクセスは interface を介してください」と最初の指示に含めると、生成されるコードの分離度が目に見えて変わります。生成後に分離をやり直すより、生成時に指示するほうが圧倒的に安上がりです。
実践 3: プラットフォーム分岐を 1 箇所に集約する
Platform.OS による分岐がコードベース全体に散らばっていると、移行時に「Android ではどう動くべきか」の仕様復元が難しくなります。分岐はアダプタ層に集約します。
// src/adapters/shareAdapter.ts — Platform 分岐をこのファイルに集約する
import { Platform, Share } from "react-native" ;
export async function shareWallpaper ( url : string , title : string ) {
// Android では message に URL を含めないと、共有先アプリで本文が空になるケースがある
return Share. share (
Platform. select ({
ios: { url, title },
default: { message: `${ title } \n ${ url }` },
})
);
}
呼び出し側は shareWallpaper(url, title) を呼ぶだけで、プラットフォームの事情を知りません。移行時には「Android の挙動だけを書いた仕様書」としてアダプタ群を読めばよく、エージェントへの入力としても人間のレビュー対象としても最小になります。
分岐が散らばっているかどうかは grep -rn "Platform.OS\|Platform.select" src/ | grep -v adapters/ | wc -l で確認できます。この数値が 0 に近いほど、移行可能性は高い状態です。
移行準備度チェックリスト
ここまでの内容を、定期的に確認できるチェックリストにまとめます。私は四半期ごとのメンテナンス時に確認することにしました。
ネイティブ依存の把握 : 棚卸しスクリプトを実行し、サードパーティネイティブの件数と Kotlin 側の代替有無を一覧化してあるか
コア層の分離 : 課金判定・表示条件・データ加工のロジックが、React に依存しないモジュールに分離されているか(目安: src/core/ 配下のファイルが import しているのは標準ライブラリと型定義のみ)
分岐の集約 : アダプタ層の外に Platform.OS 分岐が残っていないか
OTA 依存度の自覚 : 直近 3 ヶ月で EAS Update による即時修正を何回使ったか。回数が多いほど、移行後に失う運用価値が大きい
二重管理の覚悟 : iOS 版の扱い(RN のまま維持 / Rork Max で作り直し / 凍結)を決めてあるか
5 項目のうち上 3 つはコードの問題なので今日から改善できます。下 2 つは運用判断なので、数値を記録しておくだけでも将来の判断材料になります。
よくある落とし穴
最後に、この見直しを進める中で気づいた、判断を誤りやすいポイントを挙げておきます。
落とし穴 1: 移行後も OTA 更新の感覚で運用計画を立ててしまう。 Kotlin ネイティブ化すると、すべての修正がストア審査を通ります。Expo 時代の「気づいたら当日修正」を前提にしたリリース計画は崩壊します。移行を検討する段階で、Rork アプリの段階的リリース戦略 — Phased Release・Staged Rollout・Hotfix を組み合わせて『壊さない本番運用』を作る で書いたような審査前提の防御的なリリース運用へ、頭を切り替えておく必要があります。
落とし穴 2: エージェントの出力を無検証で信用する。 プレビュー段階の AI エージェントの出力は、コンパイルが通ることと仕様が保たれていることの間に大きな距離があります。特に課金・広告まわりのロジックは、移行後に必ず手動でテストケースを通すべき領域です。私は AdMob の広告表示条件のような収益直結ロジックについては、コア層に対する単体テストを移行前に書いておき、Kotlin 側でも同じテストを再現する方針にしています。テストは仕様の最も正確な記述として移行を渡れます。
落とし穴 3: 「Android だけ移行」の影響範囲を過小評価する。 移行エージェントが出力するのは Android アプリだけです。iOS 版が React Native のまま残る以上、機能追加は二箇所への実装になります。個人開発の時間予算でこれを支えられるかは、移行判断の中でも最も重い問いです。コア層を分離してあれば、ロジックの仕様を一度書いて両方に展開する形にでき、この負担をいくらか軽くできます。
次のアクション
まず棚卸しスクリプトを自分のプロジェクトで実行して、サードパーティネイティブ依存の件数を確認してみてください。数が見えるだけで、「移行」という漠然とした選択肢が、自分のアプリにとって現実的なのかどうかの輪郭がはっきりします。コア層の分離はそのあとで、新しく書くコードから少しずつ始めれば十分です。
移行エージェントが正式版になる頃、慌てて準備を始めるのか、いつでも動ける状態で様子を見ているのか。その差は今日の設計判断から生まれるのだと考えています。