Channel通道的界说与完成

前言

需求是这个姿态的,运用需求搜集用户的头像,这个简略,直接用官方的 Camera 插件摄影即可。

可是有些用户会传递一些非人脸的图片,或多人脸的图片,导致事务无法继续,所以需求移动端在搜集人脸的一起校验人脸数量。

详细作用如下:

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

如何完成?其实也简略,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,界说它的通道名与办法名。

界说如下:

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

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

那么在Android中的完成呢?

MethodChannel的构建需求两个参数,一个是BinaryMessenger,一般从Flutter Engine中获取,能够经过普通的Engine构建,也能够经过EngineCache预热引擎来获取,当然也能够运用EngineGroup来获取,假如在FlutterActivity里边,能够直接在configureFlutterEngine回调中获取。另一个参数是name,用于标识这个Channel。

详细的完成如下:

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完成的人脸检测,代码如下:

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还要简略,详细代码如下:

@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如下:

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

1.2 EventChannel完成示例

咱们以监听原生渠道的重力加速度传感器的值为例,把原生渠道的数据以 Stream 的办法传递给 Flutter 端。

先界说一个目标用于传递

class AccelerometerReadings {
  final double x;
  final double y;
  final double z;
  AccelerometerReadings(this.x, this.y, this.z);
}

Flutter的代码完成如下:

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端的完成:

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))
    }
}

详细传感器的代码完成:

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 目标,指明通道名,并发送数据:

    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() 。

那么在原生中的完成:

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 端的两个通道,代码如下:

    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 端的两个通道,代码如下:

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");
        }
    }
}

作用如下:

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

当然这是 Demo 作用,实在场景会把 BasicMessageChannel 抽取出来依据详细逻辑判别是运用 reply 还是运用主动的 send 来进行音讯的传递。

三、自界说插件的主动完成

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

网上的博客文章或许一些AI东西会告诉你 Channel 是这么写的,需求完成 FlutterPlugin 接口,相似如下:

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

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 端代码:

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的代码:

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 文件中引进自己的本地插件:

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

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