Flutter如何和Native通信-Android視角

前言

我們都知道Flutter開發(fā)的app是可以同時(shí)在iOS和Android系統(tǒng)上運(yùn)行的邦危。顯然Flutter需要有和Native通信的能力毯侦。比如說窄瘟,你的Flutter app要顯示手機(jī)的電量米死,而電量只能通過平臺的系統(tǒng)Api獲取锌历。這時(shí)就需要有個(gè)機(jī)制使得Flutter可以通過某種方式來調(diào)用這個(gè)系統(tǒng)Api并且獲得返回值。那么Flutter是如何做到的呢峦筒?答案是Platform Channels究西。

Platform Channels

先來看張圖

PlatformChannels.png

上圖來自Flutter官網(wǎng),表明了Platform Channels的架構(gòu)示意圖物喷。有細(xì)心的同學(xué)就要問了卤材,你不是說Flutter和Native通信是通過Platform Channels嗎?怎么架構(gòu)圖里面連接他們的是MethodChannel? 其實(shí)呢峦失,MethodChannel是Platform Channels中的一種扇丛,顧名思義,MethodChannel用起來應(yīng)該和方法調(diào)用差不多尉辑。那么還有別的channel帆精?有的,還有EventChannel,BasicMessageChannel等卓练。如果你需要把數(shù)據(jù)從Native平臺發(fā)送給Flutter隘蝎,推薦你使用EventChannel。Flutter framework也是在用這些通道和Native通信昆庇,具體可以參考一下FlutterView.java末贾,在這里能看到Platform Channels的更多用法。

這里需要注意一點(diǎn)整吆,為了保證UI的響應(yīng)拱撵,通過Platform Channels傳遞的消息都是異步的。

在Platform Channels上傳遞的消息都是經(jīng)過編碼的表蝙,編碼的方式也有幾種拴测,默認(rèn)的是用StandardMethodCodec。其他的還有BinaryCodec(二進(jìn)制的編碼府蛇,其實(shí)啥也沒干集索,直接把入?yún)⒔o返回了), JSONMessageCodec(JSON格式的編碼),StringCodec(String格式的編碼)汇跨。這些編解碼器允許的只能是以下這些類型:

MessageCodec接受的類型

所以如果你想把你自己定義的com.yourmodule.YourObject類型的一個(gè)實(shí)例直接扔給Platform Channels傳送是不行滴务荆。

Platform Channels 怎么用

前面大概介紹了Flutter和Native通信的Platform Channels。那么我們用具體的例子來說說Platform Channels的使用穷遂。這里使用Flutter官方出的獲取手機(jī)電量的Demo函匕。相關(guān)源代碼可以從Github下載。

Platform Channels是連接Flutter和Native的通道蚪黑,那么我們?nèi)绻⑦@樣的通道顯然要在兩端都要寫代碼嘍盅惜。

MethodChannel

先看Native 端怎么寫

MethodChannel-Native 端

為簡單起見,本例的Android端代碼都直接寫在MainActivity中忌穿。Android平臺下獲取電量是通過調(diào)用BatteryManager來獲取的抒寂,所以我們先在MainActivity中增加一個(gè)獲取電量的函數(shù):

private int getBatteryLevel() {
  int batteryLevel = -1;
  if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
    BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
    batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
  } else {
    Intent intent = new ContextWrapper(getApplicationContext()).
        registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
    batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
        intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
  }

  return batteryLevel;
}

這個(gè)函數(shù)需要能被Flutter app調(diào)用,此時(shí)就需要通過MethodChannel來建立這個(gè)通道了掠剑。
首先在MainActivityonCreate函數(shù)中加入以下代碼來新建一個(gè)MethodChannel

public class MainActivity extends FlutterActivity {
    //channel的名稱屈芜,由于app中可能會有多個(gè)channel,這個(gè)名稱需要在app內(nèi)是唯一的朴译。
    private static final String CHANNEL = "samples.flutter.io/battery";

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        GeneratedPluginRegistrant.registerWith(this);
        
        // 直接 new MethodChannel沸伏,然后設(shè)置一個(gè)Callback來處理Flutter端調(diào)用
        new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
                new MethodCallHandler() {
                    @Override
                    public void onMethodCall(MethodCall call, Result result) {
                        // 在這個(gè)回調(diào)里處理從Flutter來的調(diào)用
                    }
                });
    }
}

