Flutter开发必须掌握的Channel通道以及不同的定义方式

Channel通道的定义与实现

前言

需求是这个样子的,应用需要收集用户的头像,这个简单,直接用官方的 Camera 插件拍照即可。

但是有些用户会传递一些非人脸的图片,或多人脸的图片,导致业务无法继续,所以需要移动端在收集人脸的同时校验人脸数量。

具体效果如下:

如何实现?其实也简单,Pub里面有很多开源的框架。比较出名的例如 google_mlkit_face_detection 支持 Android 与 iOS ,本身也是很优秀的框架了,由于部分原因并没有选择第三方的框架。

换个方向,类似的功能其实各自的原生API已经有对应的实现,我们直接自己写Channel不是就可以了吗?不需要重复导一个比较重的库去实现这一个相对简单的功能。

那么如何定义与使用Channel呢?

一、Channel的类型与实现

我们常说的 Channel 全名叫 Platform Channel,它是Flutter和原生通信的工具,有三种类型:

  1. MethodChannel:用于传递方法调用(method invocation),Flutter和平台端进行直接方法调用时候可以使用。
  2. BasicMessageChannel:用于传递字符串和半结构化的信息,Flutter和平台端进行消息数据交换时候可以使用。
  3. EventChannel:用于数据流(event streams)的通信,Flutter和平台端进行事件监听、取消等可以使用。

官方Demo在此【传送门】

简单的理解:

  1. MethodChannel: Flutter可以通过它调用原生方法,具体的实现由各自原生平台实现,这种方案也是最常用的。

  2. EventChannel: 用于在事件流中将消息传递给Flutter端,常用于原生平台的监听数据(比如传感器)传递给Flutter端展示或处理。

  3. BasicMessageChannel:是一种简单的双向消息通信渠道,它允许Flutter和原生平台通过字符串或字节流发送消息,并返回一个响应,它是最基础的可以实现 MethodChannel 和 EventChannel 的功能。

1.1 MethodChannel 实现示例

这里以我们上面说的人脸数量检测为例,我们现在Flutter中定义对应的MethodChannel,定义它的通道名与方法名。

定义如下:

ini 复制代码
final _platform = MethodChannel('face_detection');

String response = await _platform.invokeMethod('checkFace');

当然一般我们会封装在一个类中便于统一管理。

那么在Android中的实现呢?

MethodChannel的构建需要两个参数,一个是BinaryMessenger,通常从Flutter Engine中获取,可以通过普通的Engine构建,也可以通过EngineCache预热引擎来获取,当然也可以使用EngineGroup来获取,如果在FlutterActivity里面,可以直接在configureFlutterEngine回调中获取。另一个参数是name,用于标识这个Channel。

具体的实现如下:

kotlin 复制代码
class MainActivity : FlutterActivity() {

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(flutterEngine.dartExecutor, "face_detection")
            .setMethodCallHandler { call, result ->

                if (call.method == "checkFace") {
                    val imagePath = call.arguments as String
                    val faceCount = detectFaces(imagePath)
                    result.success(faceCount)
                } else {
                    result.notImplemented()
                }

            }
    }

    private fun detectFaces(imagePath: String): Int {
        return CheckFaceUtils.checkFace(imagePath)
    }

}

CheckFaceUtils:Android原生API实现的人脸检测,代码如下:

arduino 复制代码
public class CheckFaceUtils {

    public static Bitmap rotateBitmapIfNeeded(Bitmap bitmap) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();

