Flutter | 如何優(yōu)雅的調(diào)用 Android 原生方法?

Flutter 的優(yōu)勢與缺點(diǎn)

Flutter 作為一個跨平臺的移動 UI 框架汉嗽,可以快速在 iOS 和 Android 上構(gòu)建高質(zhì)量的原生用戶界面欲逃。可以說是一套代碼做到了多端運(yùn)行饼暑,我們常說的 Flutter 跨平臺稳析,其實是 UI 跨平臺,編寫好了一套 UI 代碼撵孤,就可以在 IOS迈着、Android、Web 上呈現(xiàn)出同樣的效果邪码,節(jié)省了大量的人力成本裕菠,這也是 Flutter 越來越流行的原因之一。

然而 Flutter 也有它的缺點(diǎn)闭专,很明顯的是它并不能直接調(diào)用平臺的系統(tǒng)功能奴潘,比如使用藍(lán)牙、相機(jī)影钉、GPS画髓、音量、電池等平委,因此要在 Flutter 中調(diào)用這些能力就必須和原生平臺進(jìn)行通信奈虾。

Flutter 架構(gòu)概覽

了解一個框架的全貌,有助于我們從更高的視角去看待一門新技術(shù),而避免深陷代碼細(xì)節(jié)肉微,無法自拔匾鸥。首先來看一張官方給出的 Flutter 整體架構(gòu)圖,如下碉纳。

Flutter 架構(gòu)概覽

官方將 Flutter 框架大致分為了三層勿负,F(xiàn)ramework、Engine劳曹、Embedder奴愉。下面我將詳細(xì)講解一下這三層到底是什么,在實際開發(fā)中承擔(dān)著怎樣的角色铁孵。

Dart Framework 層

作為一個 Flutter 開發(fā)者锭硼,辛勤的碼農(nóng),你每天都在這片土地?fù)]灑著汗水库菲。
你每天使用的各種 UI 組件(Flutter UI 控件已經(jīng)快接近 400 個了)账忘,各種 Flutter Package志膀,F(xiàn)lutter Plugin熙宇,層出不窮的第三方庫讓你感到頭皮發(fā)涼。所以我不過多介紹大家都應(yīng)該懂了吧溉浙,Dart 是最好的語言~

Engine 層

Flutter 引擎烫止,官方是這樣介紹它的。

Flutter 的核心戳稽,主要使用 C++ 編寫馆蠕,提供了 Flutter 應(yīng)用所需的原語。當(dāng)需要繪制新一幀的內(nèi)容時惊奇,引擎將負(fù)責(zé)對需要合成的場景進(jìn)行柵格化互躬。它提供了 Flutter 核心 API 的底層實現(xiàn),包括圖形(通過 Skia)颂郎、文本布局吼渡、文件及網(wǎng)絡(luò) IO、輔助功能支持乓序、插件架構(gòu)和 Dart 運(yùn)行環(huán)境及編譯環(huán)境的工具鏈寺酪。

我覺得官方描述得已經(jīng)很詳細(xì)了,總結(jié)一下就是:它負(fù)責(zé) Flutter UI 的渲染以及宿主的交互替劈,接入了 Engine 的就叫做宿主寄雀,如 Android 品臺接入了 Flutter Engine,那么宿主就是 Android 平臺陨献。

Flutter Engine 是開源的盒犹,在 github 上可以找到,傳送門 https://github.com/flutter/engine
直觀感受下這個 Engine 項目都是用哪些語言編寫的,如下圖急膀。其實主要語言還是 C++膜蛔,其中 Dart 很多是測試代碼。

Flutter Engine

Embedder 平臺嵌入層

官方是這樣介紹它的脖阵。

平臺嵌入層是用于呈現(xiàn)所有 Flutter 內(nèi)容的原生系統(tǒng)應(yīng)用皂股,它充當(dāng)著宿主操作系統(tǒng)和 Flutter 之間的粘合劑的角色。當(dāng)你啟動一個 Flutter 應(yīng)用時命黔,嵌入層會提供一個入口呜呐,初始化 Flutter 引擎,獲取 UI 和柵格化線程悍募,創(chuàng)建 Flutter 可以寫入的紋理蘑辑。嵌入層同時負(fù)責(zé)管理應(yīng)用的生命周期,包括輸入的操作(例如鼠標(biāo)坠宴、鍵盤和觸控)洋魂、窗口大小的變化、線程管理和平臺消息的傳遞喜鼓。 Flutter 擁有 Android副砍、iOS、Windows庄岖、macOS 和 Linux 的平臺嵌入層豁翎,當(dāng)然,開發(fā)者可以創(chuàng)建自定義的嵌入層

