Tutorials / Native Mac IDE / Migrate a Flutter app to native
📝 Written ● Intermediate Updated 2026-05-19

Migrate a Flutter app to native iOS, Android, and macOS

LingCode reads your Flutter repo as a design spec — not source to translate — and writes idiomatic SwiftUI, Jetpack Compose, and macOS SwiftUI from scratch. No Dart in the output. No "Flutter-flavored" native code. Plan it in five phases so LingCode doesn't drift halfway through.

Why translate-don't-port works better than the opposite

0

The naïve approach is to map each Flutter widget to a native equivalent one-to-one and hope it composes. It doesn't. Three things break:

  • State idioms diverge. Provider, Riverpod, and Bloc don't have one-to-one analogues — they have one-to-many. Provider becomes @Observable+@Environment on iOS but ViewModel+StateFlow on Android. Translating literally produces awkward hybrids.
  • Layout primitives differ. A Container with padding, alignment, decoration, and a child is one widget in Flutter — three modifiers in SwiftUI and a Box with modifiers in Compose. Translating widget-by-widget yields nested noise.
  • Platform channels can't be guessed. Every MethodChannel is a hand-written native hook on each platform. Faking them produces silent runtime breakage.

Reading the Flutter source for intent — what screens exist, how they navigate, what data flows where — and then writing the native version fresh produces code a native developer would actually want to maintain.

What you need

1
  • LingCodedownload the installer. Free tier is plenty for this.
  • The Flutter repo — open in LingCode. Must contain pubspec.yaml at the root.
  • An empty target directory — LingCode writes ios/, android/, and macos/ next to (not inside) your Flutter source. Keeps the Flutter app intact while you migrate.
  • Xcode and Android Studio installed — for building and running the native output. LingCode generates the projects; you run them.

Phase 1 — Inventory the Flutter app

2

Open the chat panel and ask LingCode to scan the Flutter repo and write a conversion plan. A prompt that works:

Read this Flutter repo and write CONVERSION_PLAN.md in
../native-output/. Summarize:
- pubspec.yaml: deps, Flutter SDK constraint, assets, fonts.
- lib/ tree: every screen/route and its purpose.
- State management: Provider / Riverpod / Bloc / GetX / setState.
- Data layer: HTTP client, local DB (sqflite/Hive/Isar), shared prefs.
- Platform channels and native plugins, listed one by one.
- Theme, typography, color tokens, l10n strings.
- Gotchas: BackdropFilter, CustomPainter, ShaderMask, FFI plugins,
  native ads SDKs, Firebase Crashlytics — anything without a clean
  native mapping.

Don't start porting yet. Stop after the plan.

LingCode reads the repo, builds a todo list as it scans, and writes CONVERSION_PLAN.md. Read it before approving any code generation. The "gotchas" section is the one that decides whether the project is a two-week job or a two-month job.

Tip: If you only want the plan and nothing more, say "scan only — stop after the plan." LingCode will checkpoint and wait for you.

Phase 2 — Architecture per target

3

For each target platform, LingCode needs to commit to architectural choices before generating code. Ask it to append a per-target architecture section to the plan:

Append to CONVERSION_PLAN.md a section for each of iOS, Android,
macOS that picks:
- Navigation pattern: NavigationStack on Apple, NavHost on Android.
- DI / state container: @Observable+@Environment, or ViewModel+
  StateFlow, etc. Match the original Flutter state idiom.
- Networking layer: URLSession + async/await (Apple), OkHttp/Retrofit
  (Android), or shared via Ktor if there's a reason to.
- Persistence: SwiftData / Core Data / GRDB (Apple), Room (Android).
- Asset pipeline: where pubspec assets land in each native project.

Where the screen makes sense on macOS, share SwiftUI views between
iOS and macOS — don't duplicate.

This is the last cheap step. Approving the wrong navigation pattern here means rewriting every screen later. Read the architecture section like a code review.

