flutter 專題 六十四 在原生項目中集成Flutter

概述

使用Flutter從零開始開發App是一件輕松愜意的事情,但對于一些成熟的產品來說,完全摒棄原有App的歷史沉淀,全面轉向Flutter是不現實的。因此使用Flutter去統一Android、iOS技術棧,把它作為已有原生App的擴展能力,通過有序推進來提升移動終端的開發效率。
目前,想要在已有的原生App里嵌入一些Flutter頁面主要有兩種方案。一種是將原生工程作為Flutter工程的子工程,由Flutter進行統一管理,這種模式稱為統一管理模式。另一種是將Flutter工程作為原生工程的子模塊,維持原有的原生工程管理方式不變,這種模式被稱為三端分離模式,如下圖所示。
在這里插入圖片描述
三端代碼分離模式的原理是把Flutter模塊作為原生工程的子模塊,從而快速地接入Flutter模塊,降低原生工程的改造成本。在Flutter 1.1x時代,在原生已有app中接入Flutter的步驟比較繁瑣,具體可以可以參考:Flutter與原生混合開發
不過,從Flutter 1.20.x版本開始,Flutter對原生app接入Flutter進行了優化和升級,下面是具體介紹。

原生Android集成Flutter

支持的特性

  • 在 Gradle 腳本中添加一個自動構建并引入 Flutter 模塊的 Flutter SDK 鉤子。

  • 將 Flutter 模塊構建為通用的 Android Archive (AAR) 以便集成到您自己的構建系統中,并提高 Jetifier 與 AndroidX 的互操作性;

  • FlutterEngine API 用于啟動并持續地為掛載 FlutterActivity 或 FlutterFragment 提供獨立的 Flutter 環境;

  • Android Studio 的 Android 與 Flutter 同時編輯,以及 Flutter module 創建與導入向導;

  • 支持Java 和 Kotlin 為宿主的應用程序;

集成Flutter

首先,我們來看一下最終的效果,如下圖所示。
在這里插入圖片描述

集成Flutter主要有兩種方式,一種是使用Android Studio工具的方式,另一種是使用手動的方式。

使用Android Studio方式

直接使用 Android Studio 是在現有應用中自動集成 Flutter 模塊比較便捷的方法。在 Android Studio 打開現有的 Android 原生項目,然后依次點擊菜單按鈕 File > New > New Module…創建出一個可以集成的新 Flutter 模塊,或者選擇導入已有的 Flutter 模塊,如下圖所示。
在這里插入圖片描述
選擇Module的類型為Flutter Module,然后在向導窗口中填寫模塊名稱、路徑等信息,如下圖所示。
在這里插入圖片描述

此時,Android Studio 插件就會自動為這個 Android 項目配置添加 Flutter 模塊作為依賴項,這時集成應用就已準備好進行下一步的構建。

手動集成

如果想要在不使用 Flutter 的 Android Studio 插件的情況下手動將 Flutter 模塊與現有的 Android 應用集成,可以使用下面的步驟。

假設我們的原生應用在 some/path/MyApp 路徑下,那么在Flutter 項目的同級目錄下新建一個Flutter模塊,命令如下。

cd some/path/
flutter create -t module --org com.example my_flutter

完成上面的命令后,會在 some/path/my_flutter/ 目錄下創建一個 Flutter 模塊項目。該模塊項目會包含一些 Dart 代碼和一些一個隱藏的子文件夾 .android/,.android 文件夾包含一個 Android 項目,該項目不僅可以幫助你通過 flutter run 運行這個 Flutter 模塊的獨立應用,而且還可以作為封裝程序來幫助引導 Flutter 模塊作為可嵌入的 Android 庫。

同時,由于Flutter Android 引擎需要使用到 Java 8 中的新特性。因此,需要在宿主 Android 應用的 build.gradle 文件的 android { } 塊中聲明了以下源兼容性代碼。

android {//...compileOptions {sourceCompatibility 1.8targetCompatibility 1.8}
}

接下來,需要將Flutter module添加到原生Android工程的依賴中。將 Flutter 模塊添加到原生Android應用程序中主要有兩種方法實現。使用AAR包方式和直接使用module源碼的方式。使用AAR包方式需要先將Flutter 模塊打包成AAR包。假設,你的 Flutter 模塊在 some/path/my_flutter 目錄下,那么打包AAR包的命令如下。

cd some/path/my_flutter
flutter build aar

然后,根據屏幕上的提示完成集成操作,如下圖所示,當然也可以在Android原生工程中進行手動添加依賴代碼。
在這里插入圖片描述

事實上,該命令主要用于創建(默認情況下創建 debug/profile/release 所有模式)本地存儲庫,主要包含以下文件,如下所示。

build/host/outputs/repo
└── com└── example└── my_flutter├── flutter_release│   ├── 1.0│   │   ├── flutter_release-1.0.aar│   │   ├── flutter_release-1.0.aar.md5│   │   ├── flutter_release-1.0.aar.sha1│   │   ├── flutter_release-1.0.pom│   │   ├── flutter_release-1.0.pom.md5│   │   └── flutter_release-1.0.pom.sha1│   ├── maven-metadata.xml│   ├── maven-metadata.xml.md5│   └── maven-metadata.xml.sha1├── flutter_profile│   ├── ...└── flutter_debug└── ...

可以發現,使用上面的命令編譯的AAR包主要分為debug、profile和release三個版本,使用哪個版本的AAR需要根據原生的環境進行選擇。找到AAR包,然后再Android宿主應用程序中修改 app/build.gradle 文件,使其包含本地存儲庫和上述依賴項,如下所示。