注意,每個(gè)MethodChannel需要有唯一的字符串作為標(biāo)識动分,用以互相區(qū)分,這個(gè)名稱建議使用package.module...這樣的模式來命名红选。因?yàn)樗械?code>MethodChannel都是保存在以通道名為Key的Map中澜公。所以你要是設(shè)了兩個(gè)名字一樣的channel,只有后設(shè)置的那個(gè)會生效。

接下來我們來填充onMethodCall坟乾。

@Override
public void onMethodCall(MethodCall call, Result result) {
    if (call.method.equals("getBatteryLevel")) {
        int batteryLevel = getBatteryLevel();

        if (batteryLevel != -1) {
            result.success(batteryLevel);
        } else {
            result.error("UNAVAILABLE", "Battery level not available.", null);
        }
    } else {
        result.notImplemented();
    }
}               

onMethodCall有兩個(gè)入?yún)ⅲ?code>MethodCall里包含要調(diào)用的方法名稱和參數(shù)迹辐。Result是給Flutter的返回值。方法名是兩端協(xié)商好的甚侣。通過if語句判斷MethodCall.method來區(qū)分不同的方法明吩,在我們的例子里面我們只會處理名為“getBatteryLevel”的調(diào)用。在調(diào)用本地方法獲取到電量以后通過result.success(batteryLevel)調(diào)用把電量值返回給Flutter殷费。
Native端的代碼就完成了印荔。是不是很簡單?

MethodChannel-Flutter 端

接下來看Flutter端代碼怎么寫:
首先在 State中創(chuàng)建Flutter端的MethodChannel

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
...
class _MyHomePageState extends State<MyHomePage> {
  static const platform = const MethodChannel('samples.flutter.io/battery');

  // Get battery level.
}

channel的名稱要和Native端的一致详羡。
然后是通過MethodChannel調(diào)用的代碼

String _batteryLevel = 'Unknown battery level.';

  Future<Null> _getBatteryLevel() async {
    String batteryLevel;
    try {
      final int result = await platform.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    }

    setState(() {
      _batteryLevel = batteryLevel;
    });
  }

final int result = await platform.invokeMethod('getBatteryLevel');這行代碼就是通過通道來調(diào)用Native方法了仍律。注意這里的await關(guān)鍵字。前面我們說過MethodChannel是異步的实柠,所以這里必須要使用await關(guān)鍵字。
在上面Native代碼中我們把獲取到的電量通過result.success(batteryLevel);返回給Flutter窒盐。這里await表達(dá)式執(zhí)行完成以后電量就直接賦值給result變量了草则。剩下的就是怎么展示的問題了,就不再細(xì)說了蟹漓,具體可以去看代碼炕横。

需要注意的是,這里我們只介紹了從Flutter調(diào)用Native方法牧牢,其實(shí)通過MethodChannel看锉,Native也能調(diào)用Flutter的方法,這是一個(gè)雙向的通道塔鳍。

舉個(gè)例子伯铣,我們想從Native端請求Flutter端的一個(gè)getName方法獲取一個(gè)字符串。在Flutter端你需要給MethodChannel設(shè)置一個(gè)MethodCallHandler

_channel.setMethodCallHandler(platformCallHandler);

Future<dynamic> platformCallHandler(MethodCall call) async {
       switch (call.method) {
             case "getName":
             return "Hello from Flutter";
             break;
       }
}

在Native端轮纫,只需要讓對應(yīng)的的channel調(diào)用invokeMethod就行了

channel.invokeMethod("getName", null, new MethodChannel.Result() {
          @Override
          public void success(Object o) {
            // 這里就會輸出 "Hello from Flutter"
            Log.i("debug", o.toString());
          }
          @Override
          public void error(String s, String s1, Object o) {
          }
          @Override
          public void notImplemented() {
          }
        });

至此腔寡,MethodChannel的用法就介紹完了≌仆伲可以發(fā)現(xiàn)放前,通過MethodChannelNative和Flutter方法互相調(diào)用還是蠻直接的。這里只是做了個(gè)大概的介紹糯彬,具體細(xì)節(jié)和一些復(fù)雜用法還有待大家的探索凭语。

