本文翻譯自 Add-Flutter-to-existing-apps
在已有項目中繼承Flutter
約定:
本文中hostApp翻譯成: 宿主App, 主App,主應用程序,在示例中可以理解為MyApp
介紹
正在進行的一項為了更加容易地將Flutter添加到現有應用程序中的工作??梢栽?a target="_blank" rel="nofollow">Add2App project查看進度。
此頁面記錄了該工作的當前狀態,并將在我們構建必要的工具時進行更新。
最后更新時間為2018年11月26日。
“add2app”支持處于預覽狀態,目前僅在master分支上可用。
免責
由于Flutter的“Add2App”功能處于預覽狀態,因此相關的API和工具不穩定且可能會發生變化。
Flutter module項目模板
使用flutter create xxx
創建的含有Flutter/Dart代碼的Flutter項目包含非常簡單的原生應用程序(單一Activity的Android host和單一ViewController的iOS host)。你可以修改這些主應用程序以滿足你的需求并構建它們。
但是,如果你開始使用的是某一平臺的現有原生程序,你可能希望將Flutter項目作為某種形式的庫包含在該應用程序中。
這就是Flutter module模板提供的內容。執行 flutter create -t module xxx
會生成一個Flutter項目,其中包含專為現有原生應用程序使用而設計的一個Android庫和一個Cocoapods pod。
Android部分
創建Flutter module
假設你已有的一個Android應用程序some/path/MyApp
,并且希望將Flutter項目放在該同級目錄下:
$ cd some/path/
$ flutter create -t module my_flutter
這將在some/path/my_flutter/Flutte
創建一個Flutter module,其中包含一個lib/main.dart
文件作為入口,以及一個.android/
的隱藏的子文件夾,它包含了Android庫中的模塊項目。
(雖然以下不需要,但如果您愿意,可以使用Gradle構建該庫:
$ cd .android/
$ ./gradlew flutter:assembleDebug
這會在.android/Flutter/build/outputs/aar/
下生成一個flutter-debug.aar歸檔文件)
為宿主App添加Flutter module的依賴
將Flutter module作為子項目包含在主應用程序的settings.gradle
中:
// MyApp/settings.gradle
include ':app' // assumed existing content
setBinding(new Binding([gradle: this])) // new
evaluate(new File( // new
settingsDir.parentFile, // new
'my_flutter/.android/include_flutter.groovy' // new
))
通過settings.gradle的綁定和script evaluation允許Flutter module include
自己(如:flutter
)和模塊所使用的任意Flutter插件(如:package_info
, :video_player
等)
在你的應用程序中實現引入對Flutter module的依賴:
// MyApp/app/build.gradle
:
dependencies {
implementation project(':flutter')
:
}
在Java代碼中的Flutter module
使用Flutter module的Java API將Flutter視圖添加到主應用程序??梢酝ㄟ^直接使用Flutter.createView:
// MyApp/app/src/main/java/some/package/MainActivity.java
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
View flutterView = Flutter.createView(
MainActivity.this,
getLifecycle(),
"route1"
);
FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(600, 800);
layout.leftMargin = 100;
layout.topMargin = 200;
addContentView(flutterView, layout);
}
});
也可以創建一個自己處理生命周期FlutterFragment:
// MyApp/app/src/main/java/some/package/SomeActivity.java
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
tx.replace(R.id.someContainer, Flutter.createFragment("route1"));
tx.commit();
}
});
上面我們使用字符串"route1"
告訴Dart代碼在Flutter視圖中顯示哪個widget。Flutter module項目模板中的lib/main.dart
文件應該switch
(或以其他方式解釋)提供的路由字符串,也可使用window.defaultRouteName
,來確定要創建和傳遞到哪個widget到runApp
。示例:
mport 'dart:ui';
import 'package:flutter/material.dart';
void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String route) {
switch (route) {
case 'route1':
return SomeWidget(...);
case 'route2':
return SomeOtherWidget(...);
default:
return Center(
child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
);
}
}
完全取決于你想要的路由字符串以及如何解釋它們。
構建和運行應用程序
構建和運行MyApp的方式與添加Flutter module依賴項之前的方式完全相同,通常使用Android Studio。編輯,調試和分析Android代碼也是如此。
熱重啟/重新加載和調試Dart代碼
完整的IDE集成以支持使用混合應用程序的Flutter / Dart代碼正在進行中。但目前已經提供了Flutter命令行工具和Dart Observatory Web用戶界面。
連接設備或啟動模擬器。然后使Flutter CLI工具監聽應用程序:
$ cd some/path/my_flutter
$ flutter attach
Waiting for a connection from Flutter on Nexus 5X...
從Android Studio以調試模式啟動MyApp
。導航到使用Flutter的應用程序區域。然后回到終端,應該看到類似于以下內容的輸出:
Done.
Syncing files to device Nexus 5X... 5.1s
?? To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler on Nexus 5X is available at: http://127.0.0.1:59556/
For a more detailed help message, press "h". To quit, press "q".
你現在可以編輯my_flutter
的Dart代碼,按下r
終端可以重新加載更改。還可以將上面的URL粘貼到瀏覽器中,以使用Dart Observatory設置斷點,分析內存和其他調試任務。
iOS部分
創建Flutter工程模塊
假設已有iOS項目在some/path/MyApp
路徑上,我們在該同級目錄上創建一個Flutter工程模塊:
$ cd some/path/
$ flutter create -t module my_flutter
這將會在some/path/my_flutter/
目錄下創建一個以lib/main.dart
為入口的Flutter模塊項目。其中隱藏的.ios/
子目錄中包含了一些Cocopods和幫助類的Ruby腳本。
為宿主App添加Flutter依賴模塊
下面的描述是假設你現有的iOS應用程序是通過使用Xcode 10.0且使用Objective-C來創建"Single View App"
的項目。如果你現有應用程序具有不同的文件夾結構和/或/
.xcconfig
文件,則可以重復使用這些文件,但可能需要相應地調整下面提到的一些相對路徑。
假定的文件夾結構如下:
.
├── MyApp
│ ├── MyApp
│ │ ├── AppDelegate.h
│ │ ├── AppDelegate.m
│ │ └── ...
│ ...
└── my_flutter
├── .ios
│ ├── Config
│ ├── Flutter
│ ├── Runner
│ ├── Runner.xcodeproj
│ └── Runner.xcworkspace
├── lib
│ └── main.dart
├── my_flutter.iml
├── my_flutter_android.iml
├── pubspec.lock
├── pubspec.yaml
└── ..
將Flutter app添加到Podfile中
集成Flutter框架需要使用CocoaPods依賴項管理器。這是因為Flutter框架也需要可用于你可能包含在my_flutter中的任何Flutter插件。
如果需要,請參考cocoapods.org了解如何在開發設備上安裝CocoaPods。
如果你的主應用程序(MyApp
)已在使用Cocoapods,只需執行以下操作即可與my_flutter
app集成:
1.將以下幾行添加到Podfile:
flutter_application_path = 'path/to/flutter_app/'
eval(File.read(File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')), binding)
注意:根據示例的項目結構這里
path/to/flutter_app/
需要替換為../my_flutter/
2.執行pod install
每當你在some/path/my_flutter/pubspec.yaml
中更改了Flutter插件的依賴關系后,你都需要運行flutter packages get
來通過podhelper.rb
腳本更新some/path/my_flutter
的插件列表。然后再在some/path/MyApp
目錄下運行pod install
。
podhelper.rb
腳本將確保你的插件和Flutter.framework被添加到項目中,并禁用所有target的bitcode選項。
為Dart代碼添加構建階段
在MyApp
項目的導航器中最頂層,在主視圖的左側選擇TARGETS列表中的MyApp
,然后選擇Build Phases
選項卡。單擊主視圖左上角的+
添加新構建階段。選擇New Run Script Phase
并展開新添加到構建階段列表的Run Script
。
將以下內容粘貼到Shell字段正下方的文本區域中:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
最后,將新構建階段拖到Target Dependencies phase
下面。
你現在可以能夠使用?B
來構建項目。
以上操作如下圖:
幕后操作
如果你因為某些理由要手動執行此操作或調試這些步驟無法正常工作的原因,請參閱以下內容:
-
Flutter.framework
(引擎庫)已嵌入到你的app中。這必須與發布類型(debug/profile/release)以及app的架構(arm *,i386,x86_64等)相匹配。Cocoapods將其作為一個框架,并確保它嵌入到你的原生app中。 - App.framework (你的Flutter應用程序二進制文件)已嵌入到你的app中。
- flutter_assets 文件夾作為資源嵌入 - 它包含字體,圖像,并且在某些構建模式下,它還包含引擎在運行時所需的二進制文件。 此文件夾如果有問題可能導致運行時錯誤,例如“無法運行引擎進行配置”(Could not run engine for configuration) - 通常表示文件夾未嵌入,或者你嘗試通過啟用AOT的引擎交叉JIT應用程序,反之亦然!
- 任何插件都會被添加為Cocoapods依賴,這么做對插件本身來說更加具體。不過從理論上講,應該可以手動合并它們。
- 對項目中的每個target禁用Bitcode。這是與Flutter引擎鏈接的必要條件。
- Generated.xcconfig(包含特定于Flutter的環境變量)包含在Cocoapods生成的release和debug.xcconfig文件中。
構建階段腳本(xcode_backend.sh)確保你構建的二進制文件與實際位于文件夾中的Dart代碼保持同步。
在宿主app中使用FlutterViewController
你應當根據你的宿主app的實際情況進行操作。下面是一個對Xcode 10.0生成的宿主app的空白屏幕示例(SingleViewApp)。
首先聲明你的app delegate繼承自FlutterAppDelegate
。
在 AppDelegate.h
中
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>
@interface AppDelegate : FlutterAppDelegate
@end
這種情況下AppDelegate.m的內容非常簡單,除非你的宿主App需要覆蓋其他方法:
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins
#include "AppDelegate.h"
@implementation AppDelegate
// This override can be omitted if you do not have any Flutter Plugins.
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self];
return [super application:application didFinishLaunchingWithOptions:launchOptions];
}
@end
如果你使用的是Swift,則可以在以下位置執行以下操作AppDelegate.swift:
import UIKit
import Flutter
import FlutterPluginRegistrant // Only if you have Flutter Plugins.
@UIApplicationMain
class AppDelegate: FlutterAppDelegate {
// Only if you have Flutter plugins.
override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
GeneratedPluginRegistrant.register(with: self);
return super.application(application, didFinishLaunchingWithOptions: launchOptions);
}
}
<
<details>
<summary>如果app delegate已經從其他地方繼承,該如何處理? </summary>
需要為你的app delegate 實現 FlutterAppLifeCycleProvider協議
例如:
#import <Flutter/Flutter.h>
#import <UIKit/UIKit.h>
#import <FlutterPluginRegistrant/GeneratedPluginRegistrant.h> // Only if you have Flutter Plugins
@interface AppDelegate : UIResponder <UIApplicationDelegate, FlutterAppLifeCycleProvider>
@property (strong, nonatomic) UIWindow *window;
@end
具體實現則應該委托給FlutterPluginAppLifeCycleDelegate
:
@implementation AppDelegate
{
FlutterPluginAppLifeCycleDelegate *_lifeCycleDelegate;
}
- (instancetype)init {
if (self = [super init]) {
_lifeCycleDelegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
}
return self;
}
- (BOOL)application:(UIApplication*)application
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
[GeneratedPluginRegistrant registerWithRegistry:self]; // Only if you are using Flutter plugins.
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)applicationDidEnterBackground:(UIApplication*)application {
[_lifeCycleDelegate applicationDidEnterBackground:application];
}
- (void)applicationWillEnterForeground:(UIApplication*)application {
[_lifeCycleDelegate applicationWillEnterForeground:application];
}
- (void)applicationWillResignActive:(UIApplication*)application {
[_lifeCycleDelegate applicationWillResignActive:application];
}
- (void)applicationDidBecomeActive:(UIApplication*)application {
[_lifeCycleDelegate applicationDidBecomeActive:application];
}
- (void)applicationWillTerminate:(UIApplication*)application {
[_lifeCycleDelegate applicationWillTerminate:application];
}
- (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:application
didReceiveRemoteNotification:userInfo
fetchCompletionHandler:completionHandler];
}
- (BOOL)application:(UIApplication*)application
openURL:(NSURL*)url
options:(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*)application
openURL:(NSURL*)url
sourceApplication:(NSString*)sourceApplication
annotation:(id)annotation {
return [_lifeCycleDelegate application:application
openURL:url
sourceApplication:sourceApplication
annotation:annotation];
}
- (void)application:(UIApplication*)application
performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
completionHandler:(void (^)(BOOL succeeded))completionHandler NS_AVAILABLE_IOS(9_0) {
[_lifeCycleDelegate application:application
performActionForShortcutItem:shortcutItem
completionHandler:completionHandler];
}
- (void)application:(UIApplication*)application
handleEventsForBackgroundURLSession:(nonnull NSString*)identifier
completionHandler:(nonnull void (^)(void))completionHandler {
[_lifeCycleDelegate application:application
handleEventsForBackgroundURLSession:identifier
completionHandler:completionHandler];
}
- (void)application:(UIApplication*)application
performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler {
[_lifeCycleDelegate application:application performFetchWithCompletionHandler:completionHandler];
}
- (void)addApplicationLifeCycleDelegate:(NSObject<FlutterPlugin>*)delegate {
[_lifeCycleDelegate addDelegate:delegate];
}
@end
</details>
ViewController.m
:
#import <Flutter/Flutter.h>
#import "ViewController.h"
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button addTarget:self
action:@selector(handleButtonAction)
forControlEvents:UIControlEventTouchUpInside];
[button setTitle:@"Press me" forState:UIControlStateNormal];
[button setBackgroundColor:[UIColor blueColor]];
button.frame = CGRectMake(80.0, 210.0, 160.0, 40.0);
[self.view addSubview:button];
}
- (void)handleButtonAction {
FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
[self presentViewController:flutterViewController animated:false completion:nil];
}
@end
或者,使用Swift:
ViewController.swift
:
import UIKit
import Flutter
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(type:UIButtonType.custom)
button.addTarget(self, action: #selector(handleButtonAction), for: .touchUpInside)
button.setTitle("Press me", for: UIControlState.normal)
button.frame = CGRect(x: 80.0, y: 210.0, width: 160.0, height: 40.0)
button.backgroundColor = UIColor.blue
self.view.addSubview(button)
}
@objc func handleButtonAction() {
let flutterViewController = FlutterViewController()
self.present(flutterViewController, animated: false, completion: nil)
}
}
你現在應該能夠在模擬器或設備上構建和啟動MyApp。按下按鈕會全屏顯示帶有標準Flutter Demo計數應用程序的Flutter視圖。你可以使用路由在應用中的不同位置顯示不同的widgets,如上面的Android部分所述。要設置路由,需要調用:
- Objective-C:
[flutterViewController setInitialRoute:@"route1"];
- Swift:
flutterViewController.setInitialRoute("route1")
一旦你在創建FlutterViewController之后(并在presenting顯示前)。
你可以在Dart代碼中通過調用SystemNavigator.pop()
讓Flutter應用程序消失。
構建和運行應用程序
使用Xcode構建和運行MyApp的方式與添加Flutter模塊依賴項之前完全相同。編輯,調試和分析iOS代碼也是如此。
熱重啟/重新加載和調試Dart代碼
連接設備或啟動模擬器。然后使Flutter CLI工具監聽應用程序:
$ cd some/path/my_flutter
$ flutter attach
Waiting for a connection from Flutter on iPhone X...
從Xcode以調試模式啟動MyApp
。導航到使用Flutter的應用程序區域。然后回到終端,你應該看到類似于以下內容的輸出:
Done.
Syncing files to device iPhone 8... 1.3s
?? To hot reload changes while running, press "r". To hot restart (and rebuild
state), press "R".
An Observatory debugger and profiler on iPhone 8 is available at:
http://127.0.0.1:54467/
For a more detailed help message, press "h". To detach, press "d"; to quit,
press "q".
你現在可以編輯my_flutter
下的Dart代碼,按下r終端可以熱重啟加載更改。也可以將上面的URL粘貼到瀏覽器中,以使用Dart Observatory設置斷點,分析內存保留和其他調試任務。
調試特定的Flutter實例
可以將多個Flutter(root isolates
)實例添加到應用程序中。flutter attach
默認情況下連接到所有可用的isolates。然后,從連接的CLI發送的任何命令都會轉發到每個連接的isolates。
通過flutterCLI工具鍵入l
來列出所有附加的isolates。如果未指定,則會從dart入口點文件和函數名稱自動生成isolates名稱。
l
同時顯示兩個Flutter isolates的應用程序的示例輸出:
Connected views:
main.dart$main-517591213 (isolates/517591213)
main.dart$main-332962855 (isolates/332962855)
通過兩個步驟連接到特定的isolates:
1.在其Dart源文件中命名Flutter的根isolate。
// main.dart
import 'dart:ui' as ui;
void main() {
ui.window.setIsolateDebugName("debug isolate");
// ...
}
2.通過flutter attach
的--isolate-filter
選項運行。
$ flutter attach --isolate-filter='debug'
Waiting for a connection from Flutter...
Done.
Syncing files to device... 1.1s
?? To hot reload changes while running, press "r". To hot restart (and rebuild state), press "R".
An Observatory debugger and profiler is available at: http://127.0.0.1:43343/
For a more detailed help message, press "h". To detach, press "d"; to quit, press "q".
Connected view:
debug isolate (isolates/642101161)