android {// ...
}repositories {maven {url 'some/path/my_flutter/build/host/outputs/repo'// This is relative to the location of the build.gradle file// if using a relative path.}maven {url 'https://storage.googleapis.com/download.flutter.io'}
}dependencies {// ...debugImplementation 'com.example.flutter_module:flutter_debug:1.0'profileImplementation 'com.example.flutter_module:flutter_profile:1.0'releaseImplementation 'com.example.flutter_module:flutter_release:1.0'
}

當然,除了命令方式外,還可以使用Android Studio來構建AAR包。依次點擊 Android Studio 菜單中的 Build > Flutter > Build AAR 即可構建Flutter 模塊的 AAR包,如下圖所示。
在這里插入圖片描述

除了AAR包方式外,另一種方式就是使用源碼的方式進行依賴,即將flutter_module模塊作為一個模塊添加到Android原生工程中。首先,將Flutter 模塊作為子項目添加到宿主應用的 settings.gradle 中,如下所示。

// Include the host app project.
include ':app'                                   
setBinding(new Binding([gradle: this]))                              
evaluate(new File(                                                      settingsDir.parentFile,                                           'my_flutter/.android/include_flutter.groovy'                       
))                                                                   

binding 和 evaluation 腳本可以使 Flutter 模塊將其自身(如 :flutter)和該模塊使用的所有 Flutter 插件(如 :package_info,:video_player 等)都包含在 settings.gradle 上下文中,然后在原生Android工程的app目錄下的build.gradle文件下添加如下依賴代碼。

dependencies {implementation project(':flutter')
}

到此,在原生Android工程中集成Flutter環境就完成了,接下來編寫代碼即可。

添加Flutter頁面

正常跳轉

1, 添加FlutterActivity
Flutter提供了一個FlutterActivity來作為Flutter的容器頁面,FlutterActivity和Android原生的Activity沒有任何區別,可以認為它是Flutter的父容器組件,但在原生Android程序中,它就是一個普通的Activity,這個Activity必須在AndroidManifest.xml中進行注冊,如下所示。

<activityandroid:name="io.flutter.embedding.android.FlutterActivity"android:theme="@style/LaunchTheme"android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"android:hardwareAccelerated="true"android:windowSoftInputMode="adjustResize" />

對于theme屬性,我們可以使用Android的其他樣式進行替換,此主題樣式會決定了應用的系統樣式。

2,打開FlutterActivity

在AndroidManifest.xml中注冊FlutterActivity后,然后我們可以在任何地方啟動這個FlutterActivity,如下所示。

myButton.setOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {startActivity(FlutterActivity.createDefaultIntent(MainActivity.this));}
});

運行上面的代碼,發現并不會跳轉到Flutter頁面,因為我們并沒有提供跳轉的地址。下面的示例將演示如何使用自定義路由跳轉到Flutter模塊頁面中,如下所示。

myButton.addOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {startActivity(FlutterActivity.withNewEngine().initialRoute("/my_route").build(currentActivity));}
});

其中,my_route為Flutter模塊的初始路由,關于Flutter的路由知識,可以看下面的文章:Flutter開發之路由與導航

我們使用withNewEngine()工廠方法配置,創建一個的FlutterEngine實例。當運行上面的代碼時,應用就會由原生頁面跳轉到Flutter模塊頁面。

3,使用帶有緩存的FlutterEngine

每個FlutterActivity在默認情況下都會創建自己的FlutterEngine,并且每個FlutterEngine在啟動時都需要有一定的預熱時間。這意味著在原生頁面跳轉到Flutter模塊頁面之前會一定的時間延遲。為了盡量減少這個延遲,你可以在啟動Flutter頁面之前先預熱的FlutterEngine。即在應用程序中運行過程中找一個合理的時間實例化一個FlutterEngine,如在Application中進行初始化,如下所示。

public class MyApplication extends Application {@Overridepublic void onCreate() {super.onCreate();flutterEngine = new FlutterEngine(this);flutterEngine.getDartExecutor().executeDartEntrypoint(DartEntrypoint.createDefault());FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);}
}

其中,FlutterEngineCache的ID可以是任意的字符串,使用時請確保傳遞給任何使用緩存的FlutterEngine的FlutterFragment或FlutterActivity使用的是相同的ID。完成上面的自定義Application后,我們還需要在原生Android工程的AndroidManifest.xml中使用自定義的Application,如下所示。

<applicationandroid:name="MyApplication"android:theme="@style/AppTheme">
</application>

下面我們來看一下如何在FlutterActivity頁面中使用緩存的FlutterEngine,現在使用FlutterActivity跳轉到Flutter模塊時需要使用上面的ID,如下所示。

myButton.addOnClickListener(new OnClickListener() {@Overridepublic void onClick(View v) {startActivity(FlutterActivity.withCachedEngine("my_engine_id").build(currentActivity));}
});

可以發現,在使用withCachedEngine()工廠方法后,打開Flutter模塊的延遲時間大大降低了。

4,使用緩存引擎的初始路由
當使用帶有FlutterEngine配置的FlutterActivity或者FlutterFragment時,會有初始路由的概念,我們可以在代碼中添加跳轉到Flutter模塊的初始路由。然而,當我們使用帶有緩存的FlutterEngine時,FlutterActivity和FlutterFragment并沒有提供初始路由的概念。如果開發人員希望使用帶有緩存的FlutterEngine時也能自定義初始路由,那么可以在執行Dart入口點之前配置他們的緩存FlutterEngine以使用自定義初始路由,如下所示。