Phase 3 — Scaffold the native projects

4

Now ask LingCode to create the empty projects:

Scaffold the iOS, Android, and macOS projects in ../native-output/.
Use Xcode's project format for iOS and macOS (don't hand-write
.xcodeproj XML). Use Gradle for Android (don't hand-write
build.gradle). Bundle IDs:
- iOS:     com.yourcompany.yourapp
- Android: com.yourcompany.yourapp
- macOS:   com.yourcompany.yourapp.mac

LingCode uses its native scaffolders for this — same shape Xcode and Android Studio produce on "New Project." You should be able to open ios/YourApp.xcodeproj in Xcode and android/ in Android Studio and have both build the empty template.

Verify the empty projects build before generating screens. If the scaffold is broken, you'll spend hours debugging code that's actually fine. Build each target empty first.

Phase 4 — Port screen by screen

5

This is the long phase. Ask LingCode to port one screen at a time, using a todo list to track progress:

Port screens from lib/screens/ to the native projects, one screen
per todo item. For each screen:

1. Read the Flutter widget for intent — screen layout, state flow,
   navigation in and out.
2. Write the SwiftUI view (iOS + macOS where it makes sense).
3. Write the Compose composable (Android).
4. Wire navigation in both NavigationStack and NavHost.
5. Wire state into the chosen container.

For widgets with no clean native mapping, stub with
// TODO(flutter-to-native): ... and log it in CONVERSION_PLAN.md
under "deferred." Don't fake behavior.

Start with the home screen. Stop after each screen for me to review.

The "stop after each screen" instruction matters. Letting LingCode run all 20 screens without checkpoints means re-reviewing all 20 at the end, which is the worst time to catch architectural drift.

The idiom map LingCode uses internally

6

If you want to spot-check LingCode's translations, here's the mapping it follows:

Flutter widget → SwiftUI

  • Container/Padding/CenterVStack/HStack/ZStack with .padding modifiers
  • Row / ColumnHStack / VStack
  • ListViewList
  • GridViewLazyVGrid
  • Navigator.pushNavigationStack(path:)
  • FutureBuilder.task + @State
  • StreamBuilderAsyncSequence + .task
  • TextFieldTextField + @FocusState
  • GestureDetector.onTapGesture / .gesture
  • AnimatedContainer.animation
  • Hero → matched-geometry effect
  • SafeArea.safeAreaInset / .ignoresSafeArea

Flutter widget → Compose

  • ContainerBox
  • Row / ColumnRow / Column
  • ListViewLazyColumn
  • GridViewLazyVerticalGrid
  • NavigatorNavHost + NavController
  • FutureBuildercollectAsStateWithLifecycle
  • TextFieldTextField + FocusRequester
  • AnimatedContaineranimate*AsState
  • HeroModifier.sharedElement
  • SafeAreaWindowInsets

State → native

  • Provider → @Observable + @Environment (Apple) / ViewModel + StateFlow (Android)
  • Riverpod → same shape, with explicit DI
  • Bloc / Cubit → reducer-style ViewModel with sealed event/state types
  • GetX → don't try to mirror; flatten into the native idiom
  • setState@State (Apple) / remember (Compose)

The "deferred" pile — what LingCode won't fake

7

Some Flutter features don't have an honest native mapping. LingCode stubs them and logs them in CONVERSION_PLAN.md rather than guessing — you decide what to do with each:

  • BackdropFilter, custom CustomPainter, ShaderMask edge cases. Custom rendering goes through Metal / SkSL or platform-specific shaders. No one-shot translation.
  • Every platform channel. Each MethodChannel is a hand-written native hook on iOS and Android — Swift methods and Kotlin functions you write yourself. LingCode stubs the call sites.
  • FFI plugins. Same story — your C/C++ library needs Swift and Kotlin bindings, not Dart's FFI.
  • Native ads SDKs. AdMob and Meta Audience SDKs ship native; the Flutter plugin is a thin wrapper. Re-integrate natively.
  • Firebase Crashlytics initialization. Must live in AppDelegate (iOS) and Application (Android), not the shared layer.
  • RawKeyboardListener edge cases. Apple has onKeyPress; Android has onKeyEvent — usable, but not a literal mapping.