        // 判断是否需要旋转
        if (width > height) {
            Matrix matrix = new Matrix();
            matrix.postRotate(270); // 旋转90度
            return Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);
        } else {
            return bitmap;
        }
    }

    /**
     * 检查BitMap中包含的人脸数量
     */
    public static int checkFace(String imagePath) {

        Bitmap b = BitmapFactory.decodeFile(imagePath);
        if (b != null) {
            //处理横竖Bitmap的旋转
            b = rotateBitmapIfNeeded(b);

            // 检测前必须转化为RGB_565格式。文末有详述连接
            Bitmap bitmap = b.copy(Bitmap.Config.RGB_565, true);
            b.recycle();
            // 设置你想检测的数量,数值越大错误率越高,所以需要置信度来判断,但有时候置信度也会出问题
            int MAX_FACES = 5; // I found it can detect number of face at least 27,
            FaceDetector faceDet = new FaceDetector(bitmap.getWidth(), bitmap.getHeight(), MAX_FACES);
            // 将人脸数据存储到faceArray中
            FaceDetector.Face[] faceArray = new FaceDetector.Face[MAX_FACES];
            // 返回找到图片中人脸的数量,同时把返回的脸部位置信息放到faceArray中,过程耗时,图片越大耗时越久
            int findFaceNum = faceDet.findFaces(bitmap, faceArray);
            Log.w("FaceSDKUtils", "找到脸部数量:" + findFaceNum);
            bitmap.recycle();
            return findFaceNum;
        } else {
            Log.w("FaceSDKUtils", "目标文件不是图片,无法获取Bitmap");
            return -1;
        }

    }
}

iOS 的实现,也是类似的思路,只是人脸检测的代码比Android还要简单,具体代码如下:

swift 复制代码
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    
    var channel:FlutterMethodChannel!
    
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
      self.initPlatformMethods()
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
    func initPlatformMethods(){
        self.channel = FlutterMethodChannel.init(name: "face_detection", binaryMessenger: self.window.rootViewController as! FlutterBinaryMessenger)
        self.channel.setMethodCallHandler { call, result in
            if (call.method == "checkFace"){
                
                result(self.checkFace(path: call.arguments as! String));
            }
        }
    }
    func checkFace(path:String) -> Int{
        var image = CIImage.init(image: .init(contentsOfFile: path)!)
        var detector = CIDetector.init(ofType: CIDetectorTypeFace, context: nil, options: [CIDetectorAccuracy:CIDetectorAccuracyHigh])
        var features = detector!.features(in: image!);
        return features.count;
    }
}

它是在应用初始化的时候就注册了。

Log如下:

1.2 EventChannel实现示例

我们以监听原生平台的重力加速度传感器的值为例,把原生平台的数据以 Stream 的方式传递给 Flutter 端。

先定义一个对象用于传递

kotlin 复制代码
class AccelerometerReadings {

  final double x;

  final double y;

  final double z;

  AccelerometerReadings(this.x, this.y, this.z);
}

Flutter的代码实现如下:

vbnet 复制代码
child: StreamBuilder<AccelerometerReadings>(
  stream: EventChannel('eventChannelDemo').receiveBroadcastStream().map(
          (dynamic event) => AccelerometerReadings(
            event[0] as double,
            event[1] as double,
            event[2] as double,
          ),
        ),
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return Text((snapshot.error as PlatformException).message!);
    } else if (snapshot.hasData) {
      return Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(
            'x轴: ' + snapshot.data!.x.toStringAsFixed(3),
            style: textStyle,
          ),
          Text(
            'y轴: ' + snapshot.data!.y.toStringAsFixed(3),
            style: textStyle,
          ),
          Text(
            'z轴: ' + snapshot.data!.z.toStringAsFixed(3),
            style: textStyle,
          )
        ],
      );
    }

当然了,这是简单的使用,其实一般都是封装到一个类中便于统一管理。

Android端的实现:

kotlin 复制代码
class MainActivity : FlutterActivity() {

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

       val sensorManger: SensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager

       val accelerometerSensor: Sensor = sensorManger.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)

       EventChannel(flutterEngine.dartExecutor, "eventChannelDemo").setStreamHandler(AccelerometerStreamHandler(sensorManger, accelerometerSensor))
    }


}

具体传感器的代码实现:

kotlin 复制代码
class AccelerometerStreamHandler(sManager: SensorManager, s: Sensor) : EventChannel.StreamHandler, SensorEventListener {
    private val sensorManager: SensorManager = sManager
    private val accelerometerSensor: Sensor = s
    private lateinit var eventSink: EventChannel.EventSink

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
        if (events != null) {
            eventSink = events
            sensorManager.registerListener(this, accelerometerSensor, SensorManager.SENSOR_DELAY_UI)
        }
    }

    override fun onCancel(arguments: Any?) {
        sensorManager.unregisterListener(this)
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}

    override fun onSensorChanged(sensorEvent: SensorEvent?) {
        if (sensorEvent != null) {
            val axisValues = listOf(sensorEvent.values[0], sensorEvent.values[1], sensorEvent.values[2])
            eventSink.success(axisValues)
        } else {
            eventSink.error("DATA_UNAVAILABLE", "Cannot get accelerometer data", null)
        }
    }
}

1.3 BasicMessageChannel 实现示例

之前我们都是演示的 Flutter 拿原生平台的数据,这里我们以原生 Android 平台拿 Flutter 的数据为例,演示Android平台拿到 Flutter 项目中的图片资源。

当然只是示例啊,真实项目很少这么干...

同样的先在 Flutter 端先定义 BasicMessageChannel 对象,指明通道名,并发送数据:

ini 复制代码
    final channelToAndroid = BasicMessageChannel<ByteData>(
      'image_data_from_flutter',
      BinaryCodec(),
    );

    // 获取assets中的图片对应的ByteData数据,并发送给原生
    rootBundle.load(Assets.imagesBlackBack).then((value) async {

      ByteData? res = await channelToAndroid.send(value);
      Log.d('res :$res');

      if (res != null) {
        // 将 ByteData 转换为字节数组
        Uint8List bytes = res.buffer.asUint8List();
        // 将字节数组转换为字符串
        String stringData = utf8.decode(bytes);
        // 在控制台输出接收到的字符串
        Log.d('Received string from Android: $stringData');
      }

    });

需要注意的是,这里使用 BinaryCodec,数据格式为 ByteData,如果是想传送 String 字符串类型,那么就可以指定为 StringCodec() 。

那么在原生中的实现:

kotlin 复制代码
class MainActivity : FlutterActivity() {

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        BasicMessageChannel<ByteBuffer>(
            flutterEngine.dartExecutor, "image_data_from_flutter", BinaryCodec.INSTANCE
        ).setMessageHandler { message, reply ->

            //转换为Android需要的ByteArray
            val byteBuffer = message as ByteBuffer
            val imageByteArray = ByteArray(byteBuffer.capacity())
            byteBuffer.get(imageByteArray)
            Log.d("TAG","imageByteArray:$imageByteArray")

            //收到之后如果想回复给Flutter端,也可以加一下代码
            val str = "感谢Flutter老哥送上的图片数据"
            val strBytes = str.toByteArray(Charset.forName("UTF-8"))
            val byteBuffer2 = ByteBuffer.allocateDirect(strBytes.size)
            byteBuffer2.put(strBytes)
            byteBuffer2.flip()
            reply.reply(byteBuffer2)
        }

    }

}

这样就可以完成一个简单的类似请求与响应的效果,那么如何做到双端通信呢?

其实我们在原生端创建了两个 BasicMessageChannel 对象,分别用于从 Flutter 端接收数据(channelFromFlutter)和向 Flutter 端发送数据(channelToFlutter)。在 Flutter 端也创建了两个相应的 BasicMessageChannel 对象,用于和原生端进行双向通信。

我们可以先定义 Flutter 端的两个通道,代码如下:

dart 复制代码
    final channelFromAndroid = BasicMessageChannel<String>(
      'image_data_to_flutter',
      StringCodec(),
    );

    final channelToAndroid = BasicMessageChannel<String>(
      'image_data_from_flutter',
      StringCodec(),
    );

    // 获取assets中的图片对应的ByteData数据,并发送给原生
    String? res = await channelToAndroid.send('我是来自Flutter的字符串');
    Log.d('收到来自Android的Reply :$res');


    channelFromAndroid.setMessageHandler((receivedData) async {
      // 在这里处理来自 Android 端的数据
      Log.d('收到来自Android发过来的数据: $receivedData');

      return "收到了感谢Android老铁发来得到数据";
    });