public class MyApplication extends Application {@Overridepublic void onCreate() {super.onCreate();flutterEngine = new FlutterEngine(this);flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");flutterEngine.getDartExecutor().executeDartEntrypoint(DartEntrypoint.createDefault());FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);}
}
帶有背景樣式的跳轉

如果要修改跳轉的樣式,那么可以在原生Android端自定義一個主題樣式呈現一個半透明的背景。首先打開res/values/styles.xml文件,然后添加自定義的主題,如下所示。

<style name="MyTheme" parent="@style/AppTheme"><item name="android:windowIsTranslucent">true</item></style>

然后,將FlutterActivity的主題改為我們自定義的主題,如下所示。

<activityandroid:name="io.flutter.embedding.android.FlutterActivity"android:theme="@style/MyTheme"android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"android:hardwareAccelerated="true"android:windowSoftInputMode="adjustResize"/>

然后,就可以使用透明背景啟動FlutterActivity,如下所示。

// Using a new FlutterEngine.
startActivity(FlutterActivity.withNewEngine().backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent).build(context)
);// Using a cached FlutterEngine.
startActivity(FlutterActivity.withCachedEngine("my_engine_id").backgroundMode(FlutterActivityLaunchConfigs.BackgroundMode.transparent).build(context)
);

添加FlutterFragment

在Android開發中,除了Activity之外,還可以使用Fragment來加載頁面,Fragment比Activity的粒度更小,有碎片化的意思。如果有碎片化加載的場景,那么可以使用FlutterFragment 。FlutterFragment允許開發者控制以下操作:

  • 初始化Flutter的路由;
  • Dart的初始頁面的飛入樣式;
  • 設置不透明和半透明背景;
  • FlutterFragment是否可以控制Activity;
  • FlutterEngine或者帶有緩存的FlutterEngine是否能使用;

1,將FlutterFragment 添加到Activity
使用FlutterFragment要做的第一件事就是將其添加到宿主Activity中。為了給宿主Activity添加一個FlutterFragment,需要在Activity的onCreate()中實例化并附加一個FlutterFragment的實例,這和原生Android的Fragment使用方法是一樣的,代碼如下:

public class MyActivity extends FragmentActivity {private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment";private FlutterFragment flutterFragment;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.my_activity_layout);FragmentManager fragmentManager = getSupportFragmentManager();flutterFragment = (FlutterFragment) fragmentManager.findFragmentByTag(TAG_FLUTTER_FRAGMENT);if (flutterFragment == null) {flutterFragment = FlutterFragment.createDefault();fragmentManager.beginTransaction().add( R.id.fragment_container, flutterFragment, TAG_FLUTTER_FRAGMENT ).commit();}}
}

其中,代碼中用到的原生Fragment的布局代碼如下所示。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".MainActivity"><FrameLayoutandroid:id="@+id/fragment_container"android:layout_width="match_parent"android:layout_height="match_parent" /></androidx.constraintlayout.widget.ConstraintLayout>

然后,將原生Android的啟動頁面改為我們的MyActivity即可。除此之外,我們還可以借助FlutterFragment來獲取原生代碼的生命周期,并作出相關的邏輯操作,如下所示。

public class MyActivity extends FragmentActivity {@Overridepublic void onPostResume() {super.onPostResume();flutterFragment.onPostResume();}@Overrideprotected void onNewIntent(@NonNull Intent intent) {flutterFragment.onNewIntent(intent);}@Overridepublic void onBackPressed() {flutterFragment.onBackPressed();}@Overridepublic void onRequestPermissionsResult(int requestCode,@NonNull String[] permissions,@NonNull int[] grantResults) {flutterFragment.onRequestPermissionsResult(requestCode,permissions,grantResults);}@Overridepublic void onUserLeaveHint() {flutterFragment.onUserLeaveHint();}@Overridepublic void onTrimMemory(int level) {super.onTrimMemory(level);flutterFragment.onTrimMemory(level);}
}

不過,上面的示例啟動時使用了一個新的FlutterEngine,因此啟動后會需要一定的初始化時間,導致應用啟動后會有一個空白的UI,直到FlutterEngine初始化成功后Flutter模塊的首頁渲染完成。對于這種現象,我們同樣可以在提前初始化FlutterEngine,即在應用程序的Application中初始化FlutterFragment,如下所示。

public class MyApplication extends Application {FlutterEngine flutterEngine=null;@Overridepublic void onCreate() {super.onCreate();flutterEngine = new FlutterEngine(this);flutterEngine.getNavigationChannel().setInitialRoute("your/route/here");flutterEngine.getDartExecutor().executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault());FlutterEngineCache.getInstance().put("my_engine_id", flutterEngine);}
}

在上面的代碼中,通過設置導航通道的初始路由,然后關聯的FlutterEngine在初始執行runApp() ,在初始執行runApp()后再改變導航通道的初始路由屬性是沒有效果的。然后,我們修改MyFlutterFragmentActivity類的代碼,并使用FlutterFragment.withNewEngine()使用緩存的FlutterEngine,如下所示。

public class MyFlutterFragmentActivity extends FragmentActivity {private static final String TAG_FLUTTER_FRAGMENT = "flutter_fragment";private FlutterFragment flutterFragment = null;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.flutter_fragment_activity);FragmentManager fragmentManager = getSupportFragmentManager();if (flutterFragment == null) {flutterFragment=FlutterFragment.withNewEngine().initialRoute("/").build();fragmentManager.beginTransaction().add(R.id.fragment_container, flutterFragment,TAG_FLUTTER_FRAGMENT).commit();}}
}
控制FlutterFragment的渲染模式

