Flutter如何和Native通訊-Android視角

前言

咱們都知道Flutter開發的app是能夠同時在iOS和Android系統上運行的。顯然Flutter須要有和Native通訊的能力。好比說,你的Flutter app要顯示手機的電量,而電量只能經過平臺的系統Api獲取。這時就須要有個機制使得Flutter能夠經過某種方式來調用這個系統Api而且得到返回值。那麼Flutter是如何作到的呢?答案是Platform Channels。java

Platform Channels

先來看張圖 git

PlatformChannels.png
上圖來自Flutter官網,代表了Platform Channels的架構示意圖。有細心的同窗就要問了,你不是說Flutter和Native通訊是經過Platform Channels嗎?怎麼架構圖裏面鏈接他們的是MethodChannel? 其實呢,MethodChannel是Platform Channels中的一種,顧名思義,MethodChannel用起來應該和方法調用差很少。那麼還有別的channel?有的,還有EventChannel,BasicMessageChannel等。若是你須要把數據從Native平臺發送給Flutter,推薦你使用EventChannel。Flutter framework也是在用這些通道和Native通訊,具體能夠參考一下 FlutterView.java,在這裏能看到Platform Channels的更多用法。

這裏須要注意一點,爲了保證UI的響應,經過Platform Channels傳遞的消息都是異步的。github

在Platform Channels上傳遞的消息都是通過編碼的,編碼的方式也有幾種,默認的是用StandardMethodCodec。其餘的還有BinaryCodec(二進制的編碼,其實啥也沒幹,直接把入參給返回了), JSONMessageCodec(JSON格式的編碼),StringCodec(String格式的編碼)。這些編解碼器容許的只能是如下這些類型:bash

MessageCodec接受的類型
因此若是你想把你本身定義的 com.yourmodule.YourObject類型的一個實例直接扔給Platform Channels傳送是不行滴。

Platform Channels 怎麼用

前面大概介紹了Flutter和Native通訊的Platform Channels。那麼咱們用具體的例子來講說Platform Channels的使用。這裏使用Flutter官方出的獲取手機電量的Demo。相關源代碼能夠從Github下載。架構

Platform Channels是鏈接Flutter和Native的通道,那麼咱們若是要創建這樣的通道顯然要在兩端都要寫代碼嘍。app

MethodChannel

先看Native 端怎麼寫異步

MethodChannel-Native 端

爲簡單起見,本例的Android端代碼都直接寫在MainActivity中。Android平臺下獲取電量是經過調用BatteryManager來獲取的,因此咱們先在MainActivity中增長一個獲取電量的函數:async

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;
}
複製代碼

這個函數須要能被Flutter app調用,此時就須要經過MethodChannel來創建這個通道了。 首先在MainActivityonCreate函數中加入如下代碼來新建一個MethodChannelide

public class MainActivity extends FlutterActivity {
    //channel的名稱,因爲app中可能會有多個channel,這個名稱須要在app內是惟一的。
    private static final String CHANNEL = "samples.flutter.io/battery";

    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        GeneratedPluginRegistrant.registerWith(this);
        
        // 直接 new MethodChannel,而後設置一個Callback來處理Flutter端調用
        new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
                new MethodCallHandler() {
                    @Override
                    public void onMethodCall(MethodCall call, Result result) {
                        // 在這個回調裏處理從Flutter來的調用
                    }
                });
    }
}
複製代碼

注意,每一個MethodChannel須要有惟一的字符串做爲標識,用以互相區分,這個名稱建議使用package.module...這樣的模式來命名。由於全部的MethodChannel都是保存在以通道名爲Key的Map中。因此你要是設了兩個名字同樣的channel,只有後設置的那個會生效。函數

接下來咱們來填充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有兩個入參,MethodCall裏包含要調用的方法名稱和參數。Result是給Flutter的返回值。方法名是兩端協商好的。經過if語句判斷MethodCall.method來區分不一樣的方法,在咱們的例子裏面咱們只會處理名爲「getBatteryLevel」的調用。在調用本地方法獲取到電量之後經過result.success(batteryLevel)調用把電量值返回給Flutter。 Native端的代碼就完成了。是否是很簡單?

MethodChannel-Flutter 端

接下來看Flutter端代碼怎麼寫: 首先在 State中建立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調用的代碼

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');這行代碼就是經過通道來調用Native方法了。注意這裏的await關鍵字。前面咱們說過MethodChannel是異步的,因此這裏必需要使用await關鍵字。 在上面Native代碼中咱們把獲取到的電量經過result.success(batteryLevel);返回給Flutter。這裏await表達式執行完成之後電量就直接賦值給result變量了。剩下的就是怎麼展現的問題了,就再也不細說了,具體能夠去看代碼。

