[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-add-to-app-integrare-flutter-in-unapp-nativa-esistente":3,"comments-article-add-to-app-integrare-flutter-in-unapp-nativa-esistente":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},112,"Add-to-app: integrare Flutter in un'app nativa esistente","add-to-app-integrare-flutter-in-unapp-nativa-esistente","Non sempre si parte da zero: con l'approccio add-to-app puoi aggiungere schermate Flutter a un'app Android o iOS già in produzione. Guida pratica a module, FlutterEngine, FlutterEngineGroup, routing e comunicazione con il codice nativo.","## Perché add-to-app\n\nLa narrazione più diffusa su Flutter è quella del progetto greenfield: `flutter create`, si parte, si pubblica. Nella realtà aziendale, però, la domanda più frequente è un'altra: *ho già un'app Android in Kotlin e una in Swift con centinaia di migliaia di utenti, posso introdurre Flutter gradualmente?*\n\nLa risposta è sì, ed è la modalità **add-to-app**: Flutter viene integrato come libreria all'interno del progetto nativo e utilizzato per costruire singole schermate o interi flussi, mentre il resto dell'app rimane nativo. È la strada scelta da molte migrazioni incrementali, perché permette di:\n\n- validare Flutter su una feature non critica prima di impegnarsi;\n- condividere una sola implementazione di flussi complessi (onboarding, checkout, wizard) tra Android e iOS;\n- evitare il *big bang rewrite*, che è la modalità di migrazione con il tasso di fallimento più alto.\n\nIl prezzo da pagare esiste ed è concreto: aumento della dimensione del binario, complessità della build, e alcune limitazioni tecniche che vedremo alla fine. Vale la pena conoscerle **prima** di iniziare.\n\n## Creare il modulo Flutter\n\nIl punto di partenza non è un'app Flutter ma un **module**, un template pensato per essere consumato da progetti nativi:\n\n```bash\ncd \u002Fpercorso\u002Fdel\u002Ftuo\u002Fprogetto\nflutter create --template module my_flutter\n```\n\nIl modulo ha una struttura familiare (`lib\u002F`, `pubspec.yaml`) più due cartelle nascoste generate automaticamente: `.android\u002F` e `.ios\u002F`. Contengono i progetti wrapper che permettono di eseguire il modulo in standalone (utile per lo sviluppo con hot reload) e che espongono gli artefatti alle build native. Non vanno modificate a mano e non vanno versionate come codice di progetto.\n\nIl punto d'ingresso resta `lib\u002Fmain.dart`, ma in add-to-app conviene ragionare per **widget-schermata** più che per app intera:\n\n```dart\nimport 'package:flutter\u002Fmaterial.dart';\n\nvoid main() => runApp(const MyFlutterModule());\n\nclass MyFlutterModule extends StatelessWidget {\n  const MyFlutterModule({super.key});\n\n  @override\n  Widget build(BuildContext context) {\n    return MaterialApp(\n      theme: ThemeData(useMaterial3: true),\n      \u002F\u002F La rotta iniziale può essere imposta dal lato nativo\n      initialRoute: '\u002F',\n      routes: {\n        '\u002F': (_) => const LoyaltyScreen(),\n        '\u002Frewards': (_) => const RewardsScreen(),\n      },\n    );\n  }\n}\n```\n\n## Integrazione su Android\n\nCi sono due strategie: **source code integration** (il progetto nativo compila il modulo Flutter) e **AAR** (il modulo viene pubblicato come dipendenza Maven).\n\n### Source code integration\n\nNel `settings.gradle` del progetto Android:\n\n```groovy\ninclude ':app'\n\nsetBinding(new Binding([gradle: this]))\nevaluate(new File(\n  settingsDir.parentFile,\n  'my_flutter\u002F.android\u002Finclude_flutter.groovy'\n))\n```\n\nE nel `build.gradle` del modulo `app`:\n\n```groovy\ndependencies {\n  implementation project(':flutter')\n}\n\nandroid {\n  compileOptions {\n    sourceCompatibility JavaVersion.VERSION_11\n    targetCompatibility JavaVersion.VERSION_11\n  }\n}\n```\n\nQuesto approccio richiede che **tutti** gli sviluppatori Android abbiano il Flutter SDK installato, ma abilita hot reload e debug integrato: è l'opzione consigliata durante lo sviluppo attivo.\n\n### Integrazione via AAR\n\nSe il team nativo non deve installare Flutter (scenario tipico in aziende con pipeline consolidate), si pubblica il modulo:\n\n```bash\ncd my_flutter\nflutter build aar\n```\n\nIl comando produce un repository Maven locale e stampa lo snippet da incollare nel Gradle. Da notare la mappatura dei build type: Flutter genera varianti `debug`, `profile` e `release`, e il progetto nativo deve dichiarare un `profile` build type per non rompere la risoluzione delle dipendenze.\n\n### Lanciare una schermata\n\nIl modo più semplice è `FlutterActivity`:\n\n```kotlin\nstartActivity(FlutterActivity.createDefaultIntent(this))\n```\n\nPer inserire Flutter dentro una UI nativa esistente (per esempio in un tab di una `BottomNavigationView`) si usa invece `FlutterFragment`:\n\n```kotlin\nval fragment = FlutterFragment.withCachedEngine(\"main_engine\")\n  .build\u003CFlutterFragment>()\n\nsupportFragmentManager.beginTransaction()\n  .replace(R.id.flutter_container, fragment)\n  .commit()\n```\n\n## Integrazione su iOS\n\nSu iOS la via standard è CocoaPods. Nel `Podfile`:\n\n```ruby\nflutter_application_path = '..\u002Fmy_flutter'\nload File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')\n\ntarget 'MyApp' do\n  install_all_flutter_pods(flutter_application_path)\nend\n\npost_install do |installer|\n  flutter_post_install(installer) if defined?(flutter_post_install)\nend\n```\n\nServe inoltre una build phase che esegua lo script `xcode_backend.sh` del modulo (lo `podhelper.rb` documenta i passaggi). L'alternativa \"senza Flutter SDK\" è:\n\n```bash\ncd my_flutter\nflutter build ios-framework --output=..\u002FFlutter\u002F\n```\n\nche produce `Flutter.xcframework`, `App.xcframework` e i framework dei plugin, da linkare manualmente o distribuire via CocoaPods privato\u002FSPM.\n\nPer mostrare una schermata:\n\n```swift\nimport Flutter\n\nlet flutterViewController = FlutterViewController(\n  engine: engine,\n  nibName: nil,\n  bundle: nil\n)\nflutterViewController.modalPresentationStyle = .fullScreen\npresent(flutterViewController, animated: true)\n```\n\n## Pre-riscaldare l'engine: la differenza tra fluido e lento\n\nCreare un `FlutterEngine` significa avviare la VM Dart, caricare gli asset e registrare i plugin. Farlo nel momento in cui l'utente tocca il pulsante produce un ritardo percepibile. La soluzione è istanziare l'engine in anticipo e metterlo in cache.\n\nSu Android, nella `Application`:\n\n```kotlin\nclass MyApplication : Application() {\n  lateinit var flutterEngine: FlutterEngine\n\n  override fun onCreate() {\n    super.onCreate()\n    flutterEngine = FlutterEngine(this)\n    flutterEngine.dartExecutor.executeDartEntrypoint(\n      DartExecutor.DartEntrypoint.createDefault()\n    )\n    FlutterEngineCache.getInstance().put(\"main_engine\", flutterEngine)\n  }\n}\n```\n\nE poi:\n\n```kotlin\nstartActivity(\n  FlutterActivity.withCachedEngine(\"main_engine\").build(this)\n)\n```\n\nSu iOS, nell'`AppDelegate`:\n\n```swift\nimport Flutter\nimport FlutterPluginRegistrant\n\n@UIApplicationMain\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n  lazy var flutterEngine = FlutterEngine(name: \"main_engine\")\n\n  func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    flutterEngine.run()\n    GeneratedPluginRegistrant.register(with: flutterEngine)\n    return true\n  }\n}\n```\n\nAttenzione: su iOS la registrazione dei plugin per un engine creato manualmente è **a carico tuo**, altrimenti i plugin non funzioneranno.\n\nUn engine pre-riscaldato consuma memoria anche quando nessuna schermata Flutter è visibile: valuta se pre-caricarlo all'avvio o poco prima dell'ingresso nel flusso (per esempio quando l'utente apre la sezione che contiene il pulsante).\n\n## Più schermate Flutter con FlutterEngineGroup\n\nSe l'app ha bisogno di più istanze Flutter contemporaneamente (due tab, una schermata più un modale), creare più `FlutterEngine` indipendenti è costoso: ognuno porta con sé il proprio isolate e il proprio snapshot. `FlutterEngineGroup` risolve il problema condividendo codice, snapshot GPU e risorse tra gli engine, riducendo drasticamente l'overhead di ogni engine aggiuntivo.\n\n```kotlin\nval engineGroup = FlutterEngineGroup(context)\n\nval engineA = engineGroup.createAndRunDefaultEngine(context)\nval engineB = engineGroup.createAndRunEngine(\n  context,\n  DartExecutor.DartEntrypoint(\n    FlutterInjector.instance().flutterLoader().findAppBundlePath(),\n    \"ordersMain\"\n  )\n)\n```\n\n```swift\nlet group = FlutterEngineGroup(name: \"my_group\", project: nil)\nlet engineA = group.makeEngine(withEntrypoint: nil, libraryURI: nil)\nlet engineB = group.makeEngine(withEntrypoint: \"ordersMain\", libraryURI: nil)\n```\n\nGli entrypoint alternativi vanno marcati per non essere rimossi dal tree shaking:\n\n```dart\n@pragma('vm:entry-point')\nvoid ordersMain() => runApp(const OrdersApp());\n```\n\nRicorda che gli engine di un gruppo **non condividono lo stato Dart**: ogni engine ha il suo isolate, quindi variabili globali, singleton e provider sono separati. Lo stato condiviso va gestito lato nativo o tramite persistenza.\n\n## Routing e comunicazione con il nativo\n\nPer dire a Flutter quale schermata mostrare hai tre opzioni.\n\n**1. Rotta iniziale con un engine nuovo:**\n\n```kotlin\nstartActivity(\n  FlutterActivity\n    .withNewEngine()\n    .initialRoute(\"\u002Frewards\")\n    .build(this)\n)\n```\n\n**2. `setInitialRoute` prima di eseguire l'entrypoint** (funziona solo alla prima esecuzione di un engine):\n\n```swift\nflutterEngine.run(withEntrypoint: nil, initialRoute: \"\u002Frewards\")\n```\n\n**3. MethodChannel**, l'approccio più flessibile con engine in cache, perché permette di navigare anche quando l'engine è già avviato:\n\n```dart\nfinal channel = MethodChannel('app\u002Fnavigation');\n\nvoid setupHostNavigation(GlobalKey\u003CNavigatorState> navigatorKey) {\n  channel.setMethodCallHandler((call) async {\n    if (call.method == 'navigate') {\n      final route = call.arguments as String;\n      await navigatorKey.currentState?.pushNamed(route);\n    }\n    return null;\n  });\n}\n```\n\n```kotlin\nMethodChannel(flutterEngine.dartExecutor.binaryMessenger, \"app\u002Fnavigation\")\n  .invokeMethod(\"navigate\", \"\u002Frewards\")\n```\n\nLo stesso canale serve nella direzione opposta, ad esempio per chiudere la schermata Flutter o per ottenere il token di autenticazione già presente nell'app nativa:\n\n```dart\nfinal token = await MethodChannel('app\u002Fsession')\n    .invokeMethod\u003CString>('getAuthToken');\n```\n\nÈ una buona pratica **non duplicare l'autenticazione** nel modulo Flutter: la sessione resta una sola, quella nativa, e viene esposta via channel (o via `flutter_secure_storage` se condividi keychain\u002Fkeystore).\n\n## Sviluppo e debug\n\nDurante lo sviluppo puoi lavorare in due modi:\n\n- **standalone**: `cd my_flutter && flutter run` esegue il modulo nel wrapper `.android`\u002F`.ios`, con hot reload completo. Ideale per sviluppare la UI, a patto di *mockare* i MethodChannel del lato nativo;\n- **attach**: avvii l'app nativa da Android Studio\u002FXcode, poi `flutter attach` si collega all'engine in esecuzione abilitando hot reload, DevTools e logging.\n\n```bash\ncd my_flutter\nflutter attach\n```\n\nPer i canali, isola l'accesso alla piattaforma dietro un'interfaccia: in standalone iniettarne una fake diventa banale.\n\n```dart\nabstract interface class SessionGateway {\n  Future\u003CString?> authToken();\n}\n\nclass NativeSessionGateway implements SessionGateway {\n  static const _channel = MethodChannel('app\u002Fsession');\n\n  @override\n  Future\u003CString?> authToken() => _channel.invokeMethod\u003CString>('getAuthToken');\n}\n\nclass FakeSessionGateway implements SessionGateway {\n  @override\n  Future\u003CString?> authToken() async => 'dev-token';\n}\n```\n\n## Limiti e insidie da conoscere\n\n- **Dimensione dell'app**: l'engine aggiunge alcuni MB per architettura. Su Android usa App Bundle e split per ABI; su iOS considera l'impatto sul limite di download cellulare.\n- **Un engine, una view**: un `FlutterEngine` può essere collegato a una sola `FlutterViewController`\u002F`FlutterActivity` per volta. Riutilizzare lo stesso engine per due schermate visibili contemporaneamente causa comportamenti anomali: usa `FlutterEngineGroup`.\n- **Ciclo di vita**: quando distruggi l'engine, perdi lo stato Dart. Se l'utente deve poter tornare alla schermata Flutter trovandola come l'ha lasciata, tieni l'engine in cache; altrimenti chiamalo `destroy()`\u002Frilascialo per liberare memoria.\n- **Plugin**: alcuni plugin assumono di vivere in un'app Flutter pura e possono collidere con la controparte nativa (analytics, push, permessi). Valuta se non sia meglio esporre la funzionalità nativa esistente via MethodChannel.\n- **Transizioni e back gesture**: su iOS lo swipe-to-go-back e su Android il predictive back richiedono coordinamento tra il `Navigator` di Flutter e lo stack nativo. La regola pratica è scegliere **un solo padrone** della navigazione per ogni flusso: o Flutter gestisce tutto il flusso interno e il nativo solo l'ingresso\u002Fuscita, o viceversa.\n- **Screenshot e test end-to-end**: gli strumenti nativi (Espresso, XCUITest) non vedono l'albero dei widget Flutter. Pianifica test di widget\u002Fgolden sul modulo e test nativi solo sull'integrazione.\n\n## Strategia consigliata\n\n1. Comincia con **una sola schermata a basso rischio** e misura: tempo di apertura, memoria, dimensione del binario prima e dopo.\n2. Definisci subito i **contratti dei channel** e versionali: sono l'API tra due mondi che evolvono con cadenze diverse.\n3. Automatizza la build del modulo in CI producendo AAR e xcframework versionati, così i team nativi restano indipendenti dal Flutter SDK.\n4. Espandi per **flussi verticali completi**, non per pezzetti di UI mescolati: ogni confine nativo\u002FFlutter è un costo di integrazione.\n\nAdd-to-app non è la modalità più comoda di usare Flutter, ma è quella che rende il framework adottabile in contesti dove riscrivere da zero non è un'opzione. Con engine pre-riscaldato, `FlutterEngineGroup` e confini chiari, la qualità percepita è indistinguibile da quella di un'app pienamente nativa.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F36e0e968-b0d8-4f5e-b2c9-4604eb5b608b.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1762329374519-fcd9369b9299?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTA2NTQ1MTV8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Zulfugar Karimov","https:\u002F\u002Funsplash.com\u002F@zulfugarkarimov","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fsmartphone-displaying-opera-browser-with-ai-YWHvyUYpkcc",null,"published","2026-09-29T04:01:56+00:00","Add-to-app Flutter: integrare Flutter in app native","Come aggiungere schermate Flutter a un'app Android o iOS esistente: module, FlutterEngine, FlutterEngineGroup, routing, MethodChannel e limiti da conoscere.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316908563]