FlutterFragment默認使用SurfaceView來渲染它的Flutter內容,除此之外,還可以使用TextureView來渲染界面,不過SurfaceView的性能比TextureView好得多。但是,SurfaceView不能交錯在Android視圖層次結構中使用。此外,在Android N之前的Android版本中,SurfaceViews不能動畫化,因為它們的布局和渲染不能與其他視圖層次結構同步,此時,你需要使用TextureView而不是SurfaceView,使用 TextureView來渲染FlutterFragment的代碼如下。

// With a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine().renderMode(FlutterView.RenderMode.texture).build();// With a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id").renderMode(FlutterView.RenderMode.texture).build();

如果要給跳轉添加一個轉場的透明效果,要啟用FlutterFragment的透明屬性,可以使用下面的配置,如下所示。

// Using a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine().transparencyMode(TransparencyMode.transparent).build();// Using a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id").transparencyMode(TransparencyMode.transparent).build();
FlutterFragment 與Activity

有時候,一些應用使用Fragment來作為Flutter頁面的承載對象時,狀態欄、導航欄和屏幕方向仍然使用的是Activity,Fragment只是作為Activity的一部分。在這些應用程序中,用一個Fragment是合理的,如下圖所示。?在這里插入圖片描述
在其他應用程序中,Fragment僅僅作為UI的一部分,此時一個FlutterFragment可能被用來實現一個抽屜的內部,一個視頻播放器,或一個單一的卡片。在這些情況下,FlutterFragment不需要全屏線上,因為在同一個屏幕中還有其他UI片段,如下圖所示。

在這里插入圖片描述
FlutterFragment提供了一個概念,用來實現FlutterFragment是否能夠控制它的宿主Activity。為了防止一個FlutterFragment將它的Activity暴露給Flutter插件,也為了防止Flutter控制Activity的系統UI,FlutterFragment提供了一個shouldAttachEngineToActivity()方法,如下所示。

// Using a new FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withNewEngine().shouldAttachEngineToActivity(false).build();// Using a cached FlutterEngine.
FlutterFragment flutterFragment = FlutterFragment.withCachedEngine("my_engine_id").shouldAttachEngineToActivity(false).build();

原生iOS集成Flutter

創建Flutter模塊

為了將 Flutter 集成到原生iOS應用里,第一步要創建一個 Flutter module,創建 Flutter module的命令如下所示。

cd some/path/
flutter create --template module my_flutter

執行完上面的命令后,會在some/path/my_flutter/ 目錄下創建一個Flutter module庫。在這個目錄中,你可以像在其它 Flutter 項目中一樣,執行 flutter 命令,比如 flutter run --debug 或者 flutter build ios。打開 my_flutter 模塊,可以發現,目錄結構和普通 的Flutter 應用的目錄別無二至,如下所示。

my_flutter/
├── .ios/
│   ├── Runner.xcworkspace
│   └── Flutter/podhelper.rb
├── lib/
│   └── main.dart
├── test/
└── pubspec.yaml

默認情況下,my_flutter的Android工程和iOS工程是隱藏的,我們可以通過顯示隱藏的項目來看到Android工程和iOS工程。

集成到已有iOS應用

在原生iOS開發中,有兩種方式可以將 Flutter 集成到你的既有應用中。
1, 使用 CocoaPods 依賴管理和已安裝的 Flutter SDK 。(推薦)
2,把 Flutter engine 、Dart 代碼和所有 Flutter plugin 編譯成 framework,然后用 Xcode 手動集成到你的應用中,并更新編譯設置。

1, 使用 CocoaPods 和 Flutter SDK 集成

使用此方法集成Flutter,需要在本地安裝了 Flutter SDK。然后,只需要在 Xcode 中編譯應用,就可以自動運行腳本來集成Dart 代碼和 plugin。這個方法允許你使用 Flutter module 中的最新代碼快速迭代開發,而無需在 Xcode 以外執行額外的命令。

現在假如又一個原生iOS工程,并且 Flutter module 和這個iOS工程是處在相鄰目錄的,如下所示。

some/path/
├── my_flutter/
│   └── .ios/
│       └── Flutter/
│         └── podhelper.rb
└── MyApp/└── Podfile

1,如果你的應用(MyApp)還沒有 Podfile,可以根據?CocoaPods 使用指南?來在項目中添加 Podfile。然后,在?Podfile?中添加下面代碼:

flutter_application_path = '../my_flutter'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')

2,每個需要集成 Flutter 的 [Podfile target][],執行?install_all_flutter_pods(flutter_application_path),如下所示。

target 'MyApp' doinstall_all_flutter_pods(flutter_application_path)
end

3,最后,在MyApp原生工程下運行 pod install命令拉取原生工程需要的插件。

pod install

如果沒有任何錯誤,界面如下圖。
在這里插入圖片描述

在上面的Podfile文件中, podhelper.rb 腳本會把你的 plugins, Flutter.framework,和 App.framework 集成到你的原生iOS項目中。同時,你應用的 Debug 和 Release 編譯配置,將會集成相對應的 Debug 或 Release 的 編譯產物。可以增加一個 Profile 編譯配置用于在 profile 模式下測試應用。然后,在 Xcode 中打開 MyApp.xcworkspace ,可以使用 【?B 】快捷鍵編譯項目,并運行項目即可。

