●FUNDING — Rork raises $15M, drawing fresh attention to its mobile-first no-code AI positioning●MAX-NATIVE — Rork Max reaches native territory React Native can't: AR/LiDAR, Metal 3D, widgets, Dynamic Island, Live Activities, HealthKit, and on-device Core ML●MOBILE-FIRST — While Bolt and Lovable focus on web apps, Rork builds mobile apps — production-ready from a plain-language description●WWDC — WWDC26 wraps with AI becoming a core OS capability; the iOS 27 generation raises the value of widgets and Live Activities●PRICING — Free to start, paid plans from $25/mo, Rork Max at $200/mo — ship fast on Expo, then go native with Max where it pays off●ALL-APPLE — Rork Max generates pure Swift covering iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage●FUNDING — Rork raises $15M, drawing fresh attention to its mobile-first no-code AI positioning●MAX-NATIVE — Rork Max reaches native territory React Native can't: AR/LiDAR, Metal 3D, widgets, Dynamic Island, Live Activities, HealthKit, and on-device Core ML●MOBILE-FIRST — While Bolt and Lovable focus on web apps, Rork builds mobile apps — production-ready from a plain-language description●WWDC — WWDC26 wraps with AI becoming a core OS capability; the iOS 27 generation raises the value of widgets and Live Activities●PRICING — Free to start, paid plans from $25/mo, Rork Max at $200/mo — ship fast on Expo, then go native with Max where it pays off●ALL-APPLE — Rork Max generates pure Swift covering iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and iMessage
Keeping a Rork-Built Expo App Ready for Kotlin Migration — Design Notes After Android Studio's Migration Agent Announcement
Android Studio's new agent can migrate React Native apps to native Kotlin. Here is how I restructured a Rork-built Expo app to stay migration-ready: a native dependency audit script, a portable core layer pattern, and a readiness checklist.
Among the announcements at Google I/O 2026, the one that made me stop and reread was Android Studio's migration agent: a preview feature that analyzes React Native, iOS, or web codebases and rebuilds them as native Kotlin Android apps. My first thought wasn't "should I migrate now?" It was a quieter question — is the Expo app I'm running even structured in a way that could be migrated?
This matters directly to Rork users, because Rork generates Expo (React Native) apps. The announcement effectively adds a new long-term option to every Rork project: automated migration to native Kotlin. But whether automated migration works in practice depends less on how clever the agent is and more on how "movable" your source code happens to be. As an indie developer running both an Expo-based app and native Android apps side by side, I used this announcement as a prompt to re-examine my own code structure. These are my notes.
What the migration agent actually does
First, the facts as announced. According to Google's developer blog, the Android Studio migration agent (in preview) works like this:
It analyzes an existing React Native, iOS, or web codebase
It maps out the screens, business logic, and data flow
It rebuilds the app as a native Kotlin + Jetpack Compose Android project
The important word is agent, not converter. This is not line-by-line transpilation. An AI reads the intent of your code and rewrites it. Which means output quality depends on how legible the intent of your input code is. Code whose intent is smeared across UI handlers and effects is hard to read for humans — and just as hard for an agent.
It's also worth stressing that this is a preview feature whose behavior may change. So this article is not about how to operate the agent. It's about how to write code that won't embarrass you whenever the agent arrives in stable form.
Why I decided not to migrate yet
My conclusion up front: I'm not migrating my production Expo app for now. Three reasons.
First, giving up OTA updates is expensive. With Expo's EAS Update, JavaScript-layer fixes ship without store review. Being able to push a copy fix or a logic patch the same day matters more in solo development than it sounds. The moment you go native Kotlin, every fix waits in the review queue.
Second, your codebase splits in two. The migration agent only takes care of Android. If you keep an iOS version, you now maintain a React Native app (for iOS) and a Kotlin app (for Android) in parallel. Migrate casually and every future feature costs you twice.
Third, migration just moved to the "available whenever" column. With an agent in the picture, the cost of migrating will keep falling. Unless there's a pressing reason to move, waiting improves your terms.
The cases where migration does make sense are apps whose revenue depends on deep Android-specific integration — widgets, unusual background work, day-one support for new OS features. Notice that this is structurally the same decision as moving up to Rork Max on the Apple side. The rule I described in When Should a Rork App Move Up to Rork Max? Deciding With Store Data, Not Aspiration — only move when real store data justifies the migration cost — applies to Kotlin migration unchanged.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You can run a ready-to-use Node.js script that audits the native dependencies of your Rork-built Expo app in minutes
✦You'll learn a core-layer separation pattern, shown in working TypeScript, that survives a move to Kotlin or to Rork Max equally well
✦You'll be able to decide whether to migrate now or wait based on your own app's numbers, using a concrete readiness checklist
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
What decides migratability isn't the UI — it's where your logic lives
Before touching anything, it helps to be precise about what actually hurts in a migration. In my experience it is not the UI. UI can be rebuilt. What hurts is:
Business logic buried inside UI components. When favorites management, purchase-state checks, or display-condition math live directly inside useEffect blocks and onPress handlers, an agent can't tell which parts are screen plumbing and which parts are your actual product
Native module dependencies that grew without anyone noticing. A pure-JS package and a package with native code are completely different weights in a migration. The latter needs a Kotlin-side replacement found or written
Conversely, if you keep these two things under control, the cost drops sharply no matter which path you take — the migration agent, a rebuild on Rork Max, or a manual port. Migratability isn't about supporting a specific tool. It's a measure of how well-separated your code is.
Practice 1: audit your native dependencies with a script
Start by measuring reality. Classify the dependencies in package.json by whether they contain native code. The trick: packages that ship native code have android or ios directories inside node_modules.
// audit-native-deps.mjs// Classifies dependencies into JS-only / Expo-managed native / third-party nativeimport { 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 only: ${result.jsOnly.length}`);console.log(`Expo-managed native: ${result.expoManaged.length}`);console.log(`Third-party native: ${result.thirdPartyNative.length}`);for (const n of result.thirdPartyNative) { console.log(` ⚠ ${n} — needs a Kotlin-side replacement plan`);}
Run it with node audit-native-deps.mjs and you get output like this:
JS only: 24
Expo-managed native: 9
Third-party native: 5
⚠ react-native-mmkv — needs a Kotlin-side replacement plan
⚠ react-native-google-mobile-ads — needs a Kotlin-side replacement plan
...
When I ran this against my test Expo app, 5 of 38 dependencies were third-party native. Of those, the ad SDK and the storage library both have established Android counterparts (the AdMob Android SDK and Jetpack DataStore), which narrowed my real concerns down to one or two packages. That shift — from a vague "migration sounds scary" to a concrete "what do I do about these two?" — is the single biggest payoff of the audit.
One note: Expo-managed packages (expo-av, expo-file-system, and friends) tend to map onto standard Android APIs fairly cleanly, so counting them separately from third-party native packages reflects reality better.
Practice 2: extract business logic into an independent core layer
Next, pull the logic you'd want to carry across a migration into pure TypeScript that depends on neither React nor React Native. Favorites management makes a good example.
// src/core/favorites.ts — the core layer: no UI, no storage implementationexport 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); }}
The storage implementation lives on the React Native side as an adapter:
// src/adapters/mmkvFavoriteStore.ts — RN dependencies stay inside this fileimport { 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)); },};
Why this shape? Because FavoritesService uses nothing but an interface and standard control flow, rewriting it in Kotlin is close to mechanical: interfaces map to interfaces, classes to classes, Promise to suspend functions. To a migration agent, this file reads as a specification. Meanwhile, the MMKV dependency is contained in a single adapter file — on the Kotlin side you swap in a DataStore implementation and you're done.
You can steer Rork toward this structure at generation time, too. Include something like this in your initial prompt: "Implement state management logic such as favorites and download history as pure TypeScript service classes, separated from UI components. Access storage through an interface." The separation quality of the generated code changes visibly. Asking for separation up front is far cheaper than retrofitting it afterward.
Practice 3: concentrate platform branches in one place
When Platform.OS branches are scattered across the codebase, reconstructing "how should this behave on Android?" becomes archaeology. Concentrate the branches into the adapter layer.
// src/adapters/shareAdapter.ts — all Platform branching lives hereimport { Platform, Share } from "react-native";export async function shareWallpaper(url: string, title: string) { // On Android, omitting the URL from message can produce an empty share body return Share.share( Platform.select({ ios: { url, title }, default: { message: `${title}\n${url}` }, }) );}
Callers invoke shareWallpaper(url, title) and never learn about platform quirks. Come migration time, the adapter files double as a spec of Android-only behavior — the smallest possible input for an agent and the smallest possible review surface for a human.
You can check how scattered your branches are with grep -rn "Platform.OS\|Platform.select" src/ | grep -v adapters/ | wc -l. The closer that number is to zero, the more migratable your app is.
A migration readiness checklist
Here is everything above condensed into a checklist. I've decided to walk through it during quarterly maintenance.
Native dependency inventory: Have you run the audit script and listed each third-party native package with its Kotlin-side replacement status?
Core layer separation: Is your purchase logic, display-condition math, and data shaping isolated in modules with no React imports? (Rule of thumb: files under src/core/ import only standard libraries and type definitions)
Branch concentration: Are there any Platform.OS branches left outside the adapter layer?
OTA dependence awareness: How many times in the last three months did you ship a same-day fix via EAS Update? The higher the count, the more operational value you lose by migrating
Dual-codebase commitment: Have you decided what happens to the iOS version — keep it on React Native, rebuild it on Rork Max, or freeze it?
The first three items are code problems you can start fixing today. The last two are operational judgments; even just recording the numbers gives your future self better grounds for the decision.
Pitfalls worth knowing in advance
Finally, the places where I found it easiest to misjudge while working through this.
Pitfall 2: trusting agent output without verification. With a preview-stage AI agent, there is a long distance between "it compiles" and "the behavior is preserved." Billing and advertising logic in particular deserves hand-run test cases after any migration. For revenue-critical logic like AdMob display conditions, my policy is to write unit tests against the core layer before migrating, then reproduce the same tests on the Kotlin side. Tests cross a migration as the most precise description of your spec.
Pitfall 3: underestimating what "Android-only migration" implies. The agent outputs an Android app, full stop. With iOS still on React Native, every new feature is now implemented in two places. Whether a solo developer's time budget can absorb that is the heaviest question in the whole decision. A separated core layer softens it somewhat: you write the logic spec once and project it into both codebases.
Your next step
Run the audit script against your own project and count your third-party native dependencies. Just seeing the number turns "migration" from a hazy possibility into something with a definite outline for your specific app. Core-layer separation can come afterward, applied gradually to new code as you write it.
When the migration agent reaches stable release, some developers will scramble to get ready, and some will already be watching calmly from a position where they could move any time. The difference between the two is made by design decisions like today's.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.