Irbisa · Шпаргалка

Add-to-app и нативная интеграция

FlutterEngine, несколько engine, встраивание в существующее iOS/Android-приложение, Pigeon, плагин или FFI, размер и запуск

Senior-разработчик · 16 вопросов

  1. 01

    Что такое add-to-app и чем Flutter-модуль, созданный через flutter create -t module, отличается от Flutter-приложения?

    Add-to-app встраивает Flutter-экраны в существующее Android- или iOS-приложение вместо переписывания.

  2. 02

    Чем FlutterEngine отличается от FlutterActivity или FlutterViewController, который его показывает?

    FlutterEngine выполняет ваш Dart-код: изолят, каналы, плагины и рендерер.

  3. 03

    Как добавить Flutter-модуль в существующее iOS-приложение: через SwiftPM, CocoaPods или готовые фреймворки?

    Сегодня документированный путь ведёт через Swift Package Manager: flutter build swift-package --platform ios создаёт локальный пакет, который вы подключаете в Xcode.

    # in the Flutter module: generates build/ios/SwiftPackages/, add it to Xcode as a local packageflutter build swift-package --platform ios # build settings of the host targetFLUTTER_SWIFT_PACKAGE_OUTPUT = $(SRCROOT)/../my_flutter/build/ios/SwiftPackagesFLUTTER_APPLICATION_PATH = $(SRCROOT)/../my_flutter   # optional: Xcode rebuilds Dart…
  4. 04

    Как добавить Flutter-модуль в существующее Android-приложение (как зависимость на исходники или как AAR) и чем платите в каждом случае?

    Либо подключите Gradle-проект модуля в сборку хоста (include_flutter.groovy, затем implementation(project(":flutter"))), либо опубликуйте AAR командой flutter build aar в локальный Maven-репозиторий и зависьте от них, как от любой другой библиотеки.

    // settings.gradle.kts, option 1: source dependency (every host build compiles Dart)apply(from = File(settingsDir.parentFile, "my_flutter/.android/include_flutter.groovy"))// app/build.gradle.kts: implementation(project(":flutter")) // settings.gradle.kts, option 2: AARs from `flutter build aar`dependencyResolutionManagement {…
  5. 05

    Когда встраивать Flutter на Android как FlutterActivity, как FlutterFragment, а когда как голый FlutterView?

    FlutterActivity берут для полноэкранного Flutter-флоу, потому что кода нужно меньше всего; FlutterFragment подходит, когда Flutter является частью нативного экрана, например одной вкладкой; голый FlutterView нужен, только когда не подходит ни то ни другое, потому что жизненный цикл тогда подключаете вы сами.

    class HostActivity : FragmentActivity() {    private var flutterFragment: FlutterFragment? = null     override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)        setContentView(R.layout.host)…
  6. 06

    Кто регистрирует плагины, когда вы сами создаёте FlutterEngine, и какие плагины ломаются в add-to-app?

    На Android новый FlutterEngine регистрирует все плагины автоматически, через GeneratedPluginRegistrant; на iOS автоматически не происходит ничего, поэтому GeneratedPluginRegistrant.register(with:) вызывают на каждом запущенном engine.

    import Flutterimport FlutterPluginRegistrant @mainfinal class AppDelegate: UIResponder, UIApplicationDelegate, @preconcurrency FlutterAppLifeCycleProvider {    private let lifecycle = FlutterPluginAppLifeCycleDelegate()…
  7. 07

    Как нативный хост сообщает Flutter-модулю, какой экран открыть и с какими данными, при первом запуске и при последующих заходах?

    До запуска engine задайте ему initial route или выберите Dart entrypoint и передайте аргументы.

    // lib/main.dart of the module: the host runs main unless it names another entrypointvoid main() => runApp(const ShopApp()); // kept by the AOT compiler even though no Dart code calls it@pragma('vm:entry-point')void settingsMain() => runApp(const SettingsApp());…
  8. 08

    Найдите баг: после того как корзину перенесли в приложение Flutter-модулем, этот код блокирует сессию каждый раз, когда пользователь уходит на нативный экран.

    class SessionGuard with WidgetsBindingObserver {  SessionGuard(this.auth) {    WidgetsBinding.instance.addObserver(this);  }   final AuthService auth;   @override  void didChangeAppLifecycleState(AppLifecycleState state) {    // the user left the app: ask for Face ID when they come back    if (state == AppLifecycleState.paused) auth.lock();  }}

    В add-to-app AppLifecycleState описывает Flutter view, а не приложение.

  9. 09

    Найдите баг: каждый раз при открытии профиля появляется пустая пауза, а Flutter-экран забывает всё с прошлого захода.

    @MainActorfinal class ProfileCoordinator {    func showProfile(userId: String, from host: UIViewController) {        let engine = FlutterEngine(name: "profile")        engine.run(withEntrypoint: nil, initialRoute: "/profile/\(userId)")        GeneratedPluginRegistrant.register(with: engine)         let flutterVC = FlutterViewController(engine: engine, nibName: nil, bundle: nil)        flutterVC.modalPresentationStyle = .fullScreen        host.present(flutterVC, animated: true)    }}

    showProfile на каждое нажатие создаёт и запускает новый FlutterEngine, поэтому каждый заход платит полный старт engine до первого кадра и начинается с пустой Dart-кучи.

  10. 10

    Как отлаживать Flutter-часть и делать hot reload, если приложение запускается из Xcode или Android Studio как нативное?

    Запустите хост-приложение в debug из Xcode или Android Studio, затем выполните flutter attach в папке модуля: он найдёт Dart VM service работающего engine и даст hot reload, hot restart, DevTools и точки останова в Dart.

  11. 11

    Приложение показывает Flutter в нескольких местах сразу: во вкладке, в карточке, в полноэкранном флоу. Зачем нужен FlutterEngineGroup и что эти engine могут разделять?

    Engine одной FlutterEngineGroup разделяют AOT-код, шрифты и GPU-контекст, поэтому лишний engine дешевле по памяти и стартует быстрее независимого.

    class App : Application() {    lateinit var engines: FlutterEngineGroup     override fun onCreate() {        super.onCreate()        engines = FlutterEngineGroup(this)…
  12. 12

    Пользователь идёт native → Flutter → native → Flutter. Кто владеет навигацией и жестом «назад» на этой границе?

    Стеком между экранами владеет хост, а стеком внутри своего view владеет Flutter.

  13. 13

    Как спроектировать API между нативным хостом и Flutter-модулем, чтобы две команды могли безопасно его менять?

    Относитесь к нему как к версионированному контракту: опишите его один раз в Pigeon-файле, сгенерируйте из него Kotlin, Swift и Dart, держите его небольшим, с немногими укрупнёнными вызовами, а общим состоянием вроде сессии пусть владеет хост.

  14. 14

    В хосте уже есть ядро на C++ и SDK на Kotlin/Swift. Как Flutter-модулю вызывать каждое: через Pigeon, dart:ffi, ffigen или jnigen?

    Ядро на C++ вызывайте через dart:ffi: это один и тот же код на обеих платформах, и сериализация не нужна.

    import 'dart:ffi';import 'dart:io'; // The host already ships the C++ core: Android packages libcore.so,// iOS links it statically into the app binary.final DynamicLibrary _core = Platform.isIOS…
  15. 15

    Во что хост-приложению обходится Flutter-модуль по размеру загрузки и времени старта и как держать эту цену низкой?

    Размер: engine плюс ваш AOT-скомпилированный Dart, ассеты и плагины занимают несколько мегабайт на каждую архитектуру процессора.

    class App : Application() {    override fun onCreate() {        super.onCreate()        // cheap: loads libflutter.so and extracts assets on a background thread        FlutterInjector.instance().flutterLoader().startInitialization(this)…
  16. 16

    Вам поручили переводить большое нативное приложение на Flutter экран за экраном. Как вы спланируете внедрение add-to-app?

    Начните с самодостаточного флоу, мало связанного с остальным приложением, сначала наладьте сборку, тонкий API хоста и стратегию engine, оставьте хосту владение авторизацией, навигацией и состоянием уровня приложения и наращивайте долю Flutter, пока не станет дешевле перевернуть схему встраивания.