使用frameworks集成

除了上面的方法,你也可以創建一個 frameworks,手動修改既有 Xcode 項目,將他們集成進去。但是每當你在 Flutter module 中改變了代碼,都必須運行 flutter build ios-framework來編譯framework。下面的示例假設你想在 some/path/MyApp/Flutter/ 目錄下創建 frameworks。

flutter build ios-framework --output=some/path/MyApp/Flutter/

此時的文件目錄如下所示。

some/path/MyApp/
└── Flutter/├── Debug/│   ├── Flutter.framework│   ├── App.framework│   ├── FlutterPluginRegistrant.framework (only if you have plugins with iOS platform code)│   └── example_plugin.framework (each plugin is a separate framework)├── Profile/│   ├── Flutter.framework│   ├── App.framework│   ├── FlutterPluginRegistrant.framework│   └── example_plugin.framework└── Release/├── Flutter.framework├── App.framework├── FlutterPluginRegistrant.framework└── example_plugin.framework

然后,使用 Xcode 打開原生iOS工程,并將生成的 frameworks 集成到既有iOS應用中。例如,你可以在 some/path/MyApp/Flutter/Release/ 目錄拖拽 frameworks 到你的應用 target 編譯設置的 General > Frameworks, Libraries, and Embedded Content 下,然后在 Embed 下拉列表中選擇 “Embed & Sign”。

1, 鏈接到框架

當然,你也可以將框架從 Finder 的 some/path/MyApp/Flutter/Release/ 拖到你的目標項目中,然后點擊 build settings > Build Phases > Link Binary With Libraries。然后,在 target 的編譯設置中的 Framework Search Paths (FRAMEWORK_SEARCH_PATHS) 增加 $(PROJECT_DIR)/Flutter/Release/,如下圖所示。
在這里插入圖片描述
2,內嵌框架
生成的動態framework框架必須嵌入你的應用才能在運行時被加載。需要說明的是插件會幫助你生成 靜態或動態框架。靜態框架應該直接鏈接而不是嵌入,如果你在應用中嵌入了靜態框架,你的應用將不能發布到 App Store 并且會得到一個 Found an unexpected Mach-O header code 的 archive 錯誤。

你可以從應用框架組中拖拽框架(除了 FlutterPluginRegistrant 以及其他的靜態框架)到你的目標 ‘ build settings > Build Phases > Embed Frameworks,然后從下拉菜單中選擇 “Embed & Sign”,如下圖所示。

在這里插入圖片描述

3,使用 CocoaPods 在 Xcode 和 Flutter 框架中內嵌應用

除了使用Flutter.framework方式外,你還可以加入一個參數 --cocoapods ,然后將 Flutter 框架作為一個 CocoaPods 的 podspec 文件分發。這將會生成一個 Flutter.podspec 文件而不再生成 Flutter.framework 引擎文件,命令如下。

flutter build ios-framework --cocoapods --output=some/path/MyApp/Flutter/

執行命令后,Flutter模塊的目錄如下圖所示。

some/path/MyApp/
└── Flutter/├── Debug/│   ├── Flutter.podspec│   ├── App.framework│   ├── FlutterPluginRegistrant.framework│   └── example_plugin.framework (each plugin with iOS platform code is a separate framework)├── Profile/│   ├── Flutter.podspec│   ├── App.framework│   ├── FlutterPluginRegistrant.framework│   └── example_plugin.framework└── Release/├── Flutter.podspec├── App.framework├── FlutterPluginRegistrant.framework└── example_plugin.framework

然后,在iOS應用程序使用CocoaPods添加Flutter以來文件即可,如下所示。

pod 'Flutter', :podspec => 'some/path/MyApp/Flutter/[build mode]/Flutter.podspec'

添加一個Flutter頁面

FlutterEngine 和 FlutterViewController

為了在原生 iOS 應用中展示 Flutter 頁面,需要使用到FlutterEngine?和?FlutterViewController。其中,FlutterEngine 充當 Dart VM 和 Flutter 運行時環境; FlutterViewController 依附于 FlutterEngine,給 Flutter 傳遞 UIKit 的輸入事件,并展示被 FlutterEngine 渲染的每一幀畫面。

1,創建一個 FlutterEngine
創建 FlutterEngine 的時機由您自己決定。作為示例,我們將在應用啟動的 app delegate 中創建一個 FlutterEngine,并作為屬性暴露給外界。首先,在在 AppDelegate.h文件中添加如下代碼。

@import UIKit;
@import Flutter;@interface AppDelegate : FlutterAppDelegate // More on the FlutterAppDelegate below.
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

然后,在 AppDelegate.m文件中添加如下代碼。

// Used to connect plugins (only if you have plugins with iOS platform code).
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h>#import "AppDelegate.h"@implementation AppDelegate- (BOOL)application:(UIApplication *)applicationdidFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id> *)launchOptions {self.flutterEngine = [[FlutterEngine alloc] initWithName:@"my flutter engine"];// Runs the default Dart entrypoint with a default Flutter route.[self.flutterEngine run];// Used to connect plugins (only if you have plugins with iOS platform code).[GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];return [super application:application didFinishLaunchingWithOptions:launchOptions];
}@end

需要說明的是,GeneratedPluginRegistrant只有在需要支持的插件才能使用。然后運行項目,結果報了一個framework not found FlutterPluginRegistrant錯誤。

