在上一篇文章中,我與你介紹了在 Flutter 中實現數據持久化的三種方式,即文件、SharedPreferences 與數據庫。
其中,文件適用于字符串或者二進制流的數據持久化,我們可以根據訪問頻次,決定將它存在臨時目錄或是文檔目錄。而 SharedPreferences 則適用于存儲小型鍵值對信息,可以應對一些輕量配置緩存的場景。數據庫則適用于頻繁變化的、結構化的對象存取,可以輕松應對數據的增刪改查。
依托于與 Skia 的深度定制及優化,Flutter 給我們提供了很多關于渲染的控制和支持,能夠實現絕對的跨平臺應用層渲染一致性。但對于一個應用而言,除了應用層視覺顯示和對應的交互邏輯處理之外,有時還需要原生操作系統(Android、iOS)提供的底層能力支持。比如,我們前面提到的數據持久化,以及推送、攝像頭硬件調用等。
由于 Flutter 只接管了應用渲染層,因此這些系統底層能力是無法在 Flutter 框架內提供支持的;而另一方面,Flutter 還是一個相對年輕的生態,因此原生開發中一些相對成熟的 Java、C++ 或 Objective-C 代碼庫,比如圖片處理、音視頻編解碼等,可能在 Flutter 中還沒有相關實現。
因此,為了解決調用原生系統底層能力以及相關代碼庫復用問題,Flutter 為開發者提供了一個輕量級的解決方案,即邏輯層的方法通道(Method Channel)機制。基于方法通道,我們可以將原生代碼所擁有的能力,以接口形式暴露給 Dart,從而實現 Dart 代碼與原生代碼的交互,就像調用了一個普通的 Dart API 一樣。
接下來,我就與你詳細講述 Flutter 的方法通道機制吧。
## 方法通道
Flutter 作為一個跨平臺框架,提供了一套標準化的解決方案,為開發者屏蔽了操作系統的差異。但,Flutter 畢竟不是操作系統,因此在某些特定場景下(比如推送、藍牙、攝像頭硬件調用時),也需要具備直接訪問系統底層原生代碼的能力。為此,Flutter 提供了一套靈活而輕量級的機制來實現 Dart 和原生代碼之間的通信,即方法調用的消息傳遞機制,而方法通道則是用來傳遞通信消息的信道。
一次典型的方法調用過程類似網絡調用,由作為客戶端的 Flutter,通過方法通道向作為服務端的原生代碼宿主發送方法調用請求,原生代碼宿主在監聽到方法調用的消息后,調用平臺相關的 API 來處理 Flutter 發起的請求,最后將處理完畢的結果通過方法通道回發至 Flutter。調用過程如下圖所示:
:-: 
圖 1 方法通道示意圖
從上圖中可以看到,方法調用請求的處理和響應,在 Android 中是通過 FlutterView,而在 iOS 中則是通過 FlutterViewController 進行注冊的。FlutterView 與 FlutterViewController 為 Flutter 應用提供了一個畫板,使得構建于 Skia 之上的 Flutter 通過繪制即可實現整個應用所需的視覺效果。因此,它們不僅是 Flutter 應用的容器,同時也是 Flutter 應用的入口,自然也是注冊方法調用請求最合適的地方。
接下來,我通過一個例子來演示如何使用方法通道實現與原生代碼的交互。
## 方法通道使用示例
在實際業務中,提示用戶跳轉到應用市場(iOS 為 App Store、Android 則為各類手機應用市場)去評分是一個高頻需求,考慮到 Flutter 并未提供這樣的接口,而跳轉方式在 Android 和 iOS 上各不相同,因此我們需要分別在 Android 和 iOS 上實現這樣的功能,并暴露給 Dart 相關的接口。
我們先來看看作為客戶端的 Flutter,怎樣實現一次方法調用請求。
### Flutter 如何實現一次方法調用請求?
首先,我們需要確定一個唯一的字符串標識符,來構造一個命名通道;然后,在這個通道之上,Flutter 通過指定方法名“openAppMarket”來發起一次方法調用請求。
可以看到,這和我們平時調用一個 Dart 對象的方法完全一樣。因為方法調用過程是異步的,所以我們需要使用非阻塞(或者注冊回調)來等待原生代碼給予響應。
~~~
// 聲明 MethodChannel
const platform = MethodChannel('samples.chenhang/utils');
// 處理按鈕點擊
handleButtonClick() async{
int result;
// 異常捕獲
try {
// 異步等待方法通道的調用結果
result = await platform.invokeMethod('openAppMarket');
}
catch (e) {
result = -1;
}
print("Result:$result");
}
~~~
需要注意的是,與網絡調用類似,方法調用請求有可能會失敗(比如,Flutter 發起了原生代碼不支持的 API 調用,或是調用過程出錯等),因此我們需要把發起方法調用請求的語句用 try-catch 包裝起來。
調用方的實現搞定了,接下來就需要在原生代碼宿主中完成方法調用的響應實現了。由于我們需要適配 Android 和 iOS 兩個平臺,所以我們分別需要在兩個平臺上完成對應的接口實現。
### 在原生代碼中完成方法調用的響應
首先,**我們來看看 Android 端的實現方式**。在上一小結最后我提到,在 Android 平臺,方法調用的處理和響應是在 Flutter 應用的入口,也就是在 MainActivity 中的 FlutterView 里實現的,因此我們需要打開 Flutter 的 Android 宿主 App,找到 MainActivity.java 文件,并在其中添加相關的邏輯。
調用方與響應方都是通過命名通道進行信息交互的,所以我們需要在 onCreate 方法中,創建一個與調用方 Flutter 所使用的通道名稱一樣的 MethodChannel,并在其中設置方法處理回調,響應 openAppMarket 方法,打開應用市場的 Intent。同樣地,考慮到打開應用市場的過程可能會出錯,我們也需要增加 try-catch 來捕獲可能的異常:
~~~
protected void onCreate(Bundle savedInstanceState) {
...
// 創建與調用方標識符一樣的方法通道
new MethodChannel(getFlutterView(), "samples.chenhang/utils").setMethodCallHandler(
// 設置方法處理回調
new MethodCallHandler() {
// 響應方法請求
@Override
public void onMethodCall(MethodCall call, Result result) {
// 判斷方法名是否支持
if(call.method.equals("openAppMarket")) {
try {
// 應用市場 URI
Uri uri = Uri.parse("market://details?id=com.hangchen.example.flutter_module_page.host");
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// 打開應用市場
activity.startActivity(intent);
// 返回處理結果
result.success(0);
} catch (Exception e) {
// 打開應用市場出現異常
result.error("UNAVAILABLE", " 沒有安裝應用市場 ", null);
}
}else {
// 方法名暫不支持
result.notImplemented();
}
}
});
}
~~~
現在,方法調用響應的 Android 部分已經搞定,接下來我們來看一下**iOS 端的方法調用響應如何實現。**
在 iOS 平臺,方法調用的處理和響應是在 Flutter 應用的入口,也就是在 Applegate 中的 rootViewController(即 FlutterViewController)里實現的,因此我們需要打開 Flutter 的 iOS 宿主 App,找到 AppDelegate.m 文件,并添加相關邏輯。
與 Android 注冊方法調用響應類似,我們需要在 didFinishLaunchingWithOptions: 方法中,創建一個與調用方 Flutter 所使用的通道名稱一樣的 MethodChannel,并在其中設置方法處理回調,響應 openAppMarket 方法,通過 URL 打開應用市場:
~~~
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 創建命名方法通道
FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"samples.chenhang/utils" binaryMessenger:(FlutterViewController *)self.window.rootViewController];
// 往方法通道注冊方法調用處理回調
[channel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) {
// 方法名稱一致
if ([@"openAppMarket" isEqualToString:call.method]) {
// 打開 App Store(本例打開微信的 URL)
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"itms-apps://itunes.apple.com/xy/app/foo/id414478124"]];
// 返回方法處理結果
result(@0);
} else {
// 找不到被調用的方法
result(FlutterMethodNotImplemented);
}
}];
...
}
~~~
這樣,iOS 端的方法調用響應也已經實現了。
接下來,我們就可以在 Flutter 應用里,通過調用 openAppMarket 方法,實現打開不同操作系統提供的應用市場功能了。
需要注意的是,在原生代碼處理完畢后將處理結果返回給 Flutter 時,**我們在 Dart、Android 和 iOS 分別用了三種數據類型**:Android 端返回的是 java.lang.Integer、iOS 端返回的是 NSNumber、Dart 端接收到返回結果時又變成了 int 類型。這是為什么呢?
這是因為在使用方法通道進行方法調用時,由于涉及到跨系統數據交互,Flutter 會使用 StandardMessageCodec 對通道中傳輸的信息進行類似 JSON 的二進制序列化,以標準化數據傳輸行為。這樣在我們發送或者接收數據時,這些數據就會根據各自系統預定的規則自動進行序列化和反序列化。看到這里,你是不是對這樣類似網絡調用的方法通道技術有了更深刻的印象呢。?
對于上面提到的例子,類型為 java.lang.Integer 或 NSNumber 的返回值,先是被序列化成了一段二進制格式的數據在通道中傳輸,然后當該數據傳遞到 Flutter 后,又被反序列化成了 Dart 語言中的 int 類型的數據。
關于 Android、iOS 和 Dart 平臺間的常見數據類型轉換,我總結成了下面一張表格,幫助你理解與記憶。你只要記住,像 null、布爾、整型、字符串、數組和字典這些基本類型,是可以在各個平臺之間以平臺定義的規則去混用的,就可以了。
:-: 
圖 2 Android、iOS 和 Dart 平臺間的常見數據類型轉換
## 總結
好了,今天的分享就到這里,我們來總結一下主要內容吧。
方法通道解決了邏輯層的原生能力復用問題,使得 Flutter 能夠通過輕量級的異步方法調用,實現與原生代碼的交互。一次典型的調用過程由 Flutter 發起方法調用請求開始,請求經由唯一標識符指定的方法通道到達原生代碼宿主,而原生代碼宿主則通過注冊對應方法實現、響應并處理調用請求,最后將執行結果通過消息通道,回傳至 Flutter。
?需要注意的是,方法通道是非線程安全的。這意味著原生代碼與 Flutter 之間所有接口調用必須發生在主線程。Flutter 是單線程模型,因此自然可以確保方法調用請求是發生在主線程(Isolate)的;而原生代碼在處理方法調用請求時,如果涉及到異步或非主線程切換,需要確保回調過程是在原生系統的 UI 線程(也就是 Android 和 iOS 的主線程)中執行的,否則應用可能會出現奇怪的 Bug,甚至是 Crash。
我把今天分享所涉及到的知識點打包到了[GitHub](https://github.com/cyndibaby905/26_native_method)中,你可以下載下來,反復運行幾次,加深理解。
## 思考題
最后,我給你留下一道思考題吧。
請擴展方法通道示例,讓 openAppMarket 支持傳入 AppID 和包名,使得我們可以跳轉到任意一個 App 的應用市場。
- 前言
- 開篇詞
- 預習篇
- 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混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略