我覺得官方這段解釋得同樣很好隅忿,我?guī)缀鯚o法有更好的解釋~但我還是要談?wù)勛约旱囊娊庑陌驗閺某绦騿T的角度看代碼比談概念更具體。Embedder 層的代碼同樣包含于 Engine 項目中背桐,如下圖优烧。

Flutter Embedder

Embedder 層代碼整體可以分為兩大塊

  • 各平臺的 Embedder 層代碼,用于接入 Flutter Engine链峭。如 Android 平臺 Embedder 是用 Java 寫的畦娄,當(dāng)然還有與其對應(yīng)的 C++ 代碼,方便 Java 調(diào)用 JNI 來和 Flutter Engine 之間通信熏版。自然 IOS 就是 Object-C 了纷责。
  • 共用的 Embedder 層代碼,C++ 編寫撼短, 用于平臺消息傳遞等功能再膳。

了解了 Flutter 架構(gòu),下面開始進(jìn)入實戰(zhàn)環(huán)節(jié)曲横。

Flutter 如何與特定平臺進(jìn)行通信

本小節(jié)再次強(qiáng)調(diào)“特定平臺”這個概念喂柒,因為 Flutter 它只是一個 UI 跨平臺的框架不瓶,讀者需要牢記于心,凡是涉及到平臺相關(guān)的功能灾杰,還是必須要由原生平臺來實現(xiàn)蚊丐。本文以 Flutter 在 Android 平臺上的應(yīng)用,來講解 Flutter 該怎樣和 Android 原生進(jìn)行通信艳吠。

Flutter 與特定平臺進(jìn)行通信的流程大致是這樣的麦备。

  • Flutter 通過類似 JNI 方法調(diào)用的方式與 Flutter Engine 通信
  • Flutter Engine 層調(diào)用特定平臺的 Embedder 層代碼
  • 特定平臺接收到來自 Embedder 層的消息,進(jìn)行業(yè)務(wù)處理

反之昭娩,特定平臺想和 Flutter 進(jìn)行通信凛篙,順序剛好和上面相反。

使用 MethodChannel 通道進(jìn)行方法調(diào)用

Flutter 與特定平臺進(jìn)行通信需要通過“平臺通道”栏渺,細(xì)心的讀者可能已經(jīng)發(fā)現(xiàn)呛梆,平臺通道(Platform Channels)被官方劃分在 Engine 層。平臺通道分為三種類型磕诊,分別是 BasicMessageChannel填物、MethodChannel、EventChannel霎终。其中 MethodChannel 它使用異步方法調(diào)用的方式進(jìn)行平臺通信滞磺,這也是最常用的一種方式。

編寫 Flutter 端代碼

這里以官方的一個手機(jī)電量查詢例子來演示整個方法調(diào)用過程神僵,F(xiàn)lutter 端代碼如下雁刷。優(yōu)雅的做法是,遵循職責(zé)單一的原則保礼,將每一個方法通道封裝成一個類。比如我這個通道是用來管理電量的责语,那么就叫 BatteryChannel炮障,所有和電量有關(guān)的方法都封裝在這個類中。

class BatteryChannel {
  static const _batteryChannelName = "cn.blogss/battery";  // 1.方法通道名稱
  static MethodChannel _batteryChannel;

  static void initChannels(){
    _batteryChannel = MethodChannel(_batteryChannelName);  // 2. 實例化一個方法通道
  }

  // 3. 異步任務(wù)坤候,通過平臺通道與特定平臺進(jìn)行通信胁赢,獲取電量,這里的宿主平臺是 Android
  static getBatteryLevel() async {  
    String batteryLevel;
    try {
      final int result = await _batteryChannel.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    return batteryLevel;
  }
}

在你需要使用這個通道的時候白筹,在 Flutter 頁面中這樣做就行了智末。

class BatteryRoute extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return BatteryRouteState();
  }
}

class BatteryRouteState extends State<BatteryRoute> {
  String _batteryLevel = 'Unknown battery level.';
  // 3.異步獲取到電量,然后重新渲染頁面
  getBatteryLevel() async{
    _batteryLevel = await BatteryChannel.getBatteryLevel();
    setState(() {});
  }

  @override
  void initState() {
    super.initState();
    BatteryChannel.initChannels();  // 1. 初始化通道
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("BatteryRoute"),
        centerTitle: true,
      ),
      body: new Center(
        child: new Column(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            new ElevatedButton(
              child: new Text('Get Battery Level'),
              onPressed: (){
                getBatteryLevel();  // 2. 調(diào)用通道方法
              },
            ),
            new Text(_batteryLevel),
          ],
        ),
      ),
    );
  }
}

編寫 Android 端代碼

