Flutter外掛學習之Native通訊詳解

redli發表於2020-04-28

前言

我們都知道Flutter開發的app是可以同時在iOS和Android系統上執行的。顯然Flutter需要有和Native通訊的能力。在實際專案中,Flutter並不能全面滿足專案需求,比如獲取一些硬體資訊(如電池電量),這些都是一些比較簡單的Native需求,Flutter官方也給出了一些比較常用的Plugin。但在實際專案中可能需求就沒那麼簡單了,比如融雲通訊、環信通訊,再或者專案自定義的需求,這可能就需要我們自己去寫外掛了。這裡我就以Flutter獲取Native電池電量和電池狀態(充電中、充滿電、未充電)為例(包括IOS-Swift 和 Android-Kotlin)來實現自己的需求。

思路

Flutter是如何做到的呢?當然是Flutter官方給的Platform Channels。如圖:

Flutter外掛學習之Native通訊詳解

上圖來自Flutter官網,表明了Platform Channels的架構示意圖。Platform Channel包括MethodChannel、EventChannel和BasicMessageChannel三大通道。

Platform Channel 支援的資料型別

比較常用

Dart iOS-Swift Android-Kotlin
null nil null
bool Bool Boolean
int Int Int
float Int Int
double Double Double
String String String
List Array List
Map Dictionary HashMap

MethodChannel

俗稱方法通道(個人見解)用於傳遞方法呼叫

以 Flutter 獲取 手機電量為例, 在 Flutter 介面中要想獲取 Android/iOS 的電量, 首先要在 Native 編寫獲取電量的功能, 供 Flutter 來呼叫。

  • Native - iOS
public class SwiftFlutterPluginLearningPlugin: NSObject, FlutterPlugin {
    
    public static func register(with registrar: FlutterPluginRegistrar) {
        let instance = SwiftFlutterPluginLearningPlugin()
        let methodChannel = FlutterMethodChannel(name: "plugins.limit.io/battery", binaryMessenger: registrar.messenger())
        //註冊電池(plugins.limit.io/battery)方法通道
        registrar.addMethodCallDelegate(instance, channel: methodChannel)
    }

    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "getBatteryLevel":
            let batterLevel = getBatteryLevel()
            if(batterLevel == -1) {
                result(FlutterError(code: "UNAVAILABLE", message: "Battery info unavailable", details: nil));
            } else {
                result(batterLevel)
            }
        default:
            result(FlutterMethodNotImplemented)
        }
    }
    
    //獲取電池電量
    private func getBatteryLevel() -> Float {
        let device = UIDevice.current
        device.isBatteryMonitoringEnabled = true
        if device.batteryState == .unknown {
            return -1;
        } else {
            return UIDevice.current.batteryLevel
        }
    }
}

複製程式碼
  • Native - Android
public class FlutterPluginLearningPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler {

    private var applicationContext: Context? = null

    /**
     * 方法通道
     */
    private var methodChannel: MethodChannel? = null

    /**
     * 連線到引擎
     */
    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        onAttachedToEngine(flutterPluginBinding.applicationContext, flutterPluginBinding.binaryMessenger)
    }

    private fun onAttachedToEngine(applicationContext: Context, messenger: BinaryMessenger) {
        this.applicationContext = applicationContext
        methodChannel = MethodChannel(messenger, "plugins.limit.io/battery")
        methodChannel?.setMethodCallHandler(this)
    }

    companion object {
        @JvmStatic
        fun registerWith(registrar: Registrar) {
            val instance = FlutterPluginLearningPlugin()
            instance.onAttachedToEngine(registrar.context(), registrar.messenger())
        }
    }

    /**
     * 回撥方法
     */
    override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        if (call.method == "getBatteryLevel") {
            val batterLevel = getBatteryLevel()
            if (batterLevel != -1) {
              result.success(batterLevel)
            } else {
              result.error("UNAVAILABLE", "Battery level not available.", null);
            }
        } else {
            result.notImplemented()
        }
    }

    /**
     * 從引擎中脫離
     */
    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        applicationContext = null
        methodChannel?.setMethodCallHandler(null)
        methodChannel = null
    }

    /**
     * 獲取電池電量方法
     */
    private fun getBatteryLevel(): Int {
        var batteryLevel = -1
        batteryLevel = if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
            val batteryManager = applicationContext?.let { getSystemService(it, BatteryManager::class.java) }
            batteryManager!!.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
        }

        return batteryLevel
    }
}
複製程式碼
  • Flutter