下面就是定义 Android 端的两个通道,代码如下:

kotlin 复制代码
class MainActivity: FlutterActivity() {

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        BasicMessageChannel(
            flutterEngine.dartExecutor, "image_data_from_flutter", StringCodec.INSTANCE
        ).setMessageHandler { message, reply ->

            //转换为Android需要的ByteArray
            Log.d("TAG","收到来自Flutter的字符串:$message")

            //收到之后如果想回复给Flutter端,也可以加一下代码
            reply.reply("感谢Flutter老哥送来的数据,已经收到了")
        }

        BasicMessageChannel(
            flutterEngine.dartExecutor, "image_data_to_flutter", StringCodec.INSTANCE
        ).send("这个字符串是我主动Send给Flutter的,你能收到吗?") {

            Log.d("TAG", "Flutter的回复收到了:$it");
        }

    }

}

效果如下:

当然这是 Demo 效果,真实场景会把 BasicMessageChannel 抽取出来根据具体逻辑判断是使用 reply 还是使用主动的 send 来进行消息的传递。

三、自定义插件的自动实现

对于三种 Channel 我们都已经了解了,都是在我们自己项目中手动注册的,为什么我看一些第三方的插件都没有 FlutterActivity ,他们都没有手动注册,他们是怎么实现 Channel 的注册的?

网上的博客文章或者一些AI工具会告诉你 Channel 是这么写的,需要实现 FlutterPlugin 接口,类似如下:

kotlin 复制代码
class FaceDetectionChannel : FlutterPlugin, MethodChannel.MethodCallHandler {

    private var context: Context? = null
    private var channel: MethodChannel? = null

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        if (call.method == "detectFaces") {
            val imagePath = call.arguments as String
            val faceCount = detectFaces(imagePath) // 调用你的人脸检测方法,返回人脸数量
            result.success(faceCount)
        } else {
            result.notImplemented()
        }
    }

    private fun detectFaces(imagePath: String): Int {
        return FaceSDKUtils.checkFace(imagePath)
    }

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {

        channel = MethodChannel(binding.binaryMessenger, "face_detection_channel")
        channel?.setMethodCallHandler(this)
        context = binding.applicationContext
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel?.setMethodCallHandler(null)
    }

}

但是这样并不能真正的注册,这种是插件的写法,当我们在 pubspec.yaml 文件中依赖一个插件的时候,插件中指定了 pluginClass,会自动调用 onAttachedToEngine 方法,所以它才能达到'自动初始化'的效果。

如果是在自己的项目中写 Channel 则大可不必这么写,直接在 FlutterActivity 或 FlutterApplication 中手动注册即可。

那么我就想写一个这样的,以本地插件的形式导入到项目行不行?当然可以。

第一步我们需要在本地插件的 pubspec.yaml 中指定 plgun

yaml 复制代码
name: face_detect
description: 找到Path中人脸数量
version: 0.0.1
homepage:

environment:
  sdk: '>=3.0.2 <4.0.0'
  flutter: ">=1.20.0"

dependencies:
  platform: ^3.0.0
  flutter:
    sdk: flutter

dev_dependencies:
  test: ^1.17.4
  mockito: ^5.0.7

# The following section is specific to Flutter.
flutter:
  plugin:
    platforms:
      android:
        package: com.newki.facedetect
        pluginClass: FaceDetectionChannel
      ios:
        pluginClass: FaceDetectionChannel

在lib中只需要定义 Channel 的 Flutter 端代码:

dart 复制代码
class FaceDetectionChannel {
  //初始化MethodChannel对象
  static const MethodChannel _channel = MethodChannel('face_detection_channel');

  /// 各种平台自行实现检测人脸数量
  static Future<int> detectFaces(String imagePath) async {
    try {
      Log.d('Flutter -> detectFaces -> imagePath:$imagePath');
      final int faceCount = await _channel.invokeMethod('detectFaces', imagePath);
      return faceCount;
    } on PlatformException catch (e) {
      Log.e(e.message ?? 'detectFaces->未知异常');
      return -1;
    }
  }
}