Don't let LingCode invent behavior for these. The honest stub is better than confidently wrong code.

Phase 5 — Verify the builds

8

After the screen-by-screen phase, ask LingCode to verify each target:

For each target, run the build and report pass/fail:
- iOS:     xcodebuild -project ios/YourApp.xcodeproj
             -scheme YourApp -destination 'generic/platform=iOS Simulator'
             build
- Android: cd android && ./gradlew assembleDebug
- macOS:   xcodebuild -project macos/YourApp.xcodeproj
             -scheme YourApp build

If a target fails, diagnose and fix before moving on. Don't claim
done until every requested platform builds clean.

This is where you'd run LingCode's parallel agents — one per target — if you want to compress wall time. Each target is independent; failures in iOS won't block Android.

Run side-by-side

9

The native projects build, but the real test is parity. Open both the original Flutter app and the new iOS build in simulators, then walk through every screen comparing behavior — navigation, animations, error states, edge cases like empty lists and offline behavior.

LingCode runs the iOS simulator and Android emulator from the Run Destination Picker (⌘R) — you don't need a separate Xcode or Android Studio window for casual testing. Use Android Studio when you need ADB tooling, profilers, or AVD management.

Hard constraints — what good migrations look like

10
  • The output is Flutter-free. No flutter embed, no "add-to-app." If you wanted that, you didn't need this tutorial.
  • Platform channels are listed in the plan, not faked. Each one is a deliberate hand-port; LingCode shouldn't guess what their native side does.
  • No Flutter test files were ported. Native tests are written fresh against the native code — XCTest, JUnit, Espresso.
  • The Flutter repo wasn't mutated. No pub get, no flutter build, no edits — LingCode treats it as a read-only spec.
Use the Flutter app as a regression oracle. Keep it runnable while you migrate. Every time the native version surprises you, re-run the same flow in Flutter and compare. Faster than re-reading Dart source to remember "what was that supposed to do."

Use this in LingCode

11

This entire workflow is packaged as a skill — drop it into your LingCode skills folder and ask LingCode for "flutter to native" to invoke it automatically:

---
name: flutter-to-native
description: Convert a Flutter app to native iOS (SwiftUI), Android (Jetpack Compose), and/or macOS (SwiftUI). Reads the Flutter repo as a design spec and writes fresh idiomatic native code — zero Dart in the output. Triggers: 'migrate from Flutter', 'rewrite Flutter app in native', 'flutter to swiftui', 'flutter to compose', 'flutter to native', existing pubspec.yaml, lib/main.dart. Actions: inventory Flutter app, propose native architecture per target, scaffold ios/ android/ macos/, port screen-by-screen, verify each build clean. Anti-pattern: Flutter embed, add-to-app, faked platform channels. Five phases with checkpoints.
---

Convert a Flutter app to native. Treat the Flutter repo as a design
spec, not source to translate. Read it to understand intent
(screens, navigation, state, data), then write idiomatic native
code from scratch.

Phase 1 — Inventory: scan pubspec, lib/, state mgmt, data layer,
platform channels, theme, gotchas. Write CONVERSION_PLAN.md.

Phase 2 — Architecture per target: navigation, DI, networking,
persistence, asset pipeline.

Phase 3 — Scaffold ios/, android/, macos/.

Phase 4 — Port screen by screen, one todo per screen.

Phase 5 — Verify each target builds clean.

Stop after each phase for user review. Stub deferred items with
// TODO(flutter-to-native): ... rather than faking behavior.

Save as ~/.lingcode/skills/flutter-to-native/SKILL.md — see Install a skill for the exact location and how skills get discovered.

Get LingCode →

What's next