最后一英里的交付应用程序是一种软件应用程序,用于管理,协调和跟踪从运输中心到其最终目的地的商品交付,这通常是个人住所。这通常是整个交付过程中最复杂的部分,因为它涉及到居民区,处理流量,确保产品安全,及时地交付产品并确认成功交付。随着电子商务的兴起以及对快速有效的房屋交付的期望,对最后一英里交付应用程序的需求大大增加。在本文中,我们将向您展示如何使用Flutter and Dynamoft Vision SDK(Dynamsoft Barcode Reader,Dynamsoft Label Recognizer和Dynamsoft 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_sdk,flutter_ocr_sdk和flutter_document_scan_sdk。它们使您可以轻松地将Dynamoft Vision SDK集成到您的Flutter应用程序中。
应用设计和工作流程
通过单击下面的链接查看应用程序设计。
https://xd.adobe.com/view/7bbceea3-74e8-4013-80bc-0565dea8cc52-2eef/
应用程序的基本工作流量如下:
-
启动应用程序:在您的设备上启动应用程序。这将带您直接进入注册页面。
-
注册或登录:如果您是新用户,请通过注册创建一个新帐户。如果您是现有用户,请登录您的帐户。
-
配置文件验证:一旦您注册或签名,您将被定向到个人资料页面。此时,您的个人资料尚未验证。要验证您的个人资料,请单击按钮打开相机。
-
扫描许可证或护照:使用相机扫描驾驶执照或护照。这将为个人资料验证提供必要的个人信息。
-
个人资料验证过程:扫描许可证或护照后,您的个人资料将通过验证过程。
-
导航到订单页面:一旦您的配置文件得到验证,您将被定向到订单页面。在这里,您可以看到分配给您的订单。
-
扫描订单条形码:要获取订单的信息,请扫描订单的条形码。
-
扫描文档并交付订单:扫描订单的必要文档,然后单击按钮输送订单。
-
返回订单页:交付订单后,您将被直接回到订单页面,您可以继续下一个订单。
开发核心功能
在随后的部分中,我们将讨论如何开发应用程序的核心功能,包括相机集成,条形码扫描,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!);
}
为了在全屏中渲染相机预览和覆盖,我们使用了Stack
,Positioned
,FittedBox
和SizedBox
窗口小部件的组合。
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应用程序中
-
将以下依赖项添加到
pubspec.yaml
文件:
flutter_barcode_sdk: ^2.2.2 flutter_document_scan_sdk: ^1.0.2 flutter_ocr_sdk: ^1.1.0
-
在https://www.dynamsoft.com/customer/license/trialLicense申请Dynamoft Vision SDK的试用许可。
-
使用许可键初始化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); }
-
调用条形码扫描,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/