概述
使用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];