須要注意的是,這裏咱們只介紹了從Flutter調用Native方法,其實經過MethodChannel,Native也能調用Flutter的方法,這是一個雙向的通道

舉個例子,咱們想從Native端請求Flutter端的一個getName方法獲取一個字符串。在Flutter端你須要給MethodChannel設置一個MethodCallHandler

_channel.setMethodCallHandler(platformCallHandler);

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

在Native端,只須要讓對應的的channel調用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的用法就介紹完了。能夠發現,經過MethodChannelNative和Flutter方法互相調用仍是蠻直接的。這裏只是作了個大概的介紹,具體細節和一些複雜用法還有待你們的探索。

MethodChannel提供了方法調用的通道,那若是Native有數據流須要傳送給Flutter該怎麼辦呢?這時候就要用到EventChannel了。

EventChannel

EventChannel的使用咱們也以官方獲取電池電量的demo爲例,手機的電池狀態是不停變化的。咱們要把這樣的電池狀態變化由Native及時經過EventChannel來告訴Flutter。這種狀況用以前講的MethodChannel辦法是不行的,這意味着Flutter須要用輪詢的方式不停調用getBatteryLevel來獲取當前電量,顯然是不正確的作法。而用EventChannel的方式,則是將當前電池狀態"推送"給Flutter.

EventChannel - Native端

先看咱們熟悉的Native端怎麼來建立EventChannel, 仍是在MainActivity.onCreate中,咱們加入以下代碼:

new EventChannel(getFlutterView(), "samples.flutter.io/charging").setStreamHandler(
        new StreamHandler() {
          // 接收電池廣播的BroadcastReceiver。
          private BroadcastReceiver chargingStateChangeReceiver;
          @Override
         // 這個onListen是Flutter端開始監聽這個channel時的回調,第二個參數 EventSink是用來傳數據的載體。
          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一個EventChannel實例,並給它設置了一個StreamHandler類型的回調。其中onCancel表明對面再也不接收,這裏咱們應該作一些clean up的事情。而 onListen則表明通道已經建好,Native能夠發送數據了。注意onListen裏帶的EventSink這個參數,後續Native發送數據都是通過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;
          // 把電池狀態發給Flutter
          events.success(isCharging ? "charging" : "discharging");
        }
      }
    };
  }
複製代碼

onReceive函數內,系統發來電池狀態廣播之後,在Native這裏轉化爲約定好的字符串,而後經過調用events.success();發送給Flutter。Native端的代碼就是這樣,接下來看Flutter端。

EventChannel - Flutter端

首先仍是在State內建立EventChannel

static const EventChannel eventChannel =
      const EventChannel('samples.flutter.io/charging');
複製代碼

而後在initState的時候打開這個channel:

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

收到event之後的處理是在_onEvent函數裏:

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

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

從Native端傳過來的"charging"/"discharging"字符串直接就是入參event。好了,Flutter端的代碼也貼完了,是否是感受EventChannel用起來也很簡單?

收尾

至此,本文對Flutter和Native之間互相通訊的方式的講解也要告一段落了。Flutter的出發點就是跨平臺,而真正要作到跨平臺則取決於Flutter是否能經過簡單的方式與Native高效通訊。Platform Channels可否實現這個目標還有待大規模應用的檢驗。對於Flutter開發者來說,因爲衆多的Native平臺API須要暴露給Flutter,還有不少用Native實現的組件/業務邏輯也可能須要暴露給Flutter。這須要寫大量的通道代碼,也就是說咱們必須掌握使用Platform Channels的技能,才能體會到Flutter真正的跨平臺能力。本文中對Platform Channels的應用只是很是簡單的demo。在大型app中還存在兩大挑戰,一個是大量的通道咱們如何組織,如何維護。另外一個是通道協議如何設計才能抹平Android和iOS之間的平臺差別,這就須要開發這對兩個平臺都很是熟悉,這個貌似更加困難。

固然了,若是你作出來了完美的通道,將平臺的某個功能(好比藍牙,GPS什麼的)包裝成了優美的Flutter API,而且但願世界上其餘Flutter開發者也能使用。那麼你能夠把你智慧的結晶經過發佈Flutter插件(plugin)的方式開放給別人。下篇文章我會介紹一下如何來開發一個Flutter插件,敬請期待。

相關文章
相關標籤/搜索