flutter 外掛

class FlutterPluginLearning {
  factory FlutterPluginLearning() {
    if (_instance == null) {
      final MethodChannel methodChannel =
          const MethodChannel('plugins.limit.io/battery');
      _instance = FlutterPluginLearning.init(methodChannel);
    }
    return _instance;
  }

  FlutterPluginLearning.init(this._methodChannel);

  static FlutterPluginLearning _instance;

  final MethodChannel _methodChannel;

  Future<int> get batteryLevel => _methodChannel
      .invokeMethod<int>('getBatteryLevel')
      .then<int>((dynamic result) => result);
}

複製程式碼

flutter 呼叫

FlutterPluginLearning _battery = FlutterPluginLearning();
_battery.batteryLevel.then((int batteryLevel) {
      //to do something you want
});
複製程式碼

EventChannel

俗稱流通道(個人見解)用於資料流(event streams)的通訊 以 Flutter 獲取 手機電池狀態為例, 在 Flutter 介面中要想獲取 Android/iOS 的電池狀態, 首先要在 Native 編寫獲取電池狀態的功能, 供 Flutter 來呼叫。

  • Native - iOS
public class SwiftFlutterPluginLearningPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
    
    var eventSink: FlutterEventSink?
    
    public static func register(with registrar: FlutterPluginRegistrar) {
        let instance = SwiftFlutterPluginLearningPlugin()
        let evenChannel = FlutterEventChannel(name: "plugins.limit.io/charging", binaryMessenger: registrar.messenger())
        evenChannel.setStreamHandler(instance)
    }

    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        
    }
    
    public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        eventSink = events
        UIDevice.current.isBatteryMonitoringEnabled = true
        self.sendBatteryStateEvent()
        NotificationCenter.default.addObserver(self, selector: #selector(onBatteryStateDidChange(notification:)), name: UIDevice.batteryStateDidChangeNotification, object: nil)
        return nil
    }
    
    public func onCancel(withArguments arguments: Any?) -> FlutterError? {
        NotificationCenter.default.removeObserver(self)
        eventSink = nil
        return nil
    }
    
    @objc private func onBatteryStateDidChange(notification: NSNotification?) {
        self.sendBatteryStateEvent()
    }
    
    private func sendBatteryStateEvent() {
        if eventSink == nil {
            return
        }
        let state = UIDevice.current.batteryState
        switch state {
        case .full:
            eventSink!("full")
            break
        case .charging:
            eventSink!("charging")
            break
        case .unplugged:
            eventSink!("unplugged")
            break
        default:
            eventSink!(FlutterError(code: "UNAVAILABLE", message: "Charging status unavailable", details: nil))
            break
        }
    }
}

複製程式碼
  • Native - Android
public class FlutterPluginLearningPlugin : FlutterPlugin, MethodCallHandler, EventChannel.StreamHandler {

    private var applicationContext: Context? = null

    /**
     * 事件流通道
     * Native 需要頻繁的傳送訊息給 Flutter, 比如監聽網路狀態, 藍芽裝置等等然後傳送給 Flutter
     */
    private var eventChannel: EventChannel?= null

    private var chargingStateChangeReceiver: BroadcastReceiver? = null

