哈喽,我是老刘
前段时间有个朋友加我微信找我帮忙做一个项目
他是在校学生,一个网络安全方面的课题,需要做一个系统
其中包括Android、iOS端、Web端和服务端
他研究了一下现在的各种技术栈,发现Flutter是一个比较合适的选择

于是找到我们帮他实现这个系统
整个系统整体来说相对比较简单,其中有两点我觉得对很多学习Flutter开发的同学比较有启发
一个是这种需求的场景,使用Flutter同时实现Android、iOS、Web三个端是不是最优选择
二是其中有一个页面需要实现在同一个摄像头预览窗口内完成OCR和二维码识别

其中第一个问题我后续会单独写一篇文章来讨论
今天主要集中讨论第二个问题

为什么在一个预览界面中同时实现OCR和二维码识别这个看起来不复杂的问题值得拿出来讨论呢?
我觉得这里面涉及到了几个刚开始学习Flutter的同学都会涉及到的问题

1、Flutter如何调用系统功能

很多朋友在选择Flutter时担心碰到调用系统原生功能会比较麻烦
比如定位、电池信息、相机等
首先,从技术上来说
这些功能的实现必须要调用到系统的API
而这些API只有系统原生的SDK才提供
所以对Flutter这样的跨平台开发框架来说确实是没办法直接调用这些功能的

但是Flutter生态发展到今天
基本上大多数App能用到的系统原生功能,都已经有第三方库帮你实现好了
也就是说你现在开发一个纯Flutter的App,大概率不需要自己写代码调用这些原生功能了

其次,即使你真的碰到了低概率事件
比如你要的功能没有人实现过
或者已有的三方库没办法满足你的需求
那么通过Flutter提供的MethodChannel调用原生代码也是非常简单的

就拿这次要实现的OCR和二维码识别来举例,我们看看能不能找到可以直接使用的三方库

OCR

在pub上直接搜OCR
image.png

可以看到基本上前两个都能满足我们的需求
而且可以从这些库的描述中发现
这两个库都是对“google_mlkit_text_recognition”的封装
也就是说如果后续我们需要用到更底层一点的功能,也可以直接用google_mlkit_text_recognition”

二维码

在pub中直接搜“二维码”三个字大概率是找不到的
image.png
因为目前的pub上的库,基本都是英文描述
所以我们可以搜“qr”
image.png

这里需要稍微看一下描述
因为有些库只提供生成二维码的功能,注意别选错了

关于二维码扫描的库,客户指定了一个flutter_scankit
这种客户指定的情况,只要不是不能用,就绝不瞎哔哔
image.png

那么接下来,我们就尝试基于这两个库,去实现用户要求的在同一个界面实现OCR和二维码扫描功能

2、OCR和二维码扫描功能融合

这里其实就出现了我们开发中经常遇到的另一个问题
当三方库不能直接满足我们的要求时怎么办?
看一下两个库的使用方法就会发现
两个库的封装程度都很高
它们的默认用法都是直接把一个组件放到你的页面中,然后在回调中获取识别的结果
它们的摄像头预览内容都是在组件内直接封装好的
这样很难满足我们的要求
因为用户需要的是一个预览窗口实现多个识别

这时候我们就要看一下两个库的预览都是怎么实现的,看看有没有共用的可能
对于flutter_scankit来说,它的说明文档中有一个自定义用法
能明确看出来整个处理流程
image.png

我们直接来看flutter_scankit文档里给出的demo代码

class _BitmapModeState extends State<BitmapMode> {
  CameraController? controller;
  StreamSubscription? subscription;
  String code = '';
  ScanKitDecoder decoder = ScanKitDecoder(photoMode: false, parseResult: false);

