在上一篇文章中,我與你介紹了方法通道,這種在 Flutter 中實現調用原生 Android、iOS 代碼的輕量級解決方案。使用方法通道,我們可以把原生代碼所擁有的能力,以接口形式提供給 Dart。
這樣,當發起方法調用時,Flutter 應用會以類似網絡異步調用的方式,將請求數據通過一個唯一標識符指定的方法通道傳輸至原生代碼宿主;而原生代碼處理完畢后,會將響應結果通過方法通道回傳至 Flutter,從而實現 Dart 代碼與原生 Android、iOS 代碼的交互。這,與調用一個本地的 Dart 異步 API 并無太多區別。
通過方法通道,我們可以把原生操作系統提供的底層能力,以及現有原生開發中一些相對成熟的解決方案,以接口封裝的形式在 Dart 層快速搞定,從而解決原生代碼在 Flutter 上的復用問題。然后,我們可以利用 Flutter 本身提供的豐富控件,做好 UI 渲染。
底層能力 + 應用層渲染,看似我們已經搞定了搭建一個復雜 App 的所有內容。但,真的是這樣嗎?
## 構建一個復雜 App 都需要什么?
別急,在下結論之前,我們先按照四象限分析法,把能力和渲染分解成四個維度,分析構建一個復雜 App 都需要什么。
:-: 
圖 1 四象限分析法
經過分析,我們終于發現,原來構建一個 App 需要覆蓋那么多的知識點,通過 Flutter 和方法通道只能搞定應用層渲染、應用層能力和底層能力,對于那些涉及到底層渲染,比如瀏覽器、相機、地圖,以及原生自定義視圖的場景,自己在 Flutter 上重新開發一套顯然不太現實。
在這種情況下,使用混合視圖看起來是一個不錯的選擇。我們可以在 Flutter 的 Widget 樹中提前預留一塊空白區域,在 Flutter 的畫板中(即 FlutterView 與 FlutterViewController)嵌入一個與空白區域完全匹配的原生視圖,就可以實現想要的視覺效果了。
但是,采用這種方案極其不優雅,因為嵌入的原生視圖并不在 Flutter 的渲染層級中,需要同時在 Flutter 側與原生側做大量的適配工作,才能實現正常的用戶交互體驗。
幸運的是,Flutter 提供了一個平臺視圖(Platform View)的概念。它提供了一種方法,允許開發者在 Flutter 里面嵌入原生系統(Android 和 iOS)的視圖,并加入到 Flutter 的渲染樹中,實現與 Flutter 一致的交互體驗。
這樣一來,通過平臺視圖,我們就可以將一個原生控件包裝成 Flutter 控件,嵌入到 Flutter 頁面中,就像使用一個普通的 Widget 一樣。
接下來,我就與你詳細講述如何使用平臺視圖。
## 平臺視圖
如果說方法通道解決的是原生能力邏輯復用問題,那么平臺視圖解決的就是原生視圖復用問題。Flutter 提供了一種輕量級的方法,讓我們可以創建原生(Android 和 iOS)的視圖,通過一些簡單的 Dart 層接口封裝之后,就可以將它插入 Widget 樹中,實現原生視圖與 Flutter 視圖的混用。
一次典型的平臺視圖使用過程與方法通道類似:
* 首先,由作為客戶端的 Flutter,通過向原生視圖的 Flutter 封裝類(在 iOS 和 Android 平臺分別是 UIKitView 和 AndroidView)傳入視圖標識符,用于發起原生視圖的創建請求;
* 然后,原生代碼側將對應原生視圖的創建交給平臺視圖工廠(PlatformViewFactory)實現;
* 最后,在原生代碼側將視圖標識符與平臺視圖工廠進行關聯注冊,讓 Flutter 發起的視圖創建請求可以直接找到對應的視圖創建工廠。
至此,我們就可以像使用 Widget 那樣,使用原生視圖了。整個流程,如下圖所示:
:-: 
圖 2 平臺視圖示例
接下來,我以一個具體的案例,也就是將一個紅色的原生視圖內嵌到 Flutter 中,與你演示如何使用平臺視圖。這部分內容主要包括兩部分:
* 作為調用發起方的 Flutter,如何實現原生視圖的接口調用?
* 如何在原生(Android 和 iOS)系統實現接口?
接下來,我將分別與你講述這兩個問題。
### Flutter 如何實現原生視圖的接口調用?
在下面的代碼中,我們在 SampleView 的內部,分別使用了原生 Android、iOS 視圖的封裝類 AndroidView 和 UIkitView,并傳入了一個唯一標識符,用于和原生視圖建立關聯:
~~~
class SampleView extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 使用 Android 平臺的 AndroidView,傳入唯一標識符 sampleView
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(viewType: 'sampleView');
} else {
// 使用 iOS 平臺的 UIKitView,傳入唯一標識符 sampleView
return UiKitView(viewType: 'sampleView');
}
}
}
~~~
可以看到,平臺視圖在 Flutter 側的使用方式比較簡單,與普通 Widget 并無明顯區別。而關于普通 Widget 的使用方式,你可以參考第[12](https://time.geekbang.org/column/article/110292)、[13](https://time.geekbang.org/column/article/110859)篇的相關內容進行復習。
調用方的實現搞定了。接下來,我們需要在原生代碼中完成視圖創建的封裝,建立相關的綁定關系。同樣的,由于需要同時適配 Android 和 iOS 平臺,我們需要分別在兩個系統上完成對應的接口實現。
### 如何在原生系統實現接口?
首先,我們來看看**Android 端的實現**。在下面的代碼中,我們分別創建了平臺視圖工廠和原生視圖封裝類,并通過視圖工廠的 create 方法,將它們關聯起來:
~~~
// 視圖工廠類
class SampleViewFactory extends PlatformViewFactory {
private final BinaryMessenger messenger;
// 初始化方法
public SampleViewFactory(BinaryMessenger msger) {
super(StandardMessageCodec.INSTANCE);
messenger = msger;
}
// 創建原生視圖封裝類,完成關聯
@Override
public PlatformView create(Context context, int id, Object obj) {
return new SimpleViewControl(context, id, messenger);
}
}
// 原生視圖封裝類
class SimpleViewControl implements PlatformView {
private final View view;// 緩存原生視圖
// 初始化方法,提前創建好視圖
public SimpleViewControl(Context context, int id, BinaryMessenger messenger) {
view = new View(context);
view.setBackgroundColor(Color.rgb(255, 0, 0));
}
// 返回原生視圖
@Override
public View getView() {
return view;
}
// 原生視圖銷毀回調
@Override
public void dispose() {
}
}
~~~
將原生視圖封裝類與原生視圖工廠完成關聯后,接下來就需要將 Flutter 側的調用與視圖工廠綁定起來了。與上一篇文章講述的方法通道類似,我們仍然需要在 MainActivity 中進行綁定操作:
~~~
protected void onCreate(Bundle savedInstanceState) {
...
Registrar registrar = registrarFor("samples.chenhang/native_views");// 生成注冊類
SampleViewFactory playerViewFactory = new SampleViewFactory(registrar.messenger());// 生成視圖工廠
registrar.platformViewRegistry().registerViewFactory("sampleView", playerViewFactory);// 注冊視圖工廠
}
~~~
完成綁定之后,平臺視圖調用響應的 Android 部分就搞定了。
接下來,我們再來看看**iOS 端的實現**。
與 Android 類似,我們同樣需要分別創建平臺視圖工廠和原生視圖封裝類,并通過視圖工廠的 create 方法,將它們關聯起來:
~~~
// 平臺視圖工廠
@interface SampleViewFactory : NSObject<FlutterPlatformViewFactory>
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger>*)messager;
@end
@implementation SampleViewFactory{
NSObject<FlutterBinaryMessenger>*_messenger;
}
- (instancetype)initWithMessenger:(NSObject<FlutterBinaryMessenger> *)messager{
self = [super init];
if (self) {
_messenger = messager;
}
return self;
}
-(NSObject<FlutterMessageCodec> *)createArgsCodec{
return [FlutterStandardMessageCodec sharedInstance];
}
// 創建原生視圖封裝實例
-(NSObject<FlutterPlatformView> *)createWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args{
SampleViewControl *activity = [[SampleViewControl alloc] initWithWithFrame:frame viewIdentifier:viewId arguments:args binaryMessenger:_messenger];
return activity;
}
@end
// 平臺視圖封裝類
@interface SampleViewControl : NSObject<FlutterPlatformView>
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id _Nullable)args binaryMessenger:(NSObject<FlutterBinaryMessenger>*)messenger;
@end
@implementation SampleViewControl{
UIView * _templcateView;
}
// 創建原生視圖
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger{
if ([super init]) {
_templcateView = [[UIView alloc] init];
_templcateView.backgroundColor = [UIColor redColor];
}
return self;
}
-(UIView *)view{
return _templcateView;
}
@end
~~~
然后,我們同樣需要把原生視圖的創建與 Flutter 側的調用關聯起來,才可以在 Flutter 側找到原生視圖的實現:
~~~
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSObject<FlutterPluginRegistrar>* registrar = [self registrarForPlugin:@"samples.chenhang/native_views"];// 生成注冊類
SampleViewFactory* viewFactory = [[SampleViewFactory alloc] initWithMessenger:registrar.messenger];// 生成視圖工廠
[registrar registerViewFactory:viewFactory withId:@"sampleView"];// 注冊視圖工廠
...
}
~~~
需要注意的是,在 iOS 平臺上,Flutter 內嵌 UIKitView 目前還處于技術預覽狀態,因此我們還需要在 Info.plist 文件中增加一項配置,把內嵌原生視圖的功能開關設置為 true,才能打開這個隱藏功能:
~~~
<dict>
...
<key>io.flutter.embedded_views_preview</key>
<true/>
....
</dict>
~~~
經過上面的封裝與綁定,Android 端與 iOS 端的平臺視圖功能都已經實現了。接下來,我們就可以在 Flutter 應用里,像使用普通 Widget 一樣,去內嵌原生視圖了:
~~~
Scaffold(
backgroundColor: Colors.yellowAccent,
body: Container(width: 200, height:200,
child: SampleView(controller: controller)
));
~~~
如下所示,我們分別在 iOS 和 Android 平臺的 Flutter 應用上,內嵌了一個紅色的原生視圖:
:-: 
圖 3 內嵌原生視圖示例
在上面的例子中,我們將原生視圖封裝在一個 StatelessWidget 中,可以有效應對靜態展示的場景。如果我們需要在程序運行時動態調整原生視圖的樣式,又該如何處理呢?
## 如何在程序運行時,動態地調整原生視圖的樣式?
與基于聲明式的 Flutter Widget,每次變化只能以數據驅動其視圖銷毀重建不同,原生視圖是基于命令式的,可以精確地控制視圖展示樣式。因此,我們可以在原生視圖的封裝類中,將其持有的修改視圖實例相關的接口,以方法通道的方式暴露給 Flutter,讓 Flutter 也可以擁有動態調整視圖視覺樣式的能力。
接下來,我以一個具體的案例來演示如何在程序運行時動態調整內嵌原生視圖的背景顏色。
在這個案例中,我們會用到原生視圖的一個初始化屬性,即 onPlatformViewCreated:原生視圖會在其創建完成后,以回調的形式通知視圖 id,因此我們可以在這個時候注冊方法通道,讓后續的視圖修改請求通過這條通道傳遞給原生視圖。
由于我們在底層直接持有了原生視圖的實例,因此理論上可以直接在這個原生視圖的 Flutter 封裝類上提供視圖修改方法,而不管它到底是 StatelessWidget 還是 StatefulWidget。但為了遵照 Flutter 的 Widget 設計理念,我們還是決定將視圖展示與視圖控制分離,即:將原生視圖封裝為一個 StatefulWidget 專門用于展示,通過其 controller 初始化參數,在運行期修改原生視圖的展示效果。如下所示:
~~~
// 原生視圖控制器
class NativeViewController {
MethodChannel _channel;
// 原生視圖完成創建后,通過 id 生成唯一方法通道
onCreate(int id) {
_channel = MethodChannel('samples.chenhang/native_views_$id');
}
// 調用原生視圖方法,改變背景顏色
Future<void> changeBackgroundColor() async {
return _channel.invokeMethod('changeBackgroundColor');
}
}
// 原生視圖 Flutter 側封裝,繼承自 StatefulWidget
class SampleView extends StatefulWidget {
const SampleView({
Key key,
this.controller,
}) : super(key: key);
// 持有視圖控制器
final NativeViewController controller;
@override
State<StatefulWidget> createState() => _SampleViewState();
}
class _SampleViewState extends State<SampleView> {
// 根據平臺確定返回何種平臺視圖
@override
Widget build(BuildContext context) {
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'sampleView',
// 原生視圖創建完成后,通過 onPlatformViewCreated 產生回調
onPlatformViewCreated: _onPlatformViewCreated,
);
} else {
return UiKitView(viewType: 'sampleView',
// 原生視圖創建完成后,通過 onPlatformViewCreated 產生回調
onPlatformViewCreated: _onPlatformViewCreated
);
}
}
// 原生視圖創建完成后,調用 control 的 onCreate 方法,傳入 view id
_onPlatformViewCreated(int id) {
if (widget.controller == null) {
return;
}
widget.controller.onCreate(id);
}
}
~~~
Flutter 的調用方實現搞定了,接下來我們分別看看 Android 和 iOS 端的實現。
程序的整體結構與之前并無不同,只是在進行原生視圖初始化時,我們需要完成方法通道的注冊和相關事件的處理;在響應方法調用消息時,我們需要判斷方法名,如果完全匹配,則修改視圖背景,否則返回異常。
Android 端接口實現代碼如下所示:
~~~
class SimpleViewControl implements PlatformView, MethodCallHandler {
private final MethodChannel methodChannel;
...
public SimpleViewControl(Context context, int id, BinaryMessenger messenger) {
...
// 用 view id 注冊方法通道
methodChannel = new MethodChannel(messenger, "samples.chenhang/native_views_" + id);
// 設置方法通道回調
methodChannel.setMethodCallHandler(this);
}
// 處理方法調用消息
@Override
public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
// 如果方法名完全匹配
if (methodCall.method.equals("changeBackgroundColor")) {
// 修改視圖背景,返回成功
view.setBackgroundColor(Color.rgb(0, 0, 255));
result.success(0);
}else {
// 調用方發起了一個不支持的 API 調用
result.notImplemented();
}
}
...
}
~~~
iOS 端接口實現代碼:
~~~
@implementation SampleViewControl{
...
FlutterMethodChannel* _channel;
}
- (instancetype)initWithWithFrame:(CGRect)frame viewIdentifier:(int64_t)viewId arguments:(id)args binaryMessenger:(NSObject<FlutterBinaryMessenger> *)messenger{
if ([super init]) {
...
// 使用 view id 完成方法通道的創建
_channel = [FlutterMethodChannel methodChannelWithName:[NSString stringWithFormat:@"samples.chenhang/native_views_%lld", viewId] binaryMessenger:messenger];
// 設置方法通道的處理回調
__weak __typeof__(self) weakSelf = self;
[_channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
[weakSelf onMethodCall:call result:result];
}];
}
return self;
}
// 響應方法調用消息
- (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
// 如果方法名完全匹配
if ([[call method] isEqualToString:@"changeBackgroundColor"]) {
// 修改視圖背景色,返回成功
_templcateView.backgroundColor = [UIColor blueColor];
result(@0);
} else {
// 調用方發起了一個不支持的 API 調用
result(FlutterMethodNotImplemented);
}
}
...
@end
~~~
通過注冊方法通道,以及暴露的 changeBackgroundColor 接口,Android 端與 iOS 端修改平臺視圖背景顏色的功能都已經實現了。接下來,我們就可以在 Flutter 應用運行期間,修改原生視圖展示樣式了:
~~~
class DefaultState extends State<DefaultPage> {
NativeViewController controller;
@override
void initState() {
controller = NativeViewController();// 初始化原生 View 控制器
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
...
// 內嵌原生 View
body: Container(width: 200, height:200,
child: SampleView(controller: controller)
),
// 設置點擊行為:改變視圖顏色
floatingActionButton: FloatingActionButton(onPressed: ()=>controller.changeBackgroundColor())
);
}
}
~~~
運行一下,效果如下所示:
:-: 
圖 4 動態修改原生視圖樣式
## 總結
好了,今天的分享就到這里。我們總結一下今天的主要內容吧。
平臺視圖解決了原生渲染能力的復用問題,使得 Flutter 能夠通過輕量級的代碼封裝,把原生視圖組裝成一個 Flutter 控件。
Flutter 提供了平臺視圖工廠和視圖標識符兩個概念,因此 Dart 層發起的視圖創建請求可以通過標識符直接找到對應的視圖創建工廠,從而實現原生視圖與 Flutter 視圖的融合復用。對于需要在運行期動態調用原生視圖接口的需求,我們可以在原生視圖的封裝類中注冊方法通道,實現精確控制原生視圖展示的效果。
需要注意的是,由于 Flutter 與原生渲染方式完全不同,因此轉換不同的渲染數據會有較大的性能開銷。如果在一個界面上同時實例化多個原生控件,就會對性能造成非常大的影響,所以我們要避免在使用 Flutter 控件也能實現的情況下去使用內嵌平臺視圖。
因為這樣做,一方面需要分別在 Android 和 iOS 端寫大量的適配橋接代碼,違背了跨平臺技術的本意,也增加了后續的維護成本;另一方面畢竟除去地圖、WebView、相機等涉及底層方案的特殊情況外,大部分原生代碼能夠實現的 UI 效果,完全可以用 Flutter 實現。
我把今天分享所涉及到的知識點打包到了[GitHub](https://github.com/cyndibaby905/27_native_view)中,你可以下載下來,反復運行幾次,加深理解。
## 思考題
最后,我給你留下一道思考題吧。
請你在動態調整原生視圖樣式的代碼基礎上,增加顏色參數,以實現動態變更原生視圖顏色的需求。
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略