MethodChannel提供了方法調(diào)用的通道,那如果Native有數(shù)據(jù)流需要傳送給Flutter該怎么辦呢撩扒?這時(shí)候就要用到EventChannel了似扔。

EventChannel

EventChannel的使用我們也以官方獲取電池電量的demo為例,手機(jī)的電池狀態(tài)是不停變化的。我們要把這樣的電池狀態(tài)變化由Native及時(shí)通過EventChannel來告訴Flutter炒辉。這種情況用之前講的MethodChannel辦法是不行的豪墅,這意味著Flutter需要用輪詢的方式不停調(diào)用getBatteryLevel來獲取當(dāng)前電量,顯然是不正確的做法黔寇。而用EventChannel的方式偶器,則是將當(dāng)前電池狀態(tài)"推送"給Flutter.

EventChannel - Native端

先看我們熟悉的Native端怎么來創(chuàng)建EventChannel, 還是在MainActivity.onCreate中,我們加入如下代碼:

new EventChannel(getFlutterView(), "samples.flutter.io/charging").setStreamHandler(
        new StreamHandler() {
          // 接收電池廣播的BroadcastReceiver缝裤。
          private BroadcastReceiver chargingStateChangeReceiver;
          @Override
         // 這個(gè)onListen是Flutter端開始監(jiān)聽這個(gè)channel時(shí)的回調(diào)屏轰,第二個(gè)參數(shù) EventSink是用來傳數(shù)據(jù)的載體。
          public void onListen(Object arguments, EventSink events) {
            chargingStateChangeReceiver = createChargingStateChangeReceiver(events);
            registerReceiver(
                chargingStateChangeReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
          }

          @Override
          public void onCancel(Object arguments) {
            // 對面不再接收
            unregisterReceiver(chargingStateChangeReceiver);
            chargingStateChangeReceiver = null;
          }
        }
    );

MethodChannel類似倘是,我們也是直接new一個(gè)EventChannel實(shí)例亭枷,并給它設(shè)置了一個(gè)StreamHandler類型的回調(diào)。其中onCancel代表對面不再接收搀崭,這里我們應(yīng)該做一些clean up的事情叨粘。而 onListen則代表通道已經(jīng)建好,Native可以發(fā)送數(shù)據(jù)了瘤睹。注意onListen里帶的EventSink這個(gè)參數(shù)升敲,后續(xù)Native發(fā)送數(shù)據(jù)都是經(jīng)過EventSink的『浯看代碼:

private BroadcastReceiver createChargingStateChangeReceiver(final EventSink events) {
    return new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
        int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);

        if (status == BatteryManager.BATTERY_STATUS_UNKNOWN) {
          events.error("UNAVAILABLE", "Charging status unavailable", null);
        } else {
          boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
                               status == BatteryManager.BATTERY_STATUS_FULL;
          // 把電池狀態(tài)發(fā)給Flutter
          events.success(isCharging ? "charging" : "discharging");
        }
      }
    };
  }

onReceive函數(shù)內(nèi)驴党,系統(tǒng)發(fā)來電池狀態(tài)廣播以后,在Native這里轉(zhuǎn)化為約定好的字符串获茬,然后通過調(diào)用events.success();發(fā)送給Flutter港庄。Native端的代碼就是這樣,接下來看Flutter端恕曲。

EventChannel - Flutter端

首先還是在State內(nèi)創(chuàng)建EventChannel

static const EventChannel eventChannel =
      const EventChannel('samples.flutter.io/charging');

然后在initState的時(shí)候打開這個(gè)channel:

@override
  void initState() {
    super.initState();
    eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
  }

收到event以后的處理是在_onEvent函數(shù)里:

void _onEvent(Object event) {
    setState(() {
      _chargingStatus =
          "Battery status: ${event == 'charging' ? '' : 'dis'}charging.";
    });
  }

  void _onError(Object error) {
    setState(() {
      _chargingStatus = 'Battery status: unknown.';
    });
  }

從Native端傳過來的"charging"/"discharging"字符串直接就是入?yún)?code>event鹏氧。好了,F(xiàn)lutter端的代碼也貼完了佩谣,是不是感覺EventChannel用起來也很簡單把还?

收尾

