本文翻譯自 Add-Flutter-to-existing-apps
在已有項(xiàng)目中繼承Flutter
約定:
本文中hostApp翻譯成: 宿主App, 主App,主應(yīng)用程序,在示例中可以理解為MyApp
介紹
正在進(jìn)行的一項(xiàng)為了更加容易地將Flutter添加到現(xiàn)有應(yīng)用程序中的工作。可以在Add2App project查看進(jìn)度。
此頁面記錄了該工作的當(dāng)前狀態(tài),并將在我們構(gòu)建必要的工具時(shí)進(jìn)行更新。
最后更新時(shí)間為2018年11月26日。
“add2app”支持處于預(yù)覽狀態(tài),目前僅在master分支上可用。
免責(zé)
由于Flutter的“Add2App”功能處于預(yù)覽狀態(tài),因此相關(guān)的API和工具不穩(wěn)定且可能會(huì)發(fā)生變化。
Flutter module項(xiàng)目模板
使用flutter create xxx
創(chuàng)建的含有Flutter/Dart代碼的Flutter項(xiàng)目包含非常簡單的原生應(yīng)用程序(單一Activity的Android host和單一ViewController的iOS host)。你可以修改這些主應(yīng)用程序以滿足你的需求并構(gòu)建它們。
但是,如果你開始使用的是某一平臺(tái)的現(xiàn)有原生程序,你可能希望將Flutter項(xiàng)目作為某種形式的庫包含在該應(yīng)用程序中。
這就是Flutter module模板提供的內(nèi)容。執(zhí)行 flutter create -t module xxx
會(huì)生成一個(gè)Flutter項(xiàng)目,其中包含專為現(xiàn)有原生應(yīng)用程序使用而設(shè)計(jì)的一個(gè)Android庫和一個(gè)Cocoapods pod。
Android部分
創(chuàng)建Flutter module
假設(shè)你已有的一個(gè)Android應(yīng)用程序some/path/MyApp
,并且希望將Flutter項(xiàng)目放在該同級(jí)目錄下:
$ cd some/path/
$ flutter create -t module my_flutter
這將在some/path/my_flutter/Flutte
創(chuàng)建一個(gè)Flutter module,其中包含一個(gè)lib/main.dart
文件作為入口,以及一個(gè).android/
的隱藏的子文件夾,它包含了Android庫中的模塊項(xiàng)目。
(雖然以下不需要,但如果您愿意,可以使用Gradle構(gòu)建該庫:
$ cd .android/
$ ./gradlew flutter:assembleDebug
這會(huì)在.android/Flutter/build/outputs/aar/
下生成一個(gè)flutter-debug.aar歸檔文件)
為宿主App添加Flutter module的依賴
將Flutter module作為子項(xiàng)目包含在主應(yīng)用程序的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
等)
在你的應(yīng)用程序中實(shí)現(xiàn)引入對(duì)Flutter module的依賴:
// MyApp/app/build.gradle
:
dependencies {
implementation project(':flutter')
:
}
在Java代碼中的Flutter module
使用Flutter module的Java API將Flutter視圖添加到主應(yīng)用程序。可以通過直接使用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);
}
});
也可以創(chuàng)建一個(gè)自己處理生命周期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視圖中顯示哪個(gè)widget。Flutter module項(xiàng)目模板中的lib/main.dart
文件應(yīng)該switch
(或以其他方式解釋)提供的路由字符串,也可使用window.defaultRouteName
,來確定要?jiǎng)?chuàng)建和傳遞到哪個(gè)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),
);
}
}
完全取決于你想要的路由字符串以及如何解釋它們。
構(gòu)建和運(yùn)行應(yīng)用程序
構(gòu)建和運(yùn)行MyApp的方式與添加Flutter module依賴項(xiàng)之前的方式完全相同,通常使用Android Studio。編輯,調(diào)試和分析Android代碼也是如此。
熱重啟/重新加載和調(diào)試Dart代碼
完整的IDE集成以支持使用混合應(yīng)用程序的Flutter / Dart代碼正在進(jìn)行中。但目前已經(jīng)提供了Flutter命令行工具和Dart Observatory Web用戶界面。
連接設(shè)備或啟動(dòng)模擬器。然后使Flutter CLI工具監(jiān)聽?wèi)?yīng)用程序:
$ cd some/path/my_flutter
$ flutter attach
Waiting for a connection from Flutter on Nexus 5X...
從Android Studio以調(diào)試模式啟動(dòng)MyApp
。導(dǎo)航到使用Flutter的應(yīng)用程序區(qū)域。然后回到終端,應(yīng)該看到類似于以下內(nèi)容的輸出:
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".
你現(xiàn)在可以編輯my_flutter
的Dart代碼,按下r
終端可以重新加載更改。還可以將上面的URL粘貼到瀏覽器中,以使用Dart Observatory設(shè)置斷點(diǎn),分析內(nèi)存和其他調(diào)試任務(wù)。
iOS部分
創(chuàng)建Flutter工程模塊
假設(shè)已有iOS項(xiàng)目在some/path/MyApp
路徑上,我們?cè)谠撏?jí)目錄上創(chuàng)建一個(gè)Flutter工程模塊:
$ cd some/path/
$ flutter create -t module my_flutter
這將會(huì)在some/path/my_flutter/
目錄下創(chuàng)建一個(gè)以lib/main.dart
為入口的Flutter模塊項(xiàng)目。其中隱藏的.ios/
子目錄中包含了一些Cocopods和幫助類的Ruby腳本。
為宿主App添加Flutter依賴模塊
下面的描述是假設(shè)你現(xiàn)有的iOS應(yīng)用程序是通過使用Xcode 10.0且使用Objective-C來創(chuàng)建"Single View App"
的項(xiàng)目。如果你現(xiàn)有應(yīng)用程序具有不同的文件夾結(jié)構(gòu)和/或/
.xcconfig
文件,則可以重復(fù)使用這些文件,但可能需要相應(yīng)地調(diào)整下面提到的一些相對(duì)路徑。
假定的文件夾結(jié)構(gòu)如下:
.
├── 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依賴項(xiàng)管理器。這是因?yàn)镕lutter框架也需要可用于你可能包含在my_flutter中的任何Flutter插件。
如果需要,請(qǐng)參考cocoapods.org了解如何在開發(fā)設(shè)備上安裝CocoaPods。
如果你的主應(yīng)用程序(MyApp
)已在使用Cocoapods,只需執(zhí)行以下操作即可與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)
注意:根據(jù)示例的項(xiàng)目結(jié)構(gòu)這里
path/to/flutter_app/
需要替換為../my_flutter/
2.執(zhí)行pod install
每當(dāng)你在some/path/my_flutter/pubspec.yaml
中更改了Flutter插件的依賴關(guān)系后,你都需要運(yùn)行flutter packages get
來通過podhelper.rb
腳本更新some/path/my_flutter
的插件列表。然后再在some/path/MyApp
目錄下運(yùn)行pod install
。
podhelper.rb
腳本將確保你的插件和Flutter.framework被添加到項(xiàng)目中,并禁用所有target的bitcode選項(xiàng)。
為Dart代碼添加構(gòu)建階段
在MyApp
項(xiàng)目的導(dǎo)航器中最頂層,在主視圖的左側(cè)選擇TARGETS列表中的MyApp
,然后選擇Build Phases
選項(xiàng)卡。單擊主視圖左上角的+
添加新構(gòu)建階段。選擇New Run Script Phase
并展開新添加到構(gòu)建階段列表的Run Script
。
將以下內(nèi)容粘貼到Shell字段正下方的文本區(qū)域中:
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" embed
最后,將新構(gòu)建階段拖到Target Dependencies phase
下面。
你現(xiàn)在可以能夠使用?B
來構(gòu)建項(xiàng)目。
以上操作如下圖:
幕后操作
如果你因?yàn)槟承├碛梢謩?dòng)執(zhí)行此操作或調(diào)試這些步驟無法正常工作的原因,請(qǐng)參閱以下內(nèi)容:
-
Flutter.framework
(引擎庫)已嵌入到你的app中。這必須與發(fā)布類型(debug/profile/release)以及app的架構(gòu)(arm *,i386,x86_64等)相匹配。Cocoapods將其作為一個(gè)框架,并確保它嵌入到你的原生app中。 - App.framework (你的Flutter應(yīng)用程序二進(jìn)制文件)已嵌入到你的app中。
- flutter_assets 文件夾作為資源嵌入 - 它包含字體,圖像,并且在某些構(gòu)建模式下,它還包含引擎在運(yùn)行時(shí)所需的二進(jìn)制文件。 此文件夾如果有問題可能導(dǎo)致運(yùn)行時(shí)錯(cuò)誤,例如“無法運(yùn)行引擎進(jìn)行配置”(Could not run engine for configuration) - 通常表示文件夾未嵌入,或者你嘗試通過啟用AOT的引擎交叉JIT應(yīng)用程序,反之亦然!
- 任何插件都會(huì)被添加為Cocoapods依賴,這么做對(duì)插件本身來說更加具體。不過從理論上講,應(yīng)該可以手動(dòng)合并它們。
- 對(duì)項(xiàng)目中的每個(gè)target禁用Bitcode。這是與Flutter引擎鏈接的必要條件。
- Generated.xcconfig(包含特定于Flutter的環(huán)境變量)包含在Cocoapods生成的release和debug.xcconfig文件中。
構(gòu)建階段腳本(xcode_backend.sh)確保你構(gòu)建的二進(jìn)制文件與實(shí)際位于文件夾中的Dart代碼保持同步。
在宿主app中使用FlutterViewController
你應(yīng)當(dāng)根據(jù)你的宿主app的實(shí)際情況進(jìn)行操作。下面是一個(gè)對(duì)Xcode 10.0生成的宿主app的空白屏幕示例(SingleViewApp)。
首先聲明你的app delegate繼承自FlutterAppDelegate
。
在 AppDelegate.h
中
#import <UIKit/UIKit.h>
#import <Flutter/Flutter.h>
@interface AppDelegate : FlutterAppDelegate
@end
這種情況下AppDelegate.m的內(nèi)容非常簡單,除非你的宿主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,則可以在以下位置執(zhí)行以下操作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已經(jīng)從其他地方繼承,該如何處理? </summary>
需要為你的app delegate 實(shí)現(xiàn) FlutterAppLifeCycleProvider協(xié)議
例如:
#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
具體實(shí)現(xiàn)則應(yīng)該委托給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)
}
}
你現(xiàn)在應(yīng)該能夠在模擬器或設(shè)備上構(gòu)建和啟動(dòng)MyApp。按下按鈕會(huì)全屏顯示帶有標(biāo)準(zhǔn)Flutter Demo計(jì)數(shù)應(yīng)用程序的Flutter視圖。你可以使用路由在應(yīng)用中的不同位置顯示不同的widgets,如上面的Android部分所述。要設(shè)置路由,需要調(diào)用:
- Objective-C:
[flutterViewController setInitialRoute:@"route1"];
- Swift:
flutterViewController.setInitialRoute("route1")
一旦你在創(chuàng)建FlutterViewController之后(并在presenting顯示前)。
你可以在Dart代碼中通過調(diào)用SystemNavigator.pop()
讓Flutter應(yīng)用程序消失。
構(gòu)建和運(yùn)行應(yīng)用程序
使用Xcode構(gòu)建和運(yùn)行MyApp的方式與添加Flutter模塊依賴項(xiàng)之前完全相同。編輯,調(diào)試和分析iOS代碼也是如此。
熱重啟/重新加載和調(diào)試Dart代碼
連接設(shè)備或啟動(dòng)模擬器。然后使Flutter CLI工具監(jiān)聽?wèi)?yīng)用程序:
$ cd some/path/my_flutter
$ flutter attach
Waiting for a connection from Flutter on iPhone X...
從Xcode以調(diào)試模式啟動(dòng)MyApp
。導(dǎo)航到使用Flutter的應(yīng)用程序區(qū)域。然后回到終端,你應(yīng)該看到類似于以下內(nèi)容的輸出:
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".
你現(xiàn)在可以編輯my_flutter
下的Dart代碼,按下r終端可以熱重啟加載更改。也可以將上面的URL粘貼到瀏覽器中,以使用Dart Observatory設(shè)置斷點(diǎn),分析內(nèi)存保留和其他調(diào)試任務(wù)。
調(diào)試特定的Flutter實(shí)例
可以將多個(gè)Flutter(root isolates
)實(shí)例添加到應(yīng)用程序中。flutter attach
默認(rèn)情況下連接到所有可用的isolates。然后,從連接的CLI發(fā)送的任何命令都會(huì)轉(zhuǎn)發(fā)到每個(gè)連接的isolates。
通過flutterCLI工具鍵入l
來列出所有附加的isolates。如果未指定,則會(huì)從dart入口點(diǎn)文件和函數(shù)名稱自動(dòng)生成isolates名稱。
l
同時(shí)顯示兩個(gè)Flutter isolates的應(yīng)用程序的示例輸出:
Connected views:
main.dart$main-517591213 (isolates/517591213)
main.dart$main-332962855 (isolates/332962855)
通過兩個(gè)步驟連接到特定的isolates:
1.在其Dart源文件中命名Flutter的根isolate。
// main.dart
import 'dart:ui' as ui;
void main() {
ui.window.setIsolateDebugName("debug isolate");
// ...
}
2.通過flutter attach
的--isolate-filter
選項(xiàng)運(yùn)行。
$ 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)