如何为使用Flutter和Dynamoft Vision SDK的最后一英里交付应用构建原型
#网络开发人员 #android #flutter #ios

最后一英里的交付应用程序是一种软件应用程序,用于管理,协调和跟踪从运输中心到其最终目的地的商品交付,这通常是个人住所。这通常是整个交付过程中最复杂的部分,因为它涉及到居民区,处理流量,确保产品安全,及时地交付产品并确认成功交付。随着电子商务的兴起以及对快速有效的房屋交付的期望,对最后一英里交付应用程序的需求大大增加。在本文中,我们将向您展示如何使用Flutter and Dynamoft Vision SDK(Dynamsoft Barcode ReaderDynamsoft Label RecognizerDynamsoft Document Normalizer)构建最后一英里交付应用程序的原型。使用此原型,您可以尝试该应用程序的设计和功能,并将其用作开发自己的最后一英里交付应用的起点。

为什么要扑打和Dynamoft Vision SDK?

  • Flutter:我们的目标是为桌面,移动和网络构建一个应用程序。 Flutter是一个跨平台UI工具包,可让您轻松地从单个代码库构建应用程序。有效的是,仅需要DART代码才能为多个平台构建UI。 Flutter还拥有大型开发人员社区和各种各样的第三方套餐,可用于为您的应用添加其他功能。
  • DynamSoft Vision SDK:DynamSoft Vision SDK是一组软件开发套件,可为条形码扫描,MRZ识别和文档处理提供API。它们可用于 Windows linux macos android ios Web 平台。 DynamSoft Vision SDK的扑动插件包括flutter_barcode_sdkflutter_ocr_sdkflutter_document_scan_sdk。它们使您可以轻松地将Dynamoft Vision SDK集成到您的Flutter应用程序中。

应用设计和工作流程

通过单击下面的链接查看应用程序设计。

https://xd.adobe.com/view/7bbceea3-74e8-4013-80bc-0565dea8cc52-2eef/

应用程序的基本工作流量如下:

  1. 启动应用程序:在您的设备上启动应用程序。这将带您直接进入注册页面。

  2. 注册或登录:如果您是新用户,请通过注册创建一个新帐户。如果您是现有用户,请登录您的帐户。

  3. 配置文件验证:一旦您注册或签名,您将被定向到个人资料页面。此时,您的个人资料尚未验证。要验证您的个人资料,请单击按钮打开相机。

  4. 扫描许可证或护照:使用相机扫描驾驶执照或护照。这将为个人资料验证提供必要的个人信息。

  5. 个人资料验证过程:扫描许可证或护照后,您的个人资料将通过验证过程。

  6. 导航到订单页面:一旦您的配置文件得到验证,您将被定向到订单页面。在这里,您可以看到分配给您的订单。

  7. 扫描订单条形码:要获取订单的信息,请扫描订单的条形码。

  8. 扫描文档并交付订单:扫描订单的必要文档,然后单击按钮输送订单。

  9. 返回订单页:交付订单后,您将被直接回到订单页面,您可以继续下一个订单。

worflow

开发核心功能

在随后的部分中,我们将讨论如何开发应用程序的核心功能,包括相机集成,条形码扫描,MRZ识别,文档扫描和数据存储管理。

如何获取相机流图像并构建相机预览小部件

我们使用相机插件获取相机流图像,这对于条形码扫描,MRZ识别和文档扫描至关重要。官方的相机插件提供了startImageStream()方法,可为 Android 和 ios 平台提供相机流。对于 Web 应用程序,可以将其takePicture()方法用于连续捕获Blob类型的图像。 camera_windows插件目前正在开发,尚未支持图像流。但是,可以在https://github.com/yushulx/flutter_camera_windows.git上访问Windows摄像头插件的修改版本。因此,pubspec.yaml文件应如下更新:

dependencies:
    camera: ^0.10.5+2
    camera_windows: 
        git:
            url: https://github.com/yushulx/flutter_camera_windows.git

适用于各个平台的相机代码可以合并到一个文件中:

void initState() {
    initCamera();
}

Future<void> initCamera() async {
    try {
        WidgetsFlutterBinding.ensureInitialized();
        _cameras = await availableCameras();
        if (_cameras.isEmpty) return;

        toggleCamera(0);
    } on CameraException catch (e) {
        print(e);
    }
}