至此,本文對Flutter和Native之間互相通信的方式的講解也要告一段落了茸俭。Flutter的出發(fā)點(diǎn)就是跨平臺吊履,而真正要做到跨平臺則取決于Flutter是否能通過簡單的方式與Native高效通信。Platform Channels能否實(shí)現(xiàn)這個(gè)目標(biāo)還有待大規(guī)模應(yīng)用的檢驗(yàn)调鬓。對于Flutter開發(fā)者來講艇炎,由于眾多的Native平臺API需要暴露給Flutter,還有很多用Native實(shí)現(xiàn)的組件/業(yè)務(wù)邏輯也可能需要暴露給Flutter腾窝。這需要寫大量的通道代碼冕臭,也就是說我們必須掌握使用Platform Channels的技能腺晾,才能體會到Flutter真正的跨平臺能力。本文中對Platform Channels的應(yīng)用只是非常簡單的demo辜贵。在大型app中還存在兩大挑戰(zhàn),一個(gè)是大量的通道我們?nèi)绾谓M織归形,如何維護(hù)托慨。另一個(gè)是通道協(xié)議如何設(shè)計(jì)才能抹平Android和iOS之間的平臺差異,這就需要開發(fā)這對兩個(gè)平臺都非常熟悉暇榴,這個(gè)貌似更加困難厚棵。

當(dāng)然了,如果你做出來了完美的通道蔼紧,將平臺的某個(gè)功能(比如藍(lán)牙婆硬,GPS什么的)包裝成了優(yōu)美的Flutter API,并且希望世界上其他Flutter開發(fā)者也能使用奸例。那么你可以把你智慧的結(jié)晶通過發(fā)布Flutter插件(plugin)的方式開放給別人彬犯。下篇文章我會介紹一下如何來開發(fā)一個(gè)Flutter插件,敬請期待查吊。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谐区,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子逻卖,更是在濱河造成了極大的恐慌宋列,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件评也,死亡現(xiàn)場離奇詭異炼杖,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)盗迟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門坤邪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人诈乒,你說我怎么就攤上這事罩扇。” “怎么了怕磨?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵喂饥,是天一觀的道長。 經(jīng)常有香客問我肠鲫,道長员帮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任导饲,我火速辦了婚禮捞高,結(jié)果婚禮上氯材,老公的妹妹穿的比我還像新娘。我一直安慰自己硝岗,他們只是感情好氢哮,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著型檀,像睡著了一般冗尤。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胀溺,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天裂七,我揣著相機(jī)與錄音,去河邊找鬼仓坞。 笑死背零,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的无埃。 我是一名探鬼主播徙瓶,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼录语!你這毒婦竟也來了倍啥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤澎埠,失蹤者是張志新(化名)和其女友劉穎虽缕,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蒲稳,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡氮趋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了江耀。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片剩胁。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖祥国,靈堂內(nèi)的尸體忽然破棺而出昵观,到底是詐尸還是另有隱情,我是刑警寧澤舌稀,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布啊犬,位于F島的核電站,受9級特大地震影響壁查,放射性物質(zhì)發(fā)生泄漏觉至。R本人自食惡果不足惜囚聚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一滩字、第九天 我趴在偏房一處隱蔽的房頂上張望非春。 院中可真熱鬧垫竞,春花似錦、人聲如沸应闯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽孽锥。三九已至嚼黔,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惜辑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工疫赎, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留盛撑,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓捧搞,卻偏偏與公主長得像抵卫,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子胎撇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

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

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,515評論 25 707
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理介粘,服務(wù)發(fā)現(xiàn),斷路器晚树,智...
    卡卡羅2017閱讀 134,600評論 18 139
  • 本文列舉了項(xiàng)目開發(fā)使用Flutter會遇到的問題姻采,以及如何使用Flutter module在現(xiàn)有項(xiàng)目中集成Flut...
    Q吹個(gè)大氣球Q閱讀 4,736評論 7 15
  • 第一百一十七首 我愛你的不完美 秋憶濃 我愛你的不完美, 就像蝸牛愛著貝殼爵憎,...
    山丘qiu閱讀 239評論 0 2
  • (PS你說:陪你走過人生中的點(diǎn)點(diǎn)滴滴慨亲。)這是你們,看著你和你愛人的照片宝鼓,她的臉上溢出了幸福和滿足感刑棵,就像一只剛剛吃...
    有你我便不再是摩西閱讀 199評論 0 0