ld: warning: directory not found for option '-F/Users/bilibili/Library/Developer/Xcode/DerivedData/iOSFlutterHybird-advitqdrflrsxldrjkqcsvdzxbop/Build/Products/Debug-iphonesimulator/FlutterPluginRegistrant'
ld: framework not found FlutterPluginRegistrant
clang: error: linker command failed with exit code 1 (use -v to see invocation)

對于這個錯誤,需要打開項目編譯配置,修改Bitcode。默認情況下,Flutter是不支持Bitcode的,Bitcode是一種iOS編譯程序的中間代碼,在原生iOS工程中集成Flutter需要禁用Bitcode,如下圖所示。
在這里插入圖片描述

2,使用 FlutterEngine 展示 FlutterViewController
在下面的例子中,展示了一個普通的 ViewController,當點擊頁面中的UIButton時就會跳轉到 FlutterViewController 的 ,這個 FlutterViewController 使用在 AppDelegate 中創建的 Flutter 引擎 (FlutterEngine)。

@import Flutter;
#import "AppDelegate.h"
#import "ViewController.h"@implementation ViewController
- (void)viewDidLoad {[super viewDidLoad];// Make a button to call the showFlutter function when pressed.UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];[button addTarget:selfaction:@selector(showFlutter)forControlEvents:UIControlEventTouchUpInside];[button setTitle:@"Show Flutter!" forState:UIControlStateNormal];button.backgroundColor = UIColor.blueColor;button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);[self.view addSubview:button];
}- (void)showFlutter {FlutterEngine *flutterEngine =((AppDelegate *)UIApplication.sharedApplication.delegate).flutterEngine;FlutterViewController *flutterViewController =[[FlutterViewController alloc] initWithEngine:flutterEngine nibName:nil bundle:nil];[self presentViewController:flutterViewController animated:YES completion:nil];
}
@end

運行上面的代碼,如果出現“symbol(s) not found for architecture x86_64”的錯誤,可以使用下面的步驟進行解決。使用Xcode打開項目,然后依次選擇TARGETS->Build Phases,然后找到Compile Sources 并點擊“+”, 在搜索框輸入APPDelegate 找到他的.m文件。

在這里插入圖片描述
3,使用隱式 FlutterEngine 創建 FlutterViewController
我們可以讓 FlutterViewController 隱式的創建 FlutterEngine,而不用提前初始化一個FlutterEngine。不過不建議這樣做,因為按需創建FlutterEngine 的話,在 FlutterViewController 被 present 出來之后,第一幀圖像渲染完之前,將會有明顯的延遲。不過,當 Flutter 頁面很少被展示時,可以使用此方式。

為了不使用已經存在的 FlutterEngine 來展現 FlutterViewController,省略 FlutterEngine 的創建步驟,并且在創建 FlutterViewController 時,去掉 FlutterEngine 的引用。

