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

Chiclaim發表於2019-07-29

前言

在實際的開發中通常需要 Flutter 呼叫 Native 的功能,或者 Native 呼叫 Flutter 的功能

它們之間的通訊主要是通過 Platform Channel 來實現的, 主要有 3channel :

  • MethodChannel 用於傳遞方法呼叫
  • EventChannel 用於資料流(event streams)的通訊
  • BasicMessageChannel 用於傳遞字串和半結構化的資訊

下圖以 MethodChannel 為例, 展示了 FlutterNative 之間的訊息傳遞:

在這裡插入圖片描述

為了應用的流暢度, 能夠及時響應使用者的操作, FlutterNative 之間訊息和響應的傳遞都是非同步的, 但是呼叫 channel api 的時候需要在 主執行緒 中呼叫

Platform Channel 支援的資料型別

Platform Channel 通過標準的訊息編解碼器來為我們在 傳送接收 資料時自動 序列化反序列化

編解碼支援的資料型別有:

Dart Android iOS
null null nil (NSNull when nested)
bool java.lang.Boolean NSNumber numberWithBool:
int java.lang.Integer NSNumber numberWithInt:
int(if 32 bits not enough) java.lang.Long NSNumber numberWithLong:
double java.lang.Double NSNumber numberWithDouble:
String java.lang.String NSString
Uint8List byte[] FlutterStandardTypedData typedDataWithBytes:
Int32List int[] FlutterStandardTypedData typedDataWithInt32:
Int64List long[] FlutterStandardTypedData typedDataWithInt64:
Float64List double[] FlutterStandardTypedData typedDataWithFloat64:
List java.util.ArrayList NSArray
Map java.util.HashMap NSDictionary

MethodChannel

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

Native 端程式碼

public class MainActivity extends FlutterActivity {
  private static final String CHANNEL = "com.example.flutter_battery/battery";
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
    new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
            (call, result) -> {
              // 在主執行緒中執行
              if (call.method.equals("getBatteryLevel")) {
                // 獲取電量
                int batteryLevel = fetchBatteryLevel();
                if (batteryLevel != -1) {
                  // 將電量返回給 Flutter 呼叫
                  result.success(batteryLevel);
                } else {
                  result.error("UNAVAILABLE", "Battery level not available.", null);
                }
              } else {
                result.notImplemented();
              }
            });
  }

  // 獲取電量的方法
  private int fetchBatteryLevel() {
    int batteryLevel;
    if (Build.VERSION.SDK_INT >= Build.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;
  }
}
複製程式碼

在 Native 程式碼中, 我們新建了一個 fetchBatteryLevel 函式來獲取電量, 然後 new 一個 MethodChannel 物件

這裡需要注意該建構函式的第二個引數 CHANNEL, 這個字串在稍後的 Flutter 中也要用到

最後為 MethodChannel 設定函式呼叫處理器 MethodCallHandler, 也就是 Flutter 呼叫 Native 函式的時候會回撥這個MethodCallHandler

Flutter 端程式碼

class _MyHomePageState extends State<MyHomePage> {
  // 建構函式引數就是上面 Android 的 CHANNEL 常量
  static const methodChannelBattery = const MethodChannel('com.example.flutter_battery/battery');

  String _batteryLevel = 'Unknown battery level.';