Android 端代碼與 Flutter 端類似徒河,如下所示系馆。同樣這個類叫BatteryChannel,下面是 Kotlin 寫的顽照,沒了解過的讀者理解上來可能有點(diǎn)難度由蘑。不過它也很簡單闽寡,這個通道就負(fù)責(zé)電量的查詢了。只需實現(xiàn) MethodChannel.MethodCallHandler 接口尼酿,重寫 onMethodCall 方法爷狈,這樣 Flutter 端的方法調(diào)用就會進(jìn)入到這里。

class BatteryChannel(flutterEngine: BinaryMessenger, context: Context): MethodChannel.MethodCallHandler {
    private val batteryChannelName = "cn.blogss/battery"
    private var channel: MethodChannel
    private var mContext: Context

    companion object {
        private const val TAG = "BatteryChannel"
    }

    init {
        Log.d(TAG, "init")
        channel = MethodChannel(flutterEngine, batteryChannelName)
        channel.setMethodCallHandler(this)
        mContext = context;
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        Log.d(TAG, "onMethodCall: " + call.method)
        if (call.method == "getBatteryLevel") {
            val batteryLevel = getBatteryLevel()
            if (batteryLevel != -1) {
                result.success(batteryLevel)
            } else {
                result.error("UNAVAILABLE", "Battery level not available.", null)
            }
        } else {
            result.notImplemented()
        }
    }

    private fun getBatteryLevel(): Int {
        val batteryLevel: Int
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val batteryManager = mContext.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = ContextWrapper(mContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) *
                    100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        }

        return batteryLevel
    }
}

然后我們還要在 MainActivity 中實例化一下這個通道裳擎,如下涎永。常用的做法是在 configureFlutterEngine 這個方法中實例化我們的通道就行了,有多少個通道鹿响,就在這里實例化多少個通道土辩。

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        BatteryChannel(flutterEngine.dartExecutor.binaryMessenger,context)  // 實例化通道
    }
}

最后看下效果。


Battery Level

寫在最后

本文詳細(xì)介紹了 Flutter 框架概覽抢野,分析了 Dart Framework拷淘、Engine、Embedded 三層實際開發(fā)中所處的位置指孤,然后通過代碼實戰(zhàn)启涯,F(xiàn)lutter 通過平臺通道與 Android 平臺進(jìn)行通信,查詢到了手機(jī)電量恃轩。讀者應(yīng)該對這兩部分知識有了深刻的理解與運(yùn)用结洼,下一篇,我將繼續(xù)帶領(lǐng)大家更深層次的探索平臺通道通信機(jī)制叉跛!

如果你對我感興趣松忍,請移步到 http://blogss.cn ,進(jìn)一步了解筷厘。

  • 如果本文幫助到了你鸣峭,歡迎點(diǎn)贊和關(guān)注 ??
  • 由于作者水平有限,文中如果有錯誤酥艳,歡迎在評論區(qū)指正 ??
  • 本文首發(fā)于簡書摊溶,未經(jīng)許可禁止轉(zhuǎn)載 ??
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市充石,隨后出現(xiàn)的幾起案子莫换,更是在濱河造成了極大的恐慌,老刑警劉巖骤铃,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件糜芳,死亡現(xiàn)場離奇詭異个从,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進(jìn)店門呻惕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贰谣,“玉大人胀葱,你說我怎么就攤上這事⊙湃危” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵咨跌,是天一觀的道長沪么。 經(jīng)常有香客問我,道長锌半,這世上最難降的妖魔是什么禽车? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮刊殉,結(jié)果婚禮上殉摔,老公的妹妹穿的比我還像新娘。我一直安慰自己记焊,他們只是感情好逸月,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著遍膜,像睡著了一般碗硬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓢颅,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天恩尾,我揣著相機(jī)與錄音,去河邊找鬼挽懦。 笑死翰意,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的信柿。 我是一名探鬼主播冀偶,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼角塑!你這毒婦竟也來了蔫磨?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤圃伶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蒲列,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體窒朋,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年蝗岖,在試婚紗的時候發(fā)現(xiàn)自己被綠了侥猩。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡抵赢,死狀恐怖欺劳,靈堂內(nèi)的尸體忽然破棺而出唧取,到底是詐尸還是另有隱情,我是刑警寧澤划提,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布枫弟,位于F島的核電站,受9級特大地震影響鹏往,放射性物質(zhì)發(fā)生泄漏淡诗。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一伊履、第九天 我趴在偏房一處隱蔽的房頂上張望韩容。 院中可真熱鬧,春花似錦唐瀑、人聲如沸群凶。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽请梢。三九已至,卻和暖如春柔滔,著一層夾襖步出監(jiān)牢的瞬間溢陪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工睛廊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留形真,地道東北人。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓超全,卻偏偏與公主長得像咆霜,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蛾坯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評論 2 354

推薦閱讀更多精彩內(nèi)容