Future<void> toggleCamera(int index) async {
    if (controller != null) controller!.dispose();

    controller = CameraController(_cameras[index], ResolutionPreset.medium);
    controller!.initialize().then((_) {
      if (!cbIsMounted()) {
        return;
      }

      previewSize = controller!.value.previewSize;

      startVideo();
    }).catchError((Object e) {
      if (e is CameraException) {
        switch (e.code) {
          case 'CameraAccessDenied':
            break;
          default:
            break;
        }
      }
    });
}

Future<void> startVideo() async {
    if (kIsWeb) {
      webCamera();
    } else if (Platform.isAndroid || Platform.isIOS) {
      mobileCamera();
    } else if (Platform.isWindows) {
      _frameAvailableStreamSubscription?.cancel();
      _frameAvailableStreamSubscription =
          (CameraPlatform.instance as CameraWindows)
              .onFrameAvailable(controller!.cameraId)
              .listen(_onFrameAvailable);
    }
}

// web
Future<void> webCamera() async {
    if (controller == null || isFinished) return;

    XFile file = await controller!.takePicture();
    // TODO
    if (!isFinished) {
      webCamera();
    }
}

// Mobile
Future<void> mobileCamera() async {
    await controller!.startImageStream((CameraImage availableImage) async {
        // TODO
    });
  }

// Windows
void _onFrameAvailable(FrameAvailabledEvent event) {
    // TODO    
}

构造摄像头预览小部件时,如果摄像机预览出现镜像,则可以使用Transform小部件水平翻转预览。

Widget getPreview() {

    if (kIsWeb) {
      return Transform(
        alignment: Alignment.center,
        transform: Matrix4.identity()..scale(-1.0, 1.0), // Flip horizontally
        child: CameraPreview(controller!),
      );
    }

    return CameraPreview(controller!);
}

为了在全屏中渲染相机预览和覆盖,我们使用了StackPositionedFittedBoxSizedBox窗口小部件的组合。

Stack(
    children: <Widget>[
        if (_mobileCamera.controller != null &&
            _mobileCamera.previewSize != null)
        Positioned(
            top: 0,
            right: 0,
            left: 0,
            bottom: 0,
            child: FittedBox(
            fit: BoxFit.cover,
            child: Stack(
                children: createCameraPreview(),
            ),
            ),
        ),
    ],
),

List<Widget> createCameraPreview() {
    if (_mobileCamera.controller != null && _mobileCamera.previewSize != null) {
      return [
        SizedBox(
            width: MediaQuery.of(context).size.width <
                    MediaQuery.of(context).size.height
                ? _mobileCamera.previewSize!.height
                : _mobileCamera.previewSize!.width,
            height: MediaQuery.of(context).size.width <
                    MediaQuery.of(context).size.height
                ? _mobileCamera.previewSize!.width
                : _mobileCamera.previewSize!.height,
            child: _mobileCamera.getPreview()),
        Positioned(
          top: 0.0,
          right: 0.0,
          bottom: 0,
          left: 0.0,
          child: createOverlay(_mobileCamera.barcodeResults,
              _mobileCamera.mrzLines, _mobileCamera.documentResults),
        )
      ];
    } else {
      return [const CircularProgressIndicator()];
    }
}

