Flutter 在經過一番洗禮後終於迎來了 Release Preview 1版本,也吸引了更多人的關注。使用 Flutter從頭開始寫一個 App非常輕鬆,但越來越多的人發現 Flutter貌似並不很友好地支援現有的 App接入。所以本文帶大家瞭解一下如何讓現有 App支援 Flutter。
環境
Flutter:0.5.1
Xcode 9.4.1
Flutter工程:flutter/examples/hello_world
Debug: Flutter Hot Restart
我們都知道在開發環境下,Flutter的 hot restart 對 UI快速成型是非常有幫助的,要讓現有的 App支援 Flutter並且開啟 Hot restart也不難。
Flutter(engine) 基礎庫
首先,在你的專案裡面拖入 Flutter.framework
,這個庫是 Flutter Engine,承載了 Dart執行時和繪圖引擎。Flutter.framework
和命令列工具版本是一一對應的,如果你不知道從哪裡找這個檔案,可以直接在 Flutter原始碼專案裡面進行一次 flutter run
,然後你就能在 /<project>/ios/Flutter/
目錄下面找到了,直接拖進專案即可。
Flutter ViewController
接下來需要把 Flutter的基礎程式碼引入現有工程,有了基礎的 Flutter ViewController才可以顯示 Flutter檢視。這一步很簡單,只需要在你現有的 ViewController中 Push過去就可以了:
- (void)jumpToFlutter { FlutterViewController *viewController = [FlutterViewController new]; [self.navigationController pushViewController:viewController animated:YES]; }
|
但是還需要注意需要把 AppDelegate裡面的生命週期事件傳遞給 Flutter
Release Preview 1後文件中提到有 FlutterAppLifeCycleProvider
這個協議,但是 0.5.1還沒有,所以這裡先野路子來了
-
直接讓現有的 AppDelegate繼承 FlutterAppDelegate
即可,但這帶來的負面影響是 root ViewController被設定為 Flutter ViewController
-
自行改造 AppDelegate。很多中大型 App喜歡在開發中將業務模組化,幸好 Flutter APPDelegate並不是很難改造過來,也能支援模組 Delegate。首先讓你的 Delegate遵循 FlutterPluginRegistry
協議
@interface DemoFlutterBaseAppDelegate : NSObject <ModularApplicationDelegate, FlutterPluginRegistry>
- (NSObject<FlutterBinaryMessenger> *)binaryMessenger;
- (NSObject<FlutterTextureRegistry> *)textures;
@end
|
|
然後在實現中支援 Flutter messenger 和 texture
@interface DemoFlutterAppDelegateRegistrar : NSObject<FlutterPluginRegistrar>
@property(nonatomic, copy) NSString *pluginKey;
@property(nonatomic, strong) DemoFlutterBaseAppDelegate *appDelegate;
- (instancetype)initWithPlugin:(NSString*)pluginKey appDelegate:(DemoFlutterBaseAppDelegate*)delegate;
@end
@implementation DemoFlutterAppDelegateRegistrar
- (instancetype)initWithPlugin:(NSString*)pluginKey appDelegate:(DemoFlutterBaseAppDelegate*)appDelegate {
self = [super init];
NSAssert(self, @"Super init cannot be nil");
_pluginKey = [pluginKey copy];
_appDelegate = appDelegate;
return self;
}
- (NSObject<FlutterBinaryMessenger>*)messenger {
return [_appDelegate binaryMessenger];
}
- (NSObject<FlutterTextureRegistry>*)textures {
return [_appDelegate textures];
}
- (void)publish:(NSObject*)value {
_appDelegate.pluginPublications[_pluginKey] = value;
}
- (void)addMethodCallDelegate:(NSObject<FlutterPlugin>*)delegate
channel:(FlutterMethodChannel*)channel {
[channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
[delegate handleMethodCall:call result:result];
}];
}
- (void)addApplicationDelegate:(NSObject<FlutterPlugin>*)delegate {
[_appDelegate.pluginDelegates addObject:delegate];
}
- (NSString*)lookupKeyForAsset:(NSString*)asset {
return [FlutterDartProject lookupKeyForAsset:asset];
}
- (NSString*)lookupKeyForAsset:(NSString*)asset fromPackage:(NSString*)package {
return [FlutterDartProject lookupKeyForAsset:asset fromPackage:package];
}
@end
@interface DemoFlutterBaseAppDelegate ()
@property(nonatomic, strong) DemoFlutterBaseViewController *rootController;
@property(readonly, nonatomic) NSMutableArray* pluginDelegates;
@property(readonly, nonatomic) NSMutableDictionary* pluginPublications;
@end
@implementation DemoFlutterBaseAppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
for (id<FlutterPlugin> plugin in _pluginDelegates) {
if ([plugin respondsToSelector:_cmd]) {
[plugin application:application didFinishLaunchingWithOptions:launchOptions];
}
}
return YES;
}
#pragma mark - getters for flutter
- (FlutterViewController *)rootController {
}
- (NSObject<FlutterBinaryMessenger> *)binaryMessenger
{
if ([self.rootController conformsToProtocol:@protocol(FlutterBinaryMessenger)]) {
return (NSObject<FlutterBinaryMessenger> *)self.rootController;
}
return nil;
}
- (NSObject<FlutterTextureRegistry> *)textures
{
if ([self.rootController conformsToProtocol:@protocol(FlutterTextureRegistry)]) {
return (NSObject<FlutterTextureRegistry> *)self.rootController;
}
return nil;
}
- (NSObject<FlutterPluginRegistrar>*)registrarForPlugin:(NSString*)pluginKey {
NSAssert(self.pluginPublications[pluginKey] == nil, @"Duplicate plugin key: %@", pluginKey);
self.pluginPublications[pluginKey] = [NSNull null];
return [[DemoFlutterAppDelegateRegistrar alloc] initWithPlugin:pluginKey appDelegate:self];
}
- (BOOL)hasPlugin:(NSString*)pluginKey {
return _pluginPublications[pluginKey] != nil;
}
- (NSObject*)valuePublishedByPlugin:(NSString*)pluginKey {
return _pluginPublications[pluginKey];
}
@end
這樣 Flutter的執行環境其實就準備好了,無論是 Hot Restart還是 AOT都可以支援。接下來我們實現 Debug Hot Restart
首先在你的 Flutter程式碼目錄下執行一遍 Flutter build bundle
,這可以幫助我們打包出一個 Flutter Asset,然後把這個 flutter_assets
目錄拖入專案。
對你的專案進行一次 build,確保能夠得到一個 .app 檔案。然後新建一個資料夾叫做 Payload
,把 .app檔案放入 Payload資料夾,然後壓縮成 zip檔案。這個檔案便可以被 Flutter命令列工具使用了。
flutter run --use-application-binary /path/to/Payload.zip
|
然後效果如圖:
其實這裡並沒有實現 Hot Reload,主要是目前 flutter工具鏈支援度還不好,不過 Hot Restart也夠用了。
Release: Flutter AOT
Release模式和 Debug下不一樣,我們需要做幾件事情:
- 把
Flutter.framework
替換成 flutter/bin/cache/artifacts/engine/ios-release/Flutter.framework
,因為上一步我們用的庫其實是 JIT Runtime
- 在 Flutter程式碼專案下面執行
flutter build aot --release --target-platform ios --ios-arch armv7,arm64
然後我們可以在 build目錄下拿到一個打包好的 App.framework
,不過別忘記在裡面放一個 Info.plist。並且把這個庫拖到工程裡面
- 刪除工程裡面的
flutter_assets
資料夾下的 isolate_snapshot_data
、kernel_blob.bin
、platform.dill
、vm_snapshot_data
這幾個檔案
- 編譯打包給真機執行,效果如下:
The End
其實 Flutter官方有支援現有 App 整合的計劃,並且現在文件也有一部分介紹,但其實整體工具鏈還沒支援上來,目前所支援的程度和上文的方法也大同小異。
如果有需要的話,現有專案完全可以採用上面的方法整合,為了減少工作流程,還需要做一些工作,比如:
- 專案中提供 App.framework 、 Flutter.framework的空殼,方便在 debug 和 release下隨時用指令碼替換
- Debug時可以先打出包給 Flutter開發者用,也可以直接新增一個 build post action,直接呼叫 flutter 命令列,把 Xcode和 flutter整合起來,不要像上文一樣全都手動,容易漏掉必要流程
- Release AOT的自動化肯定要做,並且要和現有 CI整合