Android 的具体实现上面已经给出,下面是iOS的代码:

swift 复制代码
import Flutter

class FaceDetectionChannel {
  static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "face_detection_channel", binaryMessenger: registrar.messenger())
    let instance = FaceDetectionChannel()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  func detectFaces(imagePath: String) -> Int {
    let image = CIImage(contentsOfURL: URL(fileURLWithPath: imagePath))
    let detector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: [CIDetectorAccuracy: CIDetectorAccuracyHigh])
    let features = detector?.features(in: image)
    return features?.count ?? 0
  }
}

extension FaceDetectionChannel: FlutterPlugin {
  static func register(with registrar: FlutterPluginRegistrar) {
    FaceDetectionChannel.register(with: registrar)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    if call.method == "detectFaces" {
      let arguments = call.arguments as? [String: Any]
      let imagePath = arguments?["imagePath"] as? String ?? ""
      let faceCount = detectFaces(imagePath: imagePath)
      result(faceCount)
    } else {
      result(FlutterMethodNotImplemented)
    }
  }
}

使用的时候直接在自己的项目的 pubspec.yaml 文件中引入自己的本地插件:

yaml 复制代码
  face_detect:
    path: face_detect_plugin

这样就可以实现自动 Channel 的注册与实现了。当然你也可以把这个插件发布到Pub 上,这样就可以开源出去供其他人使用了,如何发布项目到 Pub? 很多教程自己可以搜索一下并不复杂...

总结

本文总结了三种 Channel 的具体使用示例,并且说明了自己项目中的 Channel 与插件中的 Channel 初始化的不同方式。

为什么我们一定要掌握 Channel 的使用?

  1. 平台特定功能的调用:在跨平台开发中,某些平台特有的功能可能无法直接使用 Flutter 提供的现成解决方案。这时,您可以通过 Channel 实现与原生代码的通信,调用平台特定的功能。

  2. 性能优化:有时候,一些性能要求较高的任务可能需要在原生代码中执行。通过使用 Channel,您可以将这些任务委托给原生层,从而提升应用的性能和响应速度。

  3. 第三方库支持:尽管 Flutter 生态圈非常强大,但仍然有一些功能不可或缺的第三方库可能没有对应的 Flutter 插件。通过 Channel,您可以轻松地集成和使用这些第三方库,拓宽了应用的功能范围。

  4. 访问硬件功能:某些硬件功能(如相机、传感器等)可能需要直接与原生平台进行交互。通过 Channel,您可以调用原生平台的 API 来实现对硬件功能的访问和控制。

  5. 多平台适配:未来,随着 Flutter 对其他平台的支持增加,例如鸿蒙 App,您掌握 Channel 的使用也会在适配其他平台时非常有帮助,让您更快速地实现对应平台的功能。

等等总之,掌握 Channel 的使用能够让开发者更灵活、高效地进行跨平台开发。除了处理特定功能和第三方库的问题,还有一些其他场景,例如处理设备传感器、操作文件系统等,也可以使用 Channel 来实现。因此,建议在需要跨平台功能和性能优化时深入学习和掌握 Channel 的使用。

那么本期内容就到这里,如讲的不到位或错漏的地方,希望同学们可以评论区指出。

由于代码比较简单,本文全部贴出,如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力啦。

Ok,这一期就此完结。

相关推荐
无极程序员1 小时前
PHP常量
android·ide·android studio
萌面小侠Plus2 小时前
Android笔记(三十三):封装设备性能级别判断工具——低端机还是高端机
android·性能优化·kotlin·工具类·低端机
慢慢成长的码农2 小时前
Android Profiler 内存分析
android
大风起兮云飞扬丶2 小时前
Android——多线程、线程通信、handler机制
android
君蓦2 小时前
Flutter 本地存储与数据库的使用和优化
flutter
L72562 小时前
Android的Handler
android
清风徐来辽2 小时前
Android HandlerThread 基础
android
HerayChen3 小时前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野3 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java