// Existing code omitted.
// 省略已經存在的代碼
- (void)showFlutter {FlutterViewController *flutterViewController =[[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];[self presentViewController:flutterViewController animated:YES completion:nil];
}
@end
使用 FlutterAppDelegate

FlutterAppDelegate 具備如下功能:

  • 傳遞應用的回調,例如 openURL 到 Flutter 的插件 —— local_auth。
  • 傳遞狀態欄點擊(這只能在 AppDelegate 中檢測)到 Flutter 的點擊置頂行為。

我們推薦應用的UIApplicationDelegate 繼承 FlutterAppDelegate,但不是必須的,如果你的 App Delegate 不能直接繼承 FlutterAppDelegate,那么讓你的 App Delegate 實現 FlutterAppLifeCycleProvider 協議,來確保 Flutter plugins 接收到必要的回調。否則,依賴這些事件的 plugins 將會有無法預估的行為。

@import Flutter;
@import UIKit;
@import FlutterPluginRegistrant;@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@property (nonatomic,strong) FlutterEngine *flutterEngine;
@end

然后,在具體實現中,將App Delegate委托給 FlutterPluginAppLifeCycleDelegate,如下所示。

@interface AppDelegate ()
@property (nonatomic, strong) FlutterPluginAppLifeCycleDelegate* lifeCycleDelegate;
@end@implementation AppDelegate- (instancetype)init {if (self = [super init]) {_lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];}return self;
}- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey, id>*))launchOptions {self.flutterEngine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];[self.flutterEngine runWithEntrypoint:nil];[GeneratedPluginRegistrant registerWithRegistry:self.flutterEngine];return [_lifeCycleDelegate application:application didFinishLaunchingWithOptions:launchOptions];
}// Returns the key window's rootViewController, if it's a FlutterViewController.
// Otherwise, returns nil.
- (FlutterViewController*)rootFlutterViewController {UIViewController* viewController = [UIApplication sharedApplication].keyWindow.rootViewController;if ([viewController isKindOfClass:[FlutterViewController class]]) {return (FlutterViewController*)viewController;}return nil;
}- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {[super touchesBegan:touches withEvent:event];// Pass status bar taps to key window Flutter rootViewController.if (self.rootFlutterViewController != nil) {[self.rootFlutterViewController handleStatusBarTouches:event];}
}- (void)application:(UIApplication*)application
didRegisterUserNotificationSettings:(UIUserNotificationSettings*)notificationSettings {[_lifeCycleDelegate application:application
didRegisterUserNotificationSettings:notificationSettings];
}- (void)application:(UIApplication*)application
didRegisterForRemoteNotificationsWithDeviceToken:(NSData*)deviceToken {[_lifeCycleDelegate application:application
didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}- (void)application:(UIApplication*)application
didReceiveRemoteNotification:(NSDictionary*)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {[_lifeCycleDelegate application:applicationdidReceiveRemoteNotification:userInfofetchCompletionHandler:completionHandler];
}- (BOOL)application:(UIApplication*)applicationopenURL:(NSURL*)urloptions:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {return [_lifeCycleDelegate application:application openURL:url options:options];
}- (BOOL)application:(UIApplication*)application handleOpenURL:(NSURL*)url {return [_lifeCycleDelegate application:application handleOpenURL:url];
}- (BOOL)application:(UIApplication*)applicationopenURL:(NSURL*)urlsourceApplication:(NSString*)sourceApplicationannotation:(id)annotation {return [_lifeCycleDelegate application:applicationopenURL:urlsourceApplication:sourceApplicationannotation:annotation];
}- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItemcompletionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {[_lifeCycleDelegate application:applicationperformActionForShortcutItem:shortcutItemcompletionHandler:completionHandler];
}- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifiercompletionHandler:(nonnull void (^)(void))completionHandler {[_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifiercompletionHandler:completionHandler];
}- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {[_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {[_lifeCycleDelegate addDelegate:delegate];
}
@end

啟動選項

上面例子使用默認配置來啟動 Flutter,為了定制化你的 Flutter 運行時,我們可以指定 Dart 入口、庫和路由。

1,指定Dart 入口

在 FlutterEngine 上調用 run()函數,默認將會調用你的 lib/main.dart 文件里的 main() 函數。不過,我們可以使用入口方法?runWithEntrypoint()來指定一個Dart 入口,并且,使用 main() 以外的 Dart 入口函數,必須使用下面的注解,防止被 tree-shaken 優化掉,而沒有進行編譯。如下所示。

  @pragma('vm:entry-point')void myOtherEntrypoint() { ... };

2,指定Dart 庫
同時,Flutter允許開發者在指定 Dart 函數時指定特定文件。例如使用 lib/other_file.dart 文件的 myOtherEntrypoint() 函數取代 lib/main.dart 的 main() 函數,如下所示。

[flutterEngine runWithEntrypoint:@"myOtherEntrypoint" libraryURI:@"other_file.dart"];

3,指定Dart 路由

當然,當構建Flutter Engine 時,還可以為你的 Flutter 應用設置一個初始路由,如下所示。

FlutterEngine *flutterEngine =[[FlutterEngine alloc] initWithName:@"my flutter engine"];
[[flutterEngine navigationChannel] invokeMethod:@"setInitialRoute"arguments:@"/onboarding"];
[flutterEngine run];

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/81170.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/81170.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/81170.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Java高階程序員學習計劃(詳細到天,需有一定Java基礎)

??致敬讀者 ??感謝閱讀??笑口常開??生日快樂?早點睡覺??博主相關 ??博主信息??博客首頁??專欄推薦??活動信息文章目錄 Java高階程序員學習計劃(詳細到天,需有一定Java基礎)第一階段(30天)Java基礎:Java生態工具鏈:設計模式與編碼規范:第二階段(15天…

JS自動化獲取網站信息開發說明

一、自動獲取信息的必要性 1. 提高效率與節省時間 批量處理&#xff1a;自動化可以快速抓取大量數據&#xff0c;比人工手動操作快得多。 24/7 運行&#xff1a;自動化工具可以全天候工作&#xff0c;不受時間限制。 減少重復勞動&#xff1a;避免人工反復執行相同的任務&am…

Android Kotlin 依賴注入全解:Koin appModule 配置與多 ViewModel 數據共享實戰指南

一、基礎配置與概念 1. 什么是 appModule appModule 是 Koin 依賴注入框架中的核心配置模塊&#xff0c;用于集中管理應用中的所有依賴項。它本質上是一個 Koin 模塊&#xff08;org.koin.core.module.Module&#xff09;&#xff0c;通過 DSL 方式聲明各種組件的創建方式和依…

學習記錄:DAY21

我的開發日志&#xff1a;類路徑掃描、DI 容器與動態代理 前言 我失憶了&#xff0c;完全不記得自己早上干了什么。 日程 早上 10 點左右開始&#xff0c;學了一早上&#xff0c;主要是類路徑掃描相關的調試。 晚上 8 點了&#xff0c;真不能再摸&#x1f41f;了。 學習記錄 計…

【Agent】MCP協議 | 用高德MCP Server制作旅游攻略

note MCP (Model Context Protocol) 代表了 AI 與外部工具和數據交互的標準建立。MCP 的本質&#xff1a;它是一個統一的協議標準&#xff0c;使 AI 模型能夠以一致的方式連接各種數據源和工具&#xff0c;類似于 AI 世界的"USB-C"接口。 它能夠在 LLM/AI Agent 與外…

使用 Spring Data Redis 實現 Redis 數據存儲詳解

使用 Spring Data Redis 實現 Redis 數據存儲詳解 Spring Data Redis 是 Spring 生態中操作 Redis 的核心模塊&#xff0c;它封裝了 Redis 客戶端的底層細節&#xff08;如 Jedis 或 Lettuce&#xff09;&#xff0c;提供了統一的 API 來操作 Redis 的數據結構。以下是詳細實現…

Qt5與現代OpenGL學習(四)X軸方向旋轉60度

把上面兩張圖像放到D盤1文件夾內&#xff1a; shader.h #ifndef SHADER_H #define SHADER_H#include <QDebug> #include <QOpenGLShader> #include <QOpenGLShaderProgram> #include <QString>class Shader { public:Shader(const QString& verte…

【Machine Learning Q and AI 讀書筆記】- 02 自監督學習

Machine Learning Q and AI 中文譯名 大模型技術30講&#xff0c;主要總結了大模型相關的技術要點&#xff0c;結合學術和工程化&#xff0c;對LLM從業者來說&#xff0c;是一份非常好的學習實踐技術地圖. 本文是Machine Learning Q and AI 讀書筆記的第2篇&#xff0c;對應原…

using var connection = connectionFactory.CreateConnection(); using var 是什么意思

在 .NET 中&#xff0c;??垃圾回收&#xff08;Garbage Collection, GC&#xff09;?? 確實是自動管理內存的機制&#xff0c;但它 ??僅適用于托管資源&#xff08;Managed Resources&#xff09;??&#xff08;如類實例、數組等&#xff09;。然而&#xff0c;對于 ?…

Multicore-TSNE

文章目錄 TSNE使用scikit-learn庫使用Multicore-TSNE庫安裝方法基本使用方法采用不同的距離度量 其他資料 TSNE t-Distributed Stochastic Neighbor Embedding (t-SNE) 是一種高維數據的降維方法&#xff0c;由Laurens van der Maaten和Geoffrey Hinton于2008年提出&#xff0…

SI5338-EVB Usage Guide(LVPECL、LVDS、HCSL、CMOS、SSTL、HSTL)

目錄 1. 簡介 1.1 EVB 介紹 1.2 Si5338 Block Diagram 2. EVB 詳解 2.1 實物圖 2.2 基本配置 2.2.1 Universal Pin 2.2.2 IIC I/F 2.2.3 Input Clocks 2.2.4 Output Frequencies 2.2.5 Output Driver 2.2.6 Freq and Phase Offset 2.2.7 Spread Spectrum 2.2.8 快…

Spring AI應用系列——基于OpenTelemetry實現大模型調用的可觀測性實踐

一、項目背景與目標 在AI應用日益復雜的今天&#xff0c;大模型服務&#xff08;如語言理解和生成&#xff09;的性能監控和問題排查變得尤為關鍵。為了實現對大模型調用鏈路的可觀測性&#xff08;Observability&#xff09;管理&#xff0c;我們基于 Spring Boot Spring AI…

Spyglass:官方Hands-on Training(一)

相關閱讀 Spyglasshttps://blog.csdn.net/weixin_45791458/category_12828934.html?spm1001.2014.3001.5482 本文是對Spyglass Hands-on Training中第一個實驗的翻譯&#xff08;有刪改&#xff09;&#xff0c;Lab文件可以從以下鏈接獲取。Spyglass Hands-on Traininghttps:…

PCB設計工藝規范(三)走線要求

走線要求 1.走線要求2.固定孔、安裝孔、過孔要求3.基準點要求4.絲印要求 1.走線要求 印制板距板邊距離:V-CUT 邊大于 0.75mm&#xff0c;銑槽邊大于0.3mm。為了保證 PCB 加工時不出現露銅的缺陷&#xff0c;要求所有的走線及銅箔距離板邊:V-CUT邊大于 0.75mm&#xff0c;銑槽邊…

抓取工具Charles配置教程(mac電腦+ios手機)

mac電腦上的配置 1. 下載最新版本的Charles 2. 按照以下截圖進行配置 2.1 端口號配置&#xff1a; 2.2 https配置 3. mac端證書配置 4. IOS手機端網絡配置 4.1 先查看電腦上的配置 4.2 配置手機網絡 連接和電腦同一個wifi&#xff0c;然后按照以下截圖進行配置 5. 手機端證書…

【CSS】精通Flex布局(全)

目錄 1. flex布局體驗 1.1 傳統布局 與 flex布局 1.2 初體驗 2. flex布局原理 2.1 布局原理 3. flex布局父項常見屬性 3.1 常見父項屬性 3.2 屬性值 3.3 justify-content 設置主軸上的子元素排列方式 3.4 flex-wrap設置子元素是否換行 3.5 align-items 設置側軸上的…

力扣第447場周賽

這次終于趕上力扣的周賽了, 賽時成績如下(依舊還是三題 )&#xff1a; 1. 統計被覆蓋的建筑 給你一個正整數 n&#xff0c;表示一個 n x n 的城市&#xff0c;同時給定一個二維數組 buildings&#xff0c;其中 buildings[i] [x, y] 表示位于坐標 [x, y] 的一個 唯一 建筑。 如…

AI中常用概念的理解

1. RAG&#xff08;檢索增強生成&#xff09; 通俗理解&#xff1a;就像你寫作業時&#xff0c;先查課本 / 百度找資料&#xff0c;再根據資料寫答案&#xff0c;而不是純靠記憶瞎編。 AI 模型&#xff08;比如 ChatGPT&#xff09;回答問題時&#xff0c;先去 “數據庫 / 互聯…

SQLServer多版本兼容Java方案和數據采集

Maven引入 <dependency><groupId>com.microsoft.sqlserver</groupId><artifactId>sqljdbc4</artifactId><version>4.0</version></dependency><dependency><groupId>net.sourceforge.jtds</groupId><ar…

【每日八股】復習 Redis Day4:線程模型

文章目錄 復習 Redis Day4&#xff1a;線程模型介紹一下 Redis 的線程模型核心線程模型&#xff08;Redis 6.0 之前&#xff09;Redis 6.0 的多線程改進Redis 真的是單線程嗎&#xff1f;Redis 的線程模型剖析 上一篇 Redis 的應用我今天才完成&#xff0c;因此明天一并復習 Re…