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,这一期就此完结。

相关推荐
666xiaoniuzi1 小时前
深入理解 C 语言中的内存操作函数:memcpy、memmove、memset 和 memcmp
android·c语言·数据库
Python私教5 小时前
Flutter组件化开发
flutter
沐言人生6 小时前
Android10 Framework—Init进程-8.服务端属性文件创建和mmap映射
android
沐言人生6 小时前
Android10 Framework—Init进程-9.服务端属性值初始化
android·android studio·android jetpack
沐言人生6 小时前
Android10 Framework—Init进程-7.服务端属性安全上下文序列化
android·android studio·android jetpack
追光天使6 小时前
【Mac】和【安卓手机】 通过有线方式实现投屏
android·macos·智能手机·投屏·有线
helloxmg6 小时前
鸿蒙harmonyos next flutter混合开发之开发FFI plugin
flutter
小雨cc5566ru6 小时前
uniapp+Android智慧居家养老服务平台 0fjae微信小程序
android·微信小程序·uni-app
一切皆是定数7 小时前
Android车载——VehicleHal初始化(Android 11)
android·gitee
一切皆是定数7 小时前
Android车载——VehicleHal运行流程(Android 11)
android