  Future<void> _getBatteryLevel() async {
    String batteryLevel;
    try {
      // invokeMethod('getBatteryLevel') 會回撥 MethodCallHandler
      final int result = await methodChannelBattery.invokeMethod('getBatteryLevel');
      batteryLevel = 'Battery level at $result % .';
    } on PlatformException catch (e) {
      batteryLevel = "Failed to get battery level: '${e.message}'.";
    } on MissingPluginException catch (e) {
      batteryLevel = "plugin undefined";
    }
    setState(() {
      _batteryLevel = batteryLevel;
    });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        margin: EdgeInsets.only(left: 10, top: 10),
        child: Center(
          child: Column(
            children: [
              Row(
                children: <Widget>[
                  RaisedButton(
                    child: Text(
                      'GetBatteryFromNative',
                      style: TextStyle(fontSize: 12),
                    ),
                    onPressed: _getBatteryLevel,
                  ),
                  Padding(
                    padding: EdgeInsets.only(left: 10),
                    child: Text(_batteryLevel),
                  )
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }
}
複製程式碼

點選 Flutter 介面上的按鈕就可以獲取到 Android 手機裡的電量了:

0

MethodChannel 除了使用實現 Flutter 呼叫 Native 函式, 也可以 Native 呼叫 Flutter 函式

首先要在 Native 端呼叫 invokeMethod 方法, 指定你要呼叫哪個 Flutter 方法:

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

        if (batteryLevel != -1) {
            result.success(batteryLevel);
        } else {
            result.error("UNAVAILABLE", "Battery level not available.", null);
        }
    } else {
        result.notImplemented();
    }
    
    // Native 呼叫 Flutter 的 getFlutterContent 函式
    channel.invokeMethod("getFlutterContent", null, new MethodChannel.Result() {
        @Override
        public void success(Object o) {
            Log.e("BatteryPlugin", "Dart getFlutterContent() result : " + o);
        }

        @Override
        public void error(String s, String s1, Object o) {
            Log.e("BatteryPlugin", "Dart getFlutterContent() error : " + s);
        }

        @Override
        public void notImplemented() {
            Log.e("BatteryPlugin", "Dart getFlutterContent() notImplemented");

        }
    });
}
複製程式碼

然後在 Flutter 中設定 MethodChannelMethodCallHandler, 也就是說 Native 呼叫了 invokeMethod 方法後, Flutter 怎麼處理:

void initState() {
    super.initState();
    methodChannelBattery.setMethodCallHandler(batteryCallHandler);
}

Future<dynamic> batteryCallHandler(MethodCall call) async {
    switch (call.method) {
      case "getFlutterContent":
        return "This is FlutterContent";
    }
}
複製程式碼

上面程式碼的主要意思是, 當我們點選按鈕呼叫 Native 裡的函式獲取電量, 然後在 Native 中立馬呼叫 Flutter 中的 getFlutterContent 函式

然後控制檯就會輸出, 我們從 Flutter getFlutterContent() 的返回值:

Dart getFlutterContent() result : This is FlutterContent
複製程式碼

EventChannel

EventChannel 適用於事件流的通訊, 例如 Native 需要頻繁的傳送訊息給 Flutter, 比如監聽網路狀態, 藍芽裝置等等然後傳送給 Flutter

下面我們以一個案例來介紹 EventChannel 的使用, 該案例是在 Native 中每秒傳送一個事件給 Flutter:

Native 端程式碼

public class EventChannelPlugin implements EventChannel.StreamHandler {

    private Handler handler;
    private static final String CHANNEL = "com.example.flutter_battery/stream";
    private int count = 0;

    public static void registerWith(PluginRegistry.Registrar registrar) {
        // 新建 EventChannel, CHANNEL常量的作用和 MethodChannel 一樣的
        final EventChannel channel = new EventChannel(registrar.messenger(), CHANNEL);
        // 設定流的處理器(StreamHandler)
        channel.setStreamHandler(new EventChannelPlugin());
    }

    @Override
    public void onListen(Object o, EventChannel.EventSink eventSink) {
        // 每隔一秒數字+1
        handler = new Handler(message -> {
            // 然後把數字傳送給 Flutter
            eventSink.success(++count);
            handler.sendEmptyMessageDelayed(0, 1000);
            return false;
        });
        handler.sendEmptyMessage(0);

    }

    @Override
    public void onCancel(Object o) {
        handler.removeMessages(0);
        handler = null;
        count = 0;
    }
}
複製程式碼

Flutter 端程式碼

class _MyHomePageState extends State<MyHomePage> {
  // 建立 EventChannel
  static const stream = const EventChannel('com.example.flutter_battery/stream');

  int _count = 0;

  StreamSubscription _timerSubscription;

  void _startTimer() {
    if (_timerSubscription == null)
       // 監聽 EventChannel 流, 會觸發 Native onListen回撥
      _timerSubscription = stream.receiveBroadcastStream().listen(_updateTimer);
  }

  void _stopTimer() {
    _timerSubscription?.cancel();
    _timerSubscription = null;
    setState(() => _count = 0);
  }

  void _updateTimer(dynamic count) {
    print("--------$count");
    setState(() => _count = count);
  }

  @override
  void dispose() {
    super.dispose();
    _timerSubscription?.cancel();
    _timerSubscription = null;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Container(
        margin: EdgeInsets.only(left: 10, top: 10),
        child: Center(
          child: Column(
            children: [
              Row(
                children: <Widget>[
                  RaisedButton(
                    child: Text('Start EventChannel',
                        style: TextStyle(fontSize: 12)),
                    onPressed: _startTimer,
                  ),
                  Padding(
                      padding: EdgeInsets.only(left: 10),
                      child: RaisedButton(
                        child: Text('Cancel EventChannel',
                            style: TextStyle(fontSize: 12)),
                        onPressed: _stopTimer,
                      )),
                  Padding(
                    padding: EdgeInsets.only(left: 10),
                    child: Text("$_count"),
                  )
                ],
              )
            ],
          ),
        ),
      ),
    );
  }
}
複製程式碼

效果如下圖所示:

EventChannel

BasicMessageChannel

BasicMessageChannel 更像是一個訊息的通訊, 如果僅僅是簡單的通訊而不是呼叫某個方法或者是事件流, 可以使用 BasicMessageChannel

BasicMessageChannel 也可以實現 FlutterNative 的雙向通訊, 下面的示例圖就是官方的例子:

(八)Flutter 和 Native 之間的通訊詳解
點選 Native FAB 通知 Flutter 更新, 點選 Flutter FAB 通知 Native 更新

Flutter端程式碼

class _MyHomePageState extends State<MyHomePage> {
  static const String _channel = 'increment';
  static const String _pong = 'pong';
  static const String _emptyMessage = '';
  static const BasicMessageChannel<String> platform =
      BasicMessageChannel<String>(_channel, StringCodec());

  int _counter = 0;

  @override
  void initState() {
    super.initState();
    // 設定訊息處理器
    platform.setMessageHandler(_handlePlatformIncrement);
  }

  // 如果接收到 Native 的訊息 則數字+1
  Future<String> _handlePlatformIncrement(String message) async {
    setState(() {
      _counter++;
    });
    // 傳送一個空訊息
    return _emptyMessage;
  }

  // 點選 Flutter 中的 FAB 則發訊息給 Native
  void _sendFlutterIncrement() {
    platform.send(_pong);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('BasicMessageChannel'),
      ),
      body: Container(
          child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Expanded(
            child: Center(
              child: Text(
                  'Platform button tapped $_counter time${_counter == 1 ? '' : 's'}.',
                  style: const TextStyle(fontSize: 17.0)),
            ),
          ),
          Container(
            padding: const EdgeInsets.only(bottom: 15.0, left: 5.0),
            child: Row(
              children: <Widget>[
                Image.asset('assets/flutter-mark-square-64.png', scale: 1.5),
                const Text('Flutter', style: TextStyle(fontSize: 30.0)),
              ],
            ),
          ),
        ],
      )),
      floatingActionButton: FloatingActionButton(
        onPressed: _sendFlutterIncrement,
        child: const Icon(Icons.add),
      ),
    );
  }
}