  @override
  void initState() {
    availableCameras().then((val) {
      List<CameraDescription> _cameras = val;
      if (_cameras.isNotEmpty) {
        controller = CameraController(_cameras[0], ResolutionPreset.max);
        controller!.initialize().then((_) {
          if (!mounted) {
            return;
          }
          controller!.startImageStream(onLatestImageAvailable);
          setState(() {});
        });
      }
    });

    subscription = decoder.onResult.listen((event) async{
      if (event is ResultEvent && event.value.isNotEmpty()) {
        subscription!.pause();
        await stopScan();
        if (mounted) {
          setState(() {
            code = event.value.originalValue;
          });
        }
      } else if (event is ZoomEvent) {
        /// set zoom value
      }
    });
    super.initState();
  }

  void onLatestImageAvailable(CameraImage image) async {
    if(image.planes.length == 1 && image.format.group == ImageFormatGroup.bgra8888){
      await decoder.decode(image.planes[0].bytes, image.width, image.height);
    }else if(image.planes.length == 3){
      Uint8List y = image.planes[0].bytes;
      Uint8List u = image.planes[1].bytes;
      Uint8List v = image.planes[2].bytes;

      Uint8List combined = Uint8List(y.length + u.length + v.length);
      combined.setRange(0, y.length, y);
      combined.setRange(y.length, y.length + u.length, u);
      combined.setRange(y.length + u.length, y.length + u.length + v.length, v);
      await decoder.decodeYUV(combined, image.width, image.height);
    }
  }
}

原理其实很简单,这里稍作解释
首先使用camera库获取指定摄像头的预览
不熟悉camera库的同学可以去看一下说明
它一方面可以提供一个组件显示预览的内容
同时也能通过CameraController把预览的每一帧图片通过一个stream提供给使用者

controller!.startImageStream

flutter_scankit库提供了一个ScanKitDecoder类
从摄像头预览获取的图片可以传递给这个类进行识别
前面代码中的onLatestImageAvailable方法就是把预览图片转换格式后传递给ScanKitDecoder的
onLatestImageAvailable就是传递给controller!.startImageStream方法的回调

其实到这里整个流程已经比较清晰了
接下来我们只需要把预览图片也同时传递给ocr库就可以了
我们直接去看google_mlkit_text_recognition库的文档
它提供了传入图片进行ocr识别的功能

final RecognizedText recognizedText = await textRecognizer.processImage(inputImage);

String text = recognizedText.text;
for (TextBlock block in recognizedText.blocks) {
  final Rect rect = block.boundingBox;
  final List<Point<int>> cornerPoints = block.cornerPoints;
  final String text = block.text;
  final List<String> languages = block.recognizedLanguages;
  
  for (TextLine line in block.lines) {
    // Same getters as TextBlock
    for (TextElement element in line.elements) {
      // Same getters as TextBlock
    }
  }
}

剩下的事情就是一些收尾工作了
当ocr和二维码的识别都完成后关闭相机预览,以及展示结果

总结

这个功能点虽然不常见,但是实现起来并不难
主要需要花一点时间进行前期调研
我这里把整个调研过程如此详细的展示出来
主要有两个目的:
1、向大家展示一下目前Flutter生态的现状
目前Flutter生态的发展已经非常完善了
很多同学担心的需要去写原生代码的场景,现在大概率是不需要的
2、三方库的定制也没有想象中那么复杂
多数时候我们只需要研究一下SDK提供的更底层一点的API,然后组合一下这些API的调用就好了
真正需要去跟踪三方库源代码,然后找到自己实现方案的场景
说实话我也很久没有碰到过了

好了,老刘用自己最近开发的一个项目中的一个很具体的功能点
从一个很细微的角度向大家展示了Flutter生态的现状
对Flutter感兴趣或者存有疑虑的同学都欢迎联系我讨论

如果看到这里的同学有学习Flutter的兴趣,欢迎联系老刘,我们互相学习。
点击免费领老刘整理的《Flutter开发手册》,覆盖90%应用开发场景。
可以作为Flutter学习的知识地图。
覆盖90%开发场景的《Flutter开发手册》


程序员老刘
1 声望2 粉丝

客户端架构师,客户端团队负责人。一个月带领客户端团队从0基础迁移到Flutter 。目前团队已使用Flutter五年。