    /**
     * 連線到引擎
     */
    override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
        onAttachedToEngine(flutterPluginBinding.applicationContext, flutterPluginBinding.binaryMessenger)
    }

    private fun onAttachedToEngine(applicationContext: Context, messenger: BinaryMessenger) {
        this.applicationContext = applicationContext
        eventChannel = EventChannel(messenger, "plugins.limit.io/charging")
        eventChannel?.setStreamHandler(this)
    }

    companion object {
        @JvmStatic
        fun registerWith(registrar: Registrar) {
            val instance = FlutterPluginLearningPlugin()
            instance.onAttachedToEngine(registrar.context(), registrar.messenger())
        }
    }

    /**
     * 回撥方法
     */
    override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
        
    }

    /**
     * 從引擎中脫離
     */
    override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
        applicationContext = null
        eventChannel?.setStreamHandler(null)
        eventChannel = null
    }

    /**
     * 監聽
     */
    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
        chargingStateChangeReceiver = createChargingStateChangeReceiver(events)
        applicationContext?.registerReceiver(chargingStateChangeReceiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
    }

    /**
     * 取消監聽
     */
    override fun onCancel(arguments: Any?) {
        applicationContext?.unregisterReceiver(chargingStateChangeReceiver)
        chargingStateChangeReceiver = null
    }

    private fun createChargingStateChangeReceiver(events: EventChannel.EventSink?): BroadcastReceiver {
        return object : BroadcastReceiver() {
            override fun onReceive(context: Context?, intent: Intent?) {
                when (intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1)) {
                    BatteryManager.BATTERY_STATUS_CHARGING -> events?.success("charging")
                    BatteryManager.BATTERY_STATUS_FULL -> events?.success("full")
                    BatteryManager.BATTERY_STATUS_DISCHARGING -> events?.success("discharging")
                    else -> events?.error("UNAVAILABLE", "Charging status unavailable", null)
                }
            }
        }
    }

}

複製程式碼
  • Flutter

flutter 外掛

enum BatteryState {
  /// The battery is completely full of energy.
  full,

  /// The battery is currently storing energy.
  charging,

  /// The battery is currently losing energy.
  discharging
}

class FlutterPluginLearning {
  factory FlutterPluginLearning() {
    if (_instance == null) {
      
      final EventChannel eventChannel =
          const EventChannel('plugins.limit.io/charging');
      _instance = FlutterPluginLearning.init(eventChannel);
    }
    return _instance;
  }

  FlutterPluginLearning.init(this._eventChannel);

  static FlutterPluginLearning _instance;

  final EventChannel _eventChannel;
  Stream<BatteryState> _onBatteryStateChanged;

  Stream<BatteryState> get onBatteryStateChanged {
    if (_onBatteryStateChanged == null) {
      _onBatteryStateChanged = _eventChannel
          .receiveBroadcastStream()
          .map((dynamic event) => _parseBatteryState(event));
    }
    return _onBatteryStateChanged;
  }

  BatteryState _parseBatteryState(String state) {
    switch (state) {
      case 'full':
        return BatteryState.full;
      case 'charging':
        return BatteryState.charging;
      case 'discharging':
        return BatteryState.discharging;
      default:
        throw ArgumentError('$state is not a valid BatteryState.');
    }
  }
}
複製程式碼

flutter 呼叫

FlutterPluginLearning _battery = FlutterPluginLearning();
StreamSubscription<BatteryState> _batteryStateSubscription;
_batteryStateSubscription =
        _battery.onBatteryStateChanged.listen((BatteryState state) {
      String batteryState = 'UnKnow';
      switch (state) {
        case BatteryState.full:
          batteryState = '已充滿';
          break;
        case BatteryState.charging:
          batteryState = '充電中';
          break;
        case BatteryState.discharging:
          batteryState = '未充電';
          break;
        default:
      }
    // do something you want
    });
複製程式碼

BasicMessageChannel

俗稱訊息直接通道(個人見解)用於傳遞字串和半結構化的資訊。 如果僅僅是簡單的通訊而不是呼叫某個方法或者是事件流, 可以使用 BasicMessageChannel。BasicMessageChannel 也可以實現 Flutter 和 Native 的雙向通訊, 下面的示例圖就是官方的例子:

Flutter外掛學習之Native通訊詳解

詳細檢視(八)Flutter 和 Native之間的通訊詳解 的 BasicMessageChannel

Reference

(八)Flutter 和 Native之間的通訊詳解

官網 - platform-channels

原始碼

github.com/TBoyLi/flut… 覺得ok! Star ✨✨✨✨✨✨

相關文章