Flutter如何和Native通訊-Android視角

ad6623發表於2018-07-16

前言

我們都知道Flutter開發的app是可以同時在iOS和Android系統上執行的。顯然Flutter需要有和Native通訊的能力。比如說,你的Flutter app要顯示手機的電量,而電量只能通過平臺的系統Api獲取。這時就需要有個機制使得Flutter可以通過某種方式來呼叫這個系統Api並且獲得返回值。那麼Flutter是如何做到的呢?答案是Platform Channels。

Platform Channels

先來看張圖

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傳遞的訊息都是非同步的。

在Platform Channels上傳遞的訊息都是經過編碼的,編碼的方式也有幾種,預設的是用StandardMethodCodec。其他的還有BinaryCodec(二進位制的編碼,其實啥也沒幹,直接把入參給返回了), JSONMessageCodec(JSON格式的編碼),StringCodec(String格式的編碼)。這些編解碼器允許的只能是以下這些型別:

MessageCodec接受的型別
所以如果你想把你自己定義的com.yourmodule.YourObject型別的一個例項直接扔給Platform Channels傳送是不行滴。

Platform Channels 怎麼用

前面大概介紹了Flutter和Native通訊的Platform Channels。那麼我們用具體的例子來說說Platform Channels的使用。這裡使用Flutter官方出的獲取手機電量的Demo。相關原始碼可以從Github下載。

Platform Channels是連線Flutter和Native的通道,那麼我們如果要建立這樣的通道顯然要在兩端都要寫程式碼嘍。

MethodChannel

先看Native 端怎麼寫

MethodChannel-Native 端

為簡單起見,本例的Android端程式碼都直接寫在MainActivity中。Android平臺下獲取電量是通過呼叫BatteryManager來獲取的,所以我們先在MainActivity中增加一個獲取電量的函式:

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函式中加入以下程式碼來新建一個MethodChannel

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外掛,敬請期待。

相關文章