複製程式碼

Native端程式碼

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // 省略其他程式碼...
    
    messageChannel = new BasicMessageChannel<>(flutterView, CHANNEL, StringCodec.INSTANCE);
    messageChannel.
        setMessageHandler(new MessageHandler<String>() {
            @Override
            public void onMessage(String s, Reply<String> reply) {
                // 接收到Flutter訊息, 更新Native
                onFlutterIncrement();
                reply.reply(EMPTY_MESSAGE);
            }
        });

    FloatingActionButton fab = findViewById(R.id.button);
    fab.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            // 通知 Flutter 更新
            sendAndroidIncrement();
        }
    });
}

private void sendAndroidIncrement() {
    messageChannel.send(PING);
}

private void onFlutterIncrement() {
    counter++;
    TextView textView = findViewById(R.id.button_tap);
    String value = "Flutter button tapped " + counter + (counter == 1 ? " time" : " times");
    textView.setText(value);
}
複製程式碼

關於 FlutterNative 之間的通訊就介紹到這裡了. 總而言之, 如果通訊需要方法呼叫可以使用 MethodChannel, 通訊的時候用到資料流則使用 EventChannel, 如果僅僅是訊息通知則可以使用 BasicMessageChannel.

Reference

flutter.dev/docs/develo… juejin.im/post/5b84ff… juejin.im/post/5b4c3c… juejin.im/post/5b3ae6…

聯絡我

下面是我的公眾號,乾貨文章不錯過,有需要的可以關注下,有任何問題可以聯絡我:

公眾號:  chiclaim

相關文章