如何将Dynamoft Vision SDK集成到Flutter应用程序中

  1. 将以下依赖项添加到pubspec.yaml文件:

    flutter_barcode_sdk: ^2.2.2
    flutter_document_scan_sdk: ^1.0.2
    flutter_ocr_sdk: ^1.1.0
    
  2. https://www.dynamsoft.com/customer/license/trialLicense申请Dynamoft Vision SDK的试用许可。

  3. 使用许可键初始化SDK:

    FlutterBarcodeSdk barcodeReader = FlutterBarcodeSdk();
    FlutterOcrSdk mrzDetector = FlutterOcrSdk();
    FlutterDocumentScanSdk docScanner = FlutterDocumentScanSdk();
    
    Future<void> initBarcodeSDK() async {
    await barcodeReader.setLicense(
        'LICENSE-KEY');
    await barcodeReader.init();
    await barcodeReader.setBarcodeFormats(BarcodeFormat.ALL);
    }
    
    Future<void> initMRZSDK() async {
    await mrzDetector.init(
        "LICENSE-KEY");
    await mrzDetector.loadModel();
    }
    
    Future<void> initDocumentSDK() async {
    await docScanner.init(
        'LICENSE-KEY');
    await docScanner.setParameters(Template.color);
    }
    
  4. 调用条形码扫描,MRZ识别和文档扫描的方法。
    Web

        XFile file = await controller!.takePicture();
        // Barcode Scanning
        var results = await barcodeReader.decodeFile(file.path);
        // MRZ Recognition
        var results = await mrzDetector.recognizeByFile(file.path);
        // Document Scanning
        var results = await docScanner.detectFile(file.path);
    

    移动

        int format = ImagePixelFormat.IPF_NV21.index;
    
        switch (availableImage.format.group) {
            case ImageFormatGroup.yuv420:
            format = ImagePixelFormat.IPF_NV21.index;
            break;
            case ImageFormatGroup.bgra8888:
            format = ImagePixelFormat.IPF_ARGB_8888.index;
            break;
            default:
            format = ImagePixelFormat.IPF_RGB_888.index;
        }
        // Barcode Scanning
        var results = await barcodeReader
        .decodeImageBuffer(availableImage.planes[0].bytes,
            availableImage.width,
            availableImage.height,
            availableImage.planes[0].bytesPerRow,
            format);
        // MRZ Recognition
        var results = await mrzDetector
          .recognizeByBuffer(availableImage.planes[0].bytes,
            availableImage.width,
            availableImage.height,
            availableImage.planes[0].bytesPerRow,
            format);
        // Document Scanning
        var results = await docScanner
        .detectBuffer(availableImage.planes[0].bytes,
            availableImage.width,
            availableImage.height,
            availableImage.planes[0].bytesPerRow,
            format)
    

    Windows

        Map<String, dynamic> map = event.toJson();
        final Uint8List? data = map['bytes'] as Uint8List?;
        if (data != null) {
    
            int width = previewSize!.width.toInt();
            int height = previewSize!.height.toInt();
    
            // Barcode Scanning
            var results = await barcodeReader
            .decodeImageBuffer(data, width, height, width * 4,
            ImagePixelFormat.IPF_ARGB_8888.index);
            // MRZ Recognition
            var results = await mrzDetector
            .recognizeByBuffer(data, width, height, width * 4,
            ImagePixelFormat.IPF_ARGB_8888.index);
            // Document Scanning
            var results = await docScanner
            .detectBuffer(data, width, height, width * 4,
            ImagePixelFormat.IPF_ARGB_8888.index)
        }
    
    

如何将数据写入和读取flutter中的本地存储

为了模拟注册和登录过程,我们使用shared_preferences插件来存储和检索用户信息。 shared_preferences插件用于将简单数据存储在设备上的键值对中,支持 android ios macos linux Windows Web 平台。以下代码段显示了如何存储和检索用户信息:

class ProfileData {
  String? firstName;
  String? lastName;
  String? email;
  String? password;
  bool? verified;
  String? nationality;
  String? idNumber;

  ProfileData({
    this.firstName,
    this.lastName,
    this.email,
    this.password,
    this.verified,
    this.nationality,
    this.idNumber,
  });
}

// Retrieve user information
SharedPreferences prefs = await SharedPreferences.getInstance();
bool verified = prefs.getBool('verified') ?? false;
String email = prefs.getString('email') ?? '';
ProfileData data = ProfileData(
                email: email,
                firstName: snapshot.data!.getString('firstName') ?? '',
                lastName: snapshot.data!.getString('lastName') ?? '',
                password: snapshot.data!.getString('password') ?? '',
                verified: verified);

if (verified) {
    route =
        MaterialPageRoute(builder: (context) => const OrderPage());
} else {
    if (email.isEmpty) {
    route =
        MaterialPageRoute(builder: (context) => const MyHomePage());
    } else {
    route = MaterialPageRoute(
        builder: (context) => const ProfilePage());
    }
}

// Write user information
Future<void> saveData() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    await prefs.setString('firstName', data.firstName ?? '');
    await prefs.setString('lastName', data.lastName ?? '');
    await prefs.setString('email', data.email ?? '');
    await prefs.setString('password', data.password ?? '');
}

MaterialButton(
    onPressed: () {
        saveData();
    },
    color: Colors.black,
    child: const Text(
        'Sign Up',
        style: TextStyle(
            color: Colors.white,
        ),
    ),
)

尝试在线演示

https://yushulx.me/flutter-last-mile-delivery/

源代码

https://github.com/yushulx/flutter-last-mile-delivery