app scaffold

This commit is contained in:
Guangfei.Zhao
2026-08-17 15:29:55 +08:00
commit 681688dfae
301 changed files with 18414 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
/// 扫码能力的对外 API。来源:conti-docs/07-native-integration.md。
///
/// **调用方只允许 import 这个文件**,不允许直接 import `src/generated/` 里的
/// 生成代码(07 §使用规则)。调用方包括 `feature_scan` 和 `core_webview` 的
/// JSBridge——后者正是 01 里「`core_*` 允许依赖 `native_*`」这条例外存在的
/// 原因。
///
/// 本包按 01 的硬约束**不依赖仓库内任何其他包**(连 `core_foundation` 也不),
/// 所以这里抛的是包内自定义的 [NativeScanException];转成统一错误体系里的
/// `NativeException` 由调用方完成。
library;
import 'package:flutter/services.dart';
import 'src/generated/scan_api.g.dart';
export 'src/generated/scan_api.g.dart' show ScanMode, ScanOptions, ScanResult;
/// 扫码失败的错误码。
///
/// 取值与 12 的 `NativeException.code` 对齐,调用方可以直接透传。
abstract final class NativeScanErrorCode {
/// 用户主动取消。
///
/// 这不是异常流程,调用方通常应当静默返回,**不弹错误提示、不上报**。
static const String cancelled = 'CANCELLED';
/// 相机权限被拒绝。
static const String permissionDenied = 'PERMISSION_DENIED';
/// 能力暂时不可用(相机被占用、初始化失败等)。
static const String unavailable = 'UNAVAILABLE';
/// 当前平台没有实现。
///
/// 见 07 §「OHOS 后续演进」:**不允许静默返回空值或占位假数据**——
/// 静默返回会让"这个平台其实没实现"的问题一直藏到用户手里。
static const String unsupportedPlatform = 'UNSUPPORTED_PLATFORM';
/// 超时。
static const String timeout = 'TIMEOUT';
}
/// 扫码相关的异常。
class NativeScanException implements Exception {
/// [code] 取自 [NativeScanErrorCode]。
const NativeScanException(this.code, this.message);
/// 稳定错误码。
final String code;
/// 面向开发者的描述。**不要直接展示给用户**——文案由调用方按 12 的
/// `ErrorPresenter` 决定。
final String message;
/// 是否是用户主动取消。
bool get isCancelled => code == NativeScanErrorCode.cancelled;
@override
String toString() => 'NativeScanException($code): $message';
}
/// 扫码。
class NativeScan {
/// [api] 仅供测试注入;生产走默认实例。
NativeScan({ScanHostApi? api}) : _api = api ?? ScanHostApi();
final ScanHostApi _api;
/// 打开扫码页并等待一次结果。
///
/// 用户取消时抛 [NativeScanException]`code == CANCELLED`)而不是返回 null——
/// 「取消」和「扫到了空字符串」必须能区分开。
Future<ScanResult> startScan(ScanOptions options) async {
try {
return await _api.startScan(options);
} on PlatformException catch (e) {
// 原生异常类型不外泄(07 §使用规则)。
throw NativeScanException(e.code, e.message ?? '扫码失败');
} on MissingPluginException {
throw const NativeScanException(NativeScanErrorCode.unsupportedPlatform, '当前平台未实现扫码能力');
}
}
/// 当前平台是否支持指定识别类型。
///
/// 调用方应当先查这个再决定要不要显示入口,而不是等 [startScan] 抛错——
/// 车牌识别的技术路径尚未确定(07 待确认项),首版可能只有部分平台支持。
Future<bool> isModeSupported(ScanMode mode) async {
try {
return await _api.isModeSupported(mode);
} on PlatformException {
return false;
} on MissingPluginException {
return false;
}
}
}
@@ -0,0 +1,335 @@
// Autogenerated from Pigeon (v27.3.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: unused_import, unused_shown_name
// ignore_for_file: type=lint
import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List;
import 'package:flutter/services.dart';
import 'package:meta/meta.dart' show immutable, protected, visibleForTesting;
Object? _extractReplyValueOrThrow(
List<Object?>? replyList,
String channelName, {
required bool isNullValid,
}) {
if (replyList == null) {
throw PlatformException(
code: 'channel-error',
message: 'Unable to establish connection on channel: "$channelName".',
);
} else if (replyList.length > 1) {
throw PlatformException(
code: replyList[0]! as String,
message: replyList[1] as String?,
details: replyList[2],
);
} else if (!isNullValid && (replyList.isNotEmpty && replyList[0] == null)) {
throw PlatformException(
code: 'null-error',
message: 'Host platform returned null value for non-null return value.',
);
}
return replyList.firstOrNull;
}
bool _deepEquals(Object? a, Object? b) {
if (identical(a, b)) {
return true;
}
if (a is double && b is double) {
if (a.isNaN && b.isNaN) {
return true;
}
return a == b;
}
if (a is List && b is List) {
return a.length == b.length &&
a.indexed.every(((int, dynamic) item) => _deepEquals(item.$2, b[item.$1]));
}
if (a is Map && b is Map) {
if (a.length != b.length) {
return false;
}
for (final MapEntry<Object?, Object?> entryA in a.entries) {
bool found = false;
for (final MapEntry<Object?, Object?> entryB in b.entries) {
if (_deepEquals(entryA.key, entryB.key)) {
if (_deepEquals(entryA.value, entryB.value)) {
found = true;
break;
} else {
return false;
}
}
}
if (!found) {
return false;
}
}
return true;
}
return a == b;
}
int _deepHash(Object? value) {
if (value is List) {
return Object.hashAll(value.map(_deepHash));
}
if (value is Map) {
int result = 0;
for (final MapEntry<Object?, Object?> entry in value.entries) {
result += (_deepHash(entry.key) * 31) ^ _deepHash(entry.value);
}
return result;
}
if (value is double && value.isNaN) {
// Normalize NaN to a consistent hash.
return 0x7FF8000000000000.hashCode;
}
if (value is double && value == 0.0) {
// Normalize -0.0 to 0.0 so they have the same hash code.
return 0.0.hashCode;
}
return value.hashCode;
}
/// 识别类型。
///
/// **即使首版只做条码,这个参数也必须先留出来**(07 §「待确认:VIN 码与车牌
/// 识别的技术路径」):车牌走的是专用 OCR、VIN 印刷字符走通用 OCR + 校验位
/// 过滤,技术路径还没定。参数先在 schema 里占好位,后面加识别类型就不用改
/// 接口签名——改签名意味着三端生成物和所有调用点一起动。
enum ScanMode {
/// 二维码 / 条形码(商品、库位)。
barcode,
/// VIN 码。可能是 Code 39 条码,也可能只有印刷字符。
vin,
/// 车牌。
plate,
}
/// 扫码入参。
class ScanOptions {
ScanOptions({required this.mode, this.timeoutMs, this.torchEnabled, this.title});
/// 识别类型。
ScanMode mode;
/// 超时毫秒数。null 表示不超时,由用户手动取消。
int? timeoutMs;
/// 是否默认打开闪光灯。
bool? torchEnabled;
/// 扫码页标题。由调用方传,`native_scan` 不依赖任何 i18n 资源。
String? title;
List<Object?> _toList() {
return <Object?>[mode, timeoutMs, torchEnabled, title];
}
Object encode() {
return _toList();
}
static ScanOptions decode(Object result) {
result as List<Object?>;
return ScanOptions(
mode: result[0]! as ScanMode,
timeoutMs: result[1] as int?,
torchEnabled: result[2] as bool?,
title: result[3] as String?,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! ScanOptions || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(mode, other.mode) &&
_deepEquals(timeoutMs, other.timeoutMs) &&
_deepEquals(torchEnabled, other.torchEnabled) &&
_deepEquals(title, other.title);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
@override
String toString() {
return 'ScanOptions(mode: $mode, timeoutMs: $timeoutMs, torchEnabled: $torchEnabled, title: $title)';
}
}
/// 扫码结果。
class ScanResult {
ScanResult({required this.mode, required this.value, required this.durationMs, this.rawFormat});
/// 实际生效的识别类型。
ScanMode mode;
/// 识别到的文本。
String value;
/// 从打开扫码页到出结果的耗时,供埋点用(见 13 的 `scan_succeeded`)。
int durationMs;
/// 原始码制(如 `CODE_39` / `QR_CODE`)。OCR 路径下为 null。
String? rawFormat;
List<Object?> _toList() {
return <Object?>[mode, value, durationMs, rawFormat];
}
Object encode() {
return _toList();
}
static ScanResult decode(Object result) {
result as List<Object?>;
return ScanResult(
mode: result[0]! as ScanMode,
value: result[1]! as String,
durationMs: result[2]! as int,
rawFormat: result[3] as String?,
);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
bool operator ==(Object other) {
if (other is! ScanResult || other.runtimeType != runtimeType) {
return false;
}
if (identical(this, other)) {
return true;
}
return _deepEquals(mode, other.mode) &&
_deepEquals(value, other.value) &&
_deepEquals(durationMs, other.durationMs) &&
_deepEquals(rawFormat, other.rawFormat);
}
@override
// ignore: avoid_equals_and_hash_code_on_mutable_classes
int get hashCode => _deepHash(<Object?>[runtimeType, ..._toList()]);
@override
String toString() {
return 'ScanResult(mode: $mode, value: $value, durationMs: $durationMs, rawFormat: $rawFormat)';
}
}
class _PigeonCodec extends StandardMessageCodec {
const _PigeonCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is int) {
buffer.putUint8(4);
buffer.putInt64(value);
} else if (value is ScanMode) {
buffer.putUint8(129);
writeValue(buffer, value.index);
} else if (value is ScanOptions) {
buffer.putUint8(130);
writeValue(buffer, value.encode());
} else if (value is ScanResult) {
buffer.putUint8(131);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 129:
final value = readValue(buffer) as int?;
return value == null ? null : ScanMode.values[value];
case 130:
return ScanOptions.decode(readValue(buffer)!);
case 131:
return ScanResult.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}
/// Dart → 原生。
///
/// 用户取消、权限拒绝、平台未实现这三类都通过 `FlutterError` 抛出,
/// 由 Dart 侧的公共 API 转成 `NativeScanException`——**原生异常类型
/// `PlatformException`)不允许直接抛到业务代码里**(07 §使用规则)。
class ScanHostApi {
/// Constructor for [ScanHostApi]. The [binaryMessenger] named argument is
/// available for dependency injection. If it is left null, the default
/// BinaryMessenger will be used which routes to the host platform.
ScanHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
: pigeonVar_binaryMessenger = binaryMessenger,
pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty
? '.$messageChannelSuffix'
: '';
final BinaryMessenger? pigeonVar_binaryMessenger;
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
final String pigeonVar_messageChannelSuffix;
/// 打开扫码页并等待一次结果。
///
/// 用户取消时抛 code 为 `CANCELLED` 的错误,而不是返回 null——
/// 「取消」和「扫到了空字符串」必须能区分开。
Future<ScanResult> startScan(ScanOptions options) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.native_scan.ScanHostApi.startScan$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[options]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return pigeonVar_replyValue! as ScanResult;
}
/// 当前平台是否支持指定识别类型。
///
/// 车牌识别的技术路径未定(见上),首版可能只有部分平台支持;
/// 调用方应当先查这个再决定要不要显示入口,而不是等 `startScan` 抛错。
Future<bool> isModeSupported(ScanMode mode) async {
final pigeonVar_channelName =
'dev.flutter.pigeon.native_scan.ScanHostApi.isModeSupported$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[mode]);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return pigeonVar_replyValue! as bool;
}
}