Flutter 表单校验插件 is_valid 鸿蒙适配实战,纯 Dart 实现 49 种格式校验,依托正则完成邮箱、手机号、URL、密码等校验逻辑
开发工具: 华为云码道
DevEco Studio:
26.0.0(26.0.0.851)原始仓库: AbdallahAlsaifi/is_valid
本文配套仓库: oh-flutter/is_valid
is_valid 是一个「一把梭」的表单校验库:一个文件里塞了 49 个 validate* 静态方法,覆盖邮箱、电话号码、URL、密码强度、信用卡、IPv4/IPv6、UUID、语义化版本号、JSON、日期、身份证号、ISBN、MAC 地址、经纬度、IMEI、货币代码等常见与不常见场景,全部用正则和少量纯算术实现,没有任何平台分支。本文以 is_valid 0.0.7 为例,介绍从源码准备、OHOS 工程配置、ArkTS 插件壳,到真机跑通四类校验的完整过程。

插件目前支持 android、ios、web、macos、linux、windows、ohos 平台,源码位于 AtomGit 配套仓库。文中的代码以提交 3fc073b 为参考。

一、插件简介与适配目标
表单校验是个典型的「写起来烦、但不需要什么平台能力」的需求。一个注册页要判邮箱格式、手机号长度、密码复杂度、网址合法性,如果每个项目都自己抄一遍正则,很快就会在不同项目里出现互相矛盾的四五份邮箱正则。is_valid 的存在意义就是把这些判断收敛到一个包里,让 IsValid.validateEmail(...) 这种调用统一所有业务线。
它有几个很鲜明的特征:
- 单文件实现。全部逻辑都在
lib/is_valid.dart一个文件里,共 1412 行,没有拆分层,也没有抽象基类和依赖注入。 - 纯 Dart。正则表达式、
DateTime解析、字符串处理,没有dart:io、没有dart:ffi、没有 MethodChannel 业务调用,因此任何平台拿到的结果都一样。 - 联邦结构齐全但只有空壳。为了符合 Flutter 插件规范,仓库仍有
is_valid_platform_interface.dart和is_valid_method_channel.dart,但平台侧唯一的方法是模板自带的getPlatformVersion,与校验逻辑毫无关系。 - 枚举承担「选项」职责。密码规则、邮编规则、证件类型、国家码这些参数不是用字符串传,而是用 7 个枚举(
PasswordValidation、PostalCodeValidation、IDType、CountryCode、CardType、SubdomainType、TimeFormat)表达。
对一个纯 Dart 库来说,「鸿蒙适配」这个词的含义和前几篇不太一样:不需要翻译任何业务通道协议,也不需要把原生控件换成 ArkUI 组件,全部工作只剩三件事——把插件注册进 Flutter 引擎、让示例工程能在鸿蒙上跑起来、确认 49 个校验方法在 OHOS 的 Dart VM 上行为与 Android/iOS 完全一致。
所以本文的重点会放在「怎么把工程搭起来」和「真机上到底表现如何」,而不是通道协议设计。适配后示例应用在真机上的三类状态如下:
- 默认值(
example@email.com/+8613800138000/https://www.atomgit.com/ 9 位密码)全部VALID; - 把邮箱后缀改成 5 位 TLD、电话号码尾部加一位、URL 尾部插入非法字符、密码缩短到 4 位后,四项全部
INVALID; - 只把邮箱换成双段后缀(
example@email.com.cn)时,邮箱仍然VALID。
二、环境准备
环境搭建参考社区文档:Flutter OH 开发环境搭建,完成 Flutter OH SDK 安装、环境变量和 DevEco Studio 配置。
完成后,在宿主机终端执行以下命令,确认当前选中的是支持 OHOS 的 Flutter 工具链,并能发现目标设备:
flutter --version
flutter doctor -v
hdc list targets


本机输出(节选):
Flutter 3.47.7-ohos-1.0.0 • channel oh-3.47-dev
Framework • revision 4b5928c43f
Engine • revision deb287481e
Dart 3.13.3
[✓] Flutter (Channel oh-3.47-dev, 3.47.7-ohos-1.0.0)
[✓] HarmonyOS toolchain - develop for HarmonyOS devices
hdc list targets 应能看到真机或模拟器的设备 ID,本文实测设备为 4UQ9K25508013016。
2.1 开发套件版本与工程中的 SDK 版本配置
本次适配与验证使用的工作台版本:
| 项目 | 版本 |
|---|---|
| Flutter | 3.47.7-ohos-1.0.0(channel oh-3.47-dev) |
| Dart | 3.13.3 |
| DevEco Studio | 26.0.0(26.0.0.851) |
| OHOS SDK | 6.1.1(24) |
| 真机 ROM | OpenHarmony-6.1.1.120,API 24 |
| 真机型号 | TLR-AL00 |
工程侧对应的是 example/ohos/build-profile.json5 里的两行:
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "6.1.1(24)",
"targetSdkVersion": "26.0.0",
"runtimeOS": "HarmonyOS",
}
]

compatibleSdkVersion 必须不高于真机 API 版本,否则安装阶段会直接报 device is not compatible。适配提交最初写的是 26.0.0,在 API 24 的真机上装不上,后来单独用一次提交(d6cab59)降到 6.1.1(24),与真机对齐。targetSdkVersion 保持 26.0.0 不影响安装,它只表达「按哪个版本的行为运行」。
三、从源码仓库开始准备适配工程
3.1 将上游源码同步到 AtomGit
上游仓库在 GitHub(AbdallahAlsaifi/is_valid),国内访问构建依赖时经常超时,所以适配版本统一镜像到 AtomGit 的 oh-flutter 组织,仓库名与包名保持一致:
https://atomgit.com/oh-flutter/is_valid
同步方式与前面几篇相同:在 AtomGit 新建空仓库,然后把上游 master 推上去,后续的适配提交都打在 master 分支上。这样做的好处是所有改动都能通过 git diff 上游tag 一眼看全,评审时不需要在两个平台之间来回跳。
3.2 将代码拉取到宿主机
git clone https://atomgit.com/oh-flutter/is_valid.git
cd is_valid
ls
目录里能看到上游原本的六端结构(android/ios/linux/macos/windows/web),加上本次新增的 ohos,以及三份鸿蒙说明文档 README.OpenHarmony.md、README.OpenHarmony_CN.md、CHANGELOG.OpenHarmony.md。
3.3 在仓库根目录确认适配分支与版本标记
git branch --show-current
git tag --list
git log --oneline -3
四个提交的职责分别是:
| 提交 | 内容 |
|---|---|
1f704a7 | feat: adapt is_valid for OpenHarmony (0.0.7-ohos-1.0.0-beta.1),新增 ohos/、example/ohos/、三份文档,pubspec.yaml 注册 ohos 平台 |
d6cab59 | chore: update example ohos compatibleSdkVersion to 6.1.1(24),把 SDK 版本从 26.0.0 降到真机能装的 6.1.1(24) |
53fcee5 | docs(ohos): align API docs with real static methods and repair template tests,修正 README 中并不存在的 API 写法、重写两个模板测试 |
3fc073b | docs(ohos): record upstream validator quirks in the READMEs,把 validateDate 掩码、URL 协议可选、电话需带国家码三条上游行为写进文档 |
TAG 0.0.7-ohos-1.0.0 打在 3fc073b 上,对应的 Flutter 框架版本为 3.47,分支为 master——这一点和前面 human_body_selector 用 main 不同,取决于上游仓库本身的主分支名,不必强行统一。
3.4 自动补全 OHOS 适配结构
flutter create --template=plugin --platforms=ohos \
--project-name is_valid .
这条命令会在仓库根目录生成 ohos/(HAR 形式的插件实现)和 example/ohos/(宿主工程)。因为它同时会重写 pubspec.yaml 的 flutter.plugin.platforms 段,所以执行完必须核对 diff:is_valid 上游已经有 android、ios、web、macos、linux、windows 六段,命令只会追加 ohos 一段,不会覆盖已有内容。
实际新增的内容只有三行:
flutter:
plugin:
platforms:
windows:
pluginClass: IsValidPluginCApi
ohos:
pluginClass: IsValidPlugin
web:
pluginClass: IsValidWeb
fileName: is_valid_web.dart
顺序说明:flutter create 会把新平台插在已有平台之间,而不是简单追加到末尾,所以 diff 里看起来「夹在中间」是正常的。
3.5 适配后的项目目录
关键文件与行数:
| 文件 | 行数 | 作用 |
|---|---|---|
lib/is_valid.dart | 1412 | 49 个 validate* 静态方法 + 7 个枚举,全部校验逻辑 |
lib/is_valid_platform_interface.dart | 29 | 联邦接口,getPlatformVersion 抽象 |
lib/is_valid_method_channel.dart | 18 | MethodChannel('is_valid') 的 Dart 侧封装 |
ohos/src/main/ets/components/plugin/IsValidPlugin.ets | 47 | ArkTS 插件壳,实现 getPlatformVersion |
example/lib/main.dart | 161 | 演示页:四个输入框 + 结果卡片列表 |
四、Dart 接口与实现分析
4.1 跨端架构与调用时序
is_valid 的结构比前几篇简单得多:业务调用直达静态方法,平台层是并行的另一条线,两者之间没有任何交叉。
图里用虚线画出来的部分就是「适配真正涉及的全部内容」:MethodChannel 这条链路只服务于版本查询,与校验无关。
4.1.1 一次校验的时序
整个链路没有 await、没有平台往返,纯同步返回 bool。这是它能在任何平台保持一致的根本原因,也意味着鸿蒙适配不会引入任何行为差异。
4.2 49 个校验方法的组织方式
全部方法都是 IsValid 类的静态成员,命名统一为 validateXxx,返回 bool。按输入形态可以分成四类:
| 类型 | 代表方法 | 签名特点 |
|---|---|---|
| 单字符串 | validateEmail、validateIP4Address、validateUUID | static bool f(String input) |
| 命名参数 | validateURL、validateCreditCard、validateHexColorCode | static bool f({required String x}) |
| 带选项 | validatePassword、validatePostalCode、validateIDNumber | 第二个参数是 Map<枚举, dynamic> 或枚举 |
| 带掩码/长度 | validateDate、validateVIN、validateTemperatureUnit | 第二参数为格式串或数值 |
几个容易踩坑的签名,直接抄文档会编译不过:
// 密码:第一个参数是密码本身,第二个参数是规则 Map
final ok = IsValid.validatePassword('Abcdef1!', {
PasswordValidation.minLength: 8,
PasswordValidation.disallowSpecialChars: false,
});
// URL / 信用卡 / 十六进制颜色:命名参数
IsValid.validateURL(url: 'https://www.atomgit.com');
IsValid.validateCreditCard(cardNumber: '4111111111111111');
IsValid.validateHexColorCode(colorCode: '#F0F');
// 日期:第二参数是掩码,默认值有陷阱,详见 9.4
IsValid.validateDate('2026-10-23', 'yyyy-mm-dd');
配套仓库的 README 在适配初期写的是顶层函数形式(isValidEmail(email)),那是照着包的宣传语猜的,实际并不存在。53fcee5 把两份 README 的示例和 API 表都改成了真实的 IsValid.validate* 静态调用——这类「文档与实现不一致」的问题在小型个人库里很常见,适配时建议以 grep "static bool" 的结果为准。
4.3 正则策略与真机行为边界
49 个方法里有四十多个是「一条正则 + hasMatch」,实现上没什么可分析的空间,但边界行为值得单独记一笔,因为真机验证时最容易在这里产生「是适配引入的 bug 吗」的怀疑。
用 dart run 跑了一组探针(本机 flutter test 不可用,原因见 8.1),结果如下:
| 方法 | 输入 | 返回 | 说明 |
|---|---|---|---|
validateEmail | user@example.com | true | 普通邮箱 |
validateEmail | a@b.cc | true | 单字符本地部分也通过 |
validateEmail | user@sub.example.com | true | 支持子域 |
validateEmail | user@example.c | false | TLD 至少 2 位 |
validateEmail | user@@x.com | false | 双 @ 拒绝 |
validateEmail | 空串 | false | 空值不通过 |
validateURL | https://www.atomgit.com:8080/path | true | 支持端口与路径 |
validateURL | atomgit.com | true | 协议头可省略 |
validatePassword | Abcdef1! + minLength: 8 | true | 长度达标 |
validatePassword | abc! + minLength: 8 | false | 长度不足 |
validateCreditCard | 4111111111111111 | true | Luhn 校验 |
validateIP4Address | 192.168.1.1 | true | 正常 IPv4 |
validateIP4Address | 999.1.1.1 | false | 段值越界拒绝 |
validateHexColorCode | #F0F | true | 三位简写支持 |
validateUUID | 550e8400-e29b-41d4-a716-446655440000 | true | 标准 UUID |
validateSemanticVersioning | 1.2.3 | true | SemVer |
validateJSONData | {"a":1} | true | 合法 JSON |
validateDate | 2026-10-23(默认掩码) | false | 见 9.4,上游缺陷 |
validateDate | 2026-10-23(掩码 yyyy-mm-dd) | true | 小写掩码才生效 |
validatePhoneNumber | +8613812345678 + CountryCode.cn | true | 必须带 +86 |
validatePhoneNumber | 13812345678 + CountryCode.cn | false | 纯本地号不通过 |
三条结论:
validateURL的正则把协议头整体做成了可选组(http://www\.|https://www\.|...)?,所以atomgit.com这种裸域名也会被判为合法。这不是适配问题,也不是 bug,而是上游对「URL」的定义比较宽松;如果需要强制协议,调用前自己判startsWith('http')。validateDate的默认掩码存在真实缺陷,'yyyy-MM-dd'里的大写M不会被替换成\d,导致最常见的中文日期写法反而判false。详见第九章。- 电话校验的正则本身包含国家码,
+86是模式的一部分,所以传纯本地号一定失败。这一点在真机上表现与探针一致,说明 OHOS 上没有额外差异。
4.4 平台接口层:为什么它几乎是空的
abstract class IsValidPlatform extends PlatformInterface {
IsValidPlatform() : super(token: _token);
static final Object _token = Object();
static IsValidPlatform _instance = MethodChannelIsValid();
static IsValidPlatform get instance => _instance;
static set instance(IsValidPlatform instance) {
PlatformInterface.verifyToken(instance, _token);
_instance = instance;
}
Future<String?> getPlatformVersion() {
throw UnimplementedError('platformVersion() has not been implemented.');
}
}
这一层完全是 flutter create --template=plugin 的产物,没有一行与校验相关。它的存在只说明这个包遵循了联邦插件(federated plugin)的写法,不代表它真的有平台差异。对适配工作来说,这意味着:
- 不需要为 OHOS 单独实现一个
IsValidOhos类; - 不需要考虑「Dart 侧调用哪个实现」的分支;
- 唯一要在意的,是
pubspec.yaml里ohos.pluginClass指向的类名要能和ohos/index.ets的导出对上。
plugin_platform_interface 在 is_valid 里是显式声明的(^2.0.2),所以不会出现上一篇 human_body_selector 那种 Undefined name 'PlatformInterface' 的编译错误。这也是判断一个库「能不能直接编过」的快速方法:先看 pubspec.yaml 有没有这一行。
4.5 这个库为什么没有业务通道
有些读者会问:一个校验库不调用平台能力,为什么还要 MethodChannel?答案是模板惯性。getPlatformVersion 只是让人能验证「插件确实被注册了」,它的返回值 OpenHarmony OpenHarmony-6.1.1.120 在业务里几乎不会被用到。
从适配角度看,这个空壳仍然有价值:它是注册是否成功的唯一可观测信号。如果 GeneratedPluginRegistrant.ets 里没有 new IsValidPlugin(),或者通道名写错,构建不会报错、校验也照常工作,但一旦业务里真去查版本就会拿到 MissingPluginException。所以第五章仍然要认真把这个壳补完。
五、补全 OHOS 原生实现与工程配置
5.1 在 IsValidPlugin.ets 中补全插件壳
生成的模板只有空类,需要补成完整的 FlutterPlugin + MethodCallHandler:
import deviceInfo from '@ohos.deviceInfo';
import {
FlutterPlugin,
FlutterPluginBinding,
MethodCall,
MethodCallHandler,
MethodChannel,
MethodResult,
} from '@ohos/flutter_ohos';
/** IsValidPlugin **/
export default class IsValidPlugin implements FlutterPlugin, MethodCallHandler {
private channel: MethodChannel | null = null;
constructor() {
}
getUniqueClassName(): string {
return "IsValidPlugin"
}
onAttachedToEngine(binding: FlutterPluginBinding): void {
this.channel = new MethodChannel(binding.getBinaryMessenger(), "is_valid");
this.channel.setMethodCallHandler(this)
}
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null)
this.channel = null
}
}
onMethodCall(call: MethodCall, result: MethodResult): void {
try {
if (call.method == "getPlatformVersion") {
result.success(`OpenHarmony ${deviceInfo.osFullName}`);
} else {
result.notImplemented()
}
} catch (e) {
result.error("PLUGIN_ERROR", `is_valid: failed to handle ${call.method}: ${JSON.stringify(e)}`, null)
}
}
}
三点说明:
- 通道名必须与 Dart 侧一致。Dart 里是
MethodChannel('is_valid'),这里也必须是"is_valid",两边一旦不一致,调用会静默失败。 - 返回值格式对齐 Android/iOS。Android 返回
Android 14,iOS 返回iOS 17.0,这里返回OpenHarmony ${deviceInfo.osFullName},osFullName的形态是OpenHarmony-6.1.1.120。 try/catch是必要的。deviceInfo属于系统服务,在极少数场景下可能抛异常;把异常兜成result.error比让 Dart 侧永远等不到回包要好。
5.2 声明插件和宿主权限
ohos/src/main/module.json5 只有最基础的三段:
{
"module": {
"name": "is_valid",
"type": "har",
"deviceTypes": [
"default",
"tablet"
]
}
}
没有 requestPermissions,这是正确的:is_valid 不读通讯录、不定位、不访问网络,读一次 deviceInfo.osFullName 属于系统公开信息,不需要授权。宿主工程 example/ohos/entry/src/main/module.json5 同样没有 requestPermissions。
如果业务方在自己的应用里同时用了需要权限的插件(相机、定位等),权限要声明在宿主应用的 module.json5,而不是 HAR 插件里——HAR 只是代码容器,权限归应用所有。
5.3 注册并导出插件
ohos/index.ets 负责把实现类导出给 Flutter 工具链识别:
import IsValidPlugin from './src/main/ets/components/plugin/IsValidPlugin';
export default IsValidPlugin;
pubspec.yaml 里对应一行:
flutter:
plugin:
platforms:
ohos:
pluginClass: IsValidPlugin
三者名字必须完全一致:pubspec.yaml 的 pluginClass → ohos/index.ets 的默认导出 → 类名。构建期 flutter build hap 会据此生成 GeneratedPluginRegistrant.ets,本次构建产物内容为:
static registerWith(flutterEngine: FlutterEngine) {
try {
flutterEngine.getPlugins()?.add(new IntegrationTestPlugin());
flutterEngine.getPlugins()?.add(new IsValidPlugin());
} catch (e) {
...
}
}
IntegrationTestPlugin 是 integration_test 依赖自动带进来的,正式应用里不会有。看到 new IsValidPlugin() 这一行,就说明注册链路是通的。
5.4 检查 example 的 OHOS 应用结构
example/ohos 是标准的 HarmonyOS 工程:AppScope/app.json5 定义包名与版本,entry/ 是主模块,entry/src/main/ets/entryability/EntryAbility.ets 负责创建 Flutter 引擎。
需要核对的两个字段:
// AppScope/app.json5
{
"app": {
"bundleName": "com.example.color_system_example",
"versionCode": 1000000,
"versionName": "1.0.0"
}
}
// build-profile.json5
"compatibleSdkVersion": "6.1.1(24)",
"targetSdkVersion": "26.0.0",
bundleName 是从上一个示例工程 color_system 模板复制过来的,没有改成 com.example.is_valid_example。这不影响构建和运行,但有两个实际影响,需要提前知道:
- 签名用的 Profile 里登记的包名必须与
bundleName一致,否则hdc install会报signature verify failed; - 同一个设备上装
color_system、human_body_selector、is_valid等示例时,它们会互相覆盖(包名相同),验证前最好确认当前装的是哪个应用。
本次真机验证时就遇到了第 2 点:设备上新装的 json_translation 示例把 is_valid 覆盖掉了,重新执行一次 hdc install -r 才回到正确界面。正式项目里建议把 bundleName 改成与项目对应的值。
六、补全交付文件并提交适配分支
6.1 除代码外还要补全哪些文件
| 文件 | 内容 |
|---|---|
README.OpenHarmony_CN.md | 中文接入说明:依赖方式、TAG 表、环境版本、API 示例、遗留问题 |
README.OpenHarmony.md | 同上的英文版本 |
CHANGELOG.OpenHarmony.md | 适配变更记录,按 TAG 分段 |
README.OpenSource | 上游来源与开源协议说明 |
example/ohos/README.md | 示例工程的独立运行说明 |
本次额外修了两份 README 的实质错误:
- 依赖地址
CPF-Flutter→oh-flutter; - TAG
0.0.7-ohos-1.0.0-beta.1(从未发布)→0.0.7-ohos-1.0.0; - 环境行
DevEco Studio: 26.0.0 release(26.0.0.851), SDK: 26.0.0(26), ROM: 6.1.0→DevEco Studio: 26.0.0(26.0.0.851), SDK: 6.1.1(24), ROM: 6.1.0.145; - API 示例从并不存在的顶层函数
isValidEmail(...)改成真实的IsValid.validateEmail(...)。
6.2 提交前检查
flutter analyze
flutter pub get
git status --short
flutter analyze 在插件目录与 example 目录要分别跑,两者的分析配置不同。本次结果:
- 插件:
0 error,22 条info(21 条constant_identifier_names+ 1 条dart:html废弃),18 条warning(15 条equal_keys_in_map、2 条unnecessary_null_comparison、1 条unreachable_switch_default); example:No issues found!。
warning 全部来自上游实现,未在适配提交里改动:
equal_keys_in_map:出现在电话国家码、邮政编码、证件长度等多个 map 字面量里。电话表共 204 个键,去重后 202 个,cy(塞浦路斯)与gt(危地马拉)各出现两次,后写入的覆盖前者。属于数据冗余,不导致运行错误。unnecessary_null_comparison:对非空类型做了!= null判断。unreachable_switch_default:validateCurrencyCode的default分支已被前面的case覆盖。
把这些如实写进文章而不是顺手改掉,是因为适配提交的边界应当清晰:只做平台相关改动,风格问题留给上游或独立 PR。
6.3 提交并推送到 AtomGit
git add -A
git commit -m "docs(ohos): align API docs with real static methods and repair template tests"
git tag 0.0.7-ohos-1.0.0
git push origin master
git push origin 0.0.7-ohos-1.0.0
推送后可用 git ls-remote --tags origin 确认标签已到远端:
3fc073bc1a401a3ab356adfaa163f21dc3cc32c9 refs/tags/0.0.7-ohos-1.0.0
TAG 名建议遵循 上游版本-ohos-1.0.0 的格式,1.0.0 是适配迭代号,后续适配修复用 ohos-1.0.1、ohos-1.0.2 递增,不要直接改上游版本号。
七、使用根目录 example 演示接入
7.1 本地适配时使用路径依赖
example/pubspec.yaml 默认就是路径依赖,改完插件代码立刻生效,不需要发版:
dependencies:
flutter:
sdk: flutter
is_valid:
path: ../
然后:
cd example
flutter pub get
7.2 通过 AtomGit 引入插件
真实项目里用 TAG 锁定版本:
dependencies:
is_valid:
git:
url: https://atomgit.com/oh-flutter/is_valid
ref: 0.0.7-ohos-1.0.0
ref 也可以用分支名(master),但生产环境建议用 TAG,避免上游一提交就本地解析出不同的代码。
7.3 调用接口实现表单校验
演示页 example/lib/main.dart 有四个输入框,分别对应邮箱、电话、URL、密码,点「Run validation」后把四个结果渲染成带 VALID / INVALID 的卡片:
void _runValidation() {
final results = <({String label, bool ok})>[
(
label: 'Email: ${_emailCtrl.text}',
ok: IsValid.validateEmail(_emailCtrl.text),
),
(
label: 'Phone (CN): ${_phoneCtrl.text}',
ok: IsValid.validatePhoneNumber(
phoneNumber: _phoneCtrl.text, countryCode: CountryCode.cn),
),
(
label: 'URL: ${_urlCtrl.text}',
ok: IsValid.validateURL(url: _urlCtrl.text),
),
(
label: 'Password: ${_passwordCtrl.text.length} chars',
ok: IsValid.validatePassword(_passwordCtrl.text, {
PasswordValidation.minLength: 8,
}),
),
];
setState(() => _results
..clear()
..addAll(results));
}
结果用 record 列表存,是为了让「一次校验、一次刷新」的语义更清楚——校验是同步的,不存在中间态。
7.4 与 TextFormField 组合的实际写法
演示页为了直观把结果集中展示,真实表单里更常见的是 validator 回调:
TextFormField(
controller: _emailCtrl,
decoration: const InputDecoration(labelText: 'Email'),
validator: (value) {
final text = value?.trim() ?? '';
if (text.isEmpty) return '请输入邮箱';
return IsValid.validateEmail(text) ? null : '邮箱格式不正确';
},
)
密码强度校验用 Map<PasswordValidation, dynamic> 传规则,注意 minLength 是必填项之一而不是唯一项:
validator: (value) {
final text = value ?? '';
if (!IsValid.validatePassword(text, {
PasswordValidation.minLength: 8,
PasswordValidation.maxLength: 32,
PasswordValidation.disallowSpecialChars: false,
})) {
return '密码需 8–32 位';
}
return null;
},
这里有一个容易误解的地方:disallowSpecialChars: false 表示「不禁止特殊字符」,而不是「必须有特殊字符」。这个库只做「禁止类」规则,没有「必须包含大写/数字/符号」这类正向规则,如果业务需要强制复杂度,要在 validatePassword 之外自己补两条正则。
八、验证、构建与鸿蒙设备运行效果
8.1 分别验证插件与 example
flutter analyze 只能保证静态层面没有错误,它对「正则写得对不对」毫无帮助。本机 flutter test 仍然无法运行——flutter_tester 启动即报 Invalid SDK hash,与前面几篇的现象一致,属于本机工具链问题,不是库的问题。所以改用 dart run 写临时探针复算纯逻辑:
cd example
dart run tool/probe.dart
探针跑了 21 组输入,覆盖上表列出的边界,全部与预期一致(validateDate 默认掩码那一组是上游缺陷,不是适配引入)。验证完成后探针立即删除,不留在仓库里——它依赖 example 的包解析,放进仓库反而会增加维护负担。
test/ 目录下的两个上游用例保持原样:
| 文件 | 状态 |
|---|---|
test/is_valid_test.dart | getPlatformVersion 的断言被上游注释掉,只保留了「默认实例是 MethodChannelIsValid」这一条 |
test/is_valid_method_channel_test.dart | 用 mock handler 验证通道名与返回值,与平台无关,逻辑仍然成立 |
example 下两个模板测试则必须重写,因为它们根本跑不起来:
example/test/widget_test.dart是flutter create残留,断言被注释、还引用了不存在的MyApp;example/integration_test/plugin_integration_test.dart把一个 widget 当插件对象调用getPlatformVersion(),类型都不对。
53fcee5 把前者改成直接断言四类校验函数的真值,后者改成面向 IsValidPlatform 的用例,这样即使将来本机工具链修好,测试也有意义。
8.2 确认设备连接
hdc list targets
hdc -t 4UQ9K25508013016 shell param get const.ohos.apiversion
hdc -t 4UQ9K25508013016 shell param get const.product.model
本机返回:
4UQ9K25508013016
24
TLR-AL00
API 24 与 compatibleSdkVersion: "6.1.1(24)" 对应,这是 d6cab59 那次提交的直接原因。
8.3 配置签名
example/ohos/build-profile.json5 里已有 debug 签名配置:
"signingConfigs": [
{
"name": "default",
"type": "HarmonyOS",
"material": {
"certpath": "C:////Users////PC////.ohos////config////default_ohos_xxx.cer",
"keyAlias": "debugKey",
"profile": "C:////Users////PC////.ohos////config////default_ohos_xxx.p7b",
"storePassword": "...",
"keyPassword": "..."
}
}
]
这份配置是从 DevEco Studio 的自动签名里导出的,只在本机有效。换一台机器构建时,需要用自己的证书重签,或者直接在 DevEco Studio 里点 File > Project Structure > Signing Configs > Automatically generate signature。
路径里的 //// 不是笔误,是 JSON 转义后的 \\,DevEco 生成时就长这样,不要手动改成 /。
8.4 运行示例
宿主机上直接 flutter build hap --debug 在本机会卡在 ohpm install(报 00306053 Specification Limit Violation 与 BATCH RECURSION exceeds STACK limits),所以采用「手动装依赖 + hvigor 构建」的方式:
cd example/ohos
"D:/Program Files/Huawei/DevEco Studio/tools/ohpm/bin/ohpm" install
然后用 PowerShell 调 hvigorw(Bash 里路径带空格会失败):
$env:NODE_OPTIONS=""
$env:JAVA_HOME="D:\Program Files\Huawei\DevEco Studio\jbr"
$env:PATH="D:\Program Files\Huawei\DevEco Studio\jbr\bin;" + $env:PATH
Set-Location "D:\flutter\lib2\is_valid\example\ohos"
& "D:\Program Files\Huawei\DevEco Studio\tools\hvigor\bin\hvigorw.bat" assembleHap `
-p product=default -p buildMode=debug --no-daemon
两点必须注意:
$env:NODE_OPTIONS=""不能省。WorkBuddy 会注入一个 safe-delete shim,hvigor 编译期批量清理缓存时触发SAFE_DELETE_BULK_CONFIRM_REQUIRED,最终显示COMPILE RESULT:FAIL,很容易被误判成 ArkTS 语法错误;- 构建末尾可能因为调用
wmic.exe被安全策略拦截而返回非零退出码,但此时BUILD SUCCESSFUL已经打印、HAP 也已产出,以产物文件为准。
产物路径:
example/ohos/entry/build/default/outputs/default/entry-default-signed.hap
大小 145,129,166 字节(约 138 MiB,debug 包,体积主要来自 Flutter debug 引擎)。安装:
cd example/ohos/entry/build/default/outputs/default
hdc -t 4UQ9K25508013016 install -r entry-default-signed.hap
hdc -t 4UQ9K25508013016 shell aa start -a EntryAbility -b com.example.color_system_example
8.5 在设备上测试四类校验
测试步骤(每步之间用 uitest dumpLayout 确认控件坐标,不要凭截图目测):
- 启动应用,确认四个输入框的默认值:
example@email.com、+8613800138000、https://www.atomgit.com、9 个圆点的密码; - 点「Run validation」,确认四条结果都是
VALID; - 把邮箱改为
example@emaxxil.comxx(5 位 TLD),重新校验,确认邮箱变为INVALID; - 把电话尾部再加一位、URL 尾部插入非法字符、密码删到 4 位,重新校验,确认四项全部
INVALID; - 邮箱改为
example@email.com.cn(双段后缀),确认邮箱回到VALID; - 清空邮箱后校验,确认空值判
INVALID; - 聚焦任意输入框,确认软键盘弹出后布局正常、结果卡片可以继续滚动查看。
第 3–4 步的输入方式有个细节:uitest uiInput text 是在光标处插入,点输入框中间会把字符插到中间,得到 wordd 之类的结果。可靠做法是点输入框最右侧(x≈950)让光标落到末尾,再追加字符。
8.6 鸿蒙设备运行效果
左图是启动后的初始状态,底部提示「Tap “Run validation” to see results.」;中图四项全部 VALID,URL 自动换行成两行;右图把邮箱后缀改成 5 位 TLD 后,只有邮箱一项变红。
左图是四项同时无效的组合;中图把邮箱换成 example@email.com.cn,双段后缀仍然 VALID;右图清空邮箱后,空值判 INVALID 且不抛异常。
软键盘弹出后页面整体上移,输入框、按钮、结果区都没有被遮挡,说明演示页的 ListView 在没有额外配置的情况下已经能正确响应键盘 insets。这一点在鸿蒙上与 Android 表现一致,不需要像某些原生页面那样手动 expandSafeArea。
九、FAQ:适配过程与使用问题
9.1 Missing SDK components
报错形态:
Missing SDK components: [ohos-sdk]
说明 DevEco Studio 的 OpenHarmony SDK 没装全或路径没配。打开 Settings > SDK 确认 API 12+ 与 Toolchains 都已下载,再检查环境变量 OHOS_SDK_HOME(或 HOS_SDK_HOME)是否指向 sdk/default。
9.2 DevEco Studio 中看不到 entry 模块
打开 example/ohos 后只看到工程根节点。原因通常是 .hvigor 缓存与当前 DevEco 版本不匹配。删除 example/ohos/.hvigor 与 example/ohos/build 后重新打开,或在 DevEco 里执行 File > Sync and Refresh Project。
9.3 无法手动签名
hdc install 报:
error: failed to install bundle. code:9568320
error: signature verification failed.
检查两处:AppScope/app.json5 的 bundleName 与 Profile 里登记的是否一致;build-profile.json5 里 certpath / profile 指向的文件是否还在(从别的机器拷过来的配置,路径基本都会失效)。
9.4 validateDate('2026-10-23') 返回 false
这是上游实现的真实缺陷,不是适配引入。_buildDateRegex 的实现是:
static String _buildDateRegex(String mask) {
String regexPattern = mask
.replaceAllMapped('d', (_) => r'\d')
.replaceAllMapped('m', (_) => r'\d')
.replaceAllMapped('y', (_) => r'\d');
return '^$regexPattern\$';
}
只替换了小写 d/m/y。默认掩码是 'yyyy-MM-dd',其中月份是大写 M,不会被替换,最终正则变成 ^\d\d\d\d-MM-\d\d$——要求字符串里真的出现字面量 MM。所以:
IsValid.validateDate('2026-10-23'); // false
IsValid.validateDate('2026-10-23', 'yyyy-mm-dd'); // true
IsValid.validateDate('2026-MM-23'); // true(荒谬但符合实现)
三种处理方式,按侵入性排序:
- 调用方传小写掩码:
validateDate(text, 'yyyy-mm-dd'),不动上游代码; - 自己判:
DateTime.tryParse(text) != null,更符合直觉; - 给上游提 PR:把
replaceAllMapped改成大小写都处理,并校验月份、日期范围。
本次适配选择第 1 种,在 README 的使用说明里写明了掩码必须小写,避免使用者踩第二次。
9.5 validateURL('atomgit.com') 也返回 true
正则里协议头是可选组:
r'^(http:\/\/www\.|https:\/\/www\.|http:\/\/|https:\/\/)?[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?$'
所以裸域名、ftp:// 之外的任何「域名 + 可选端口 + 可选路径」都会通过。如果业务要求必须带协议:
bool isStrictUrl(String input) =>
(input.startsWith('http://') || input.startsWith('https://')) &&
IsValid.validateURL(url: input);
另外注意 TLD 段是 [a-z]{2,5},所以 .museum(6 位)、.travel(6 位)都会被判为非法,这与 9.4 的邮箱 TLD 限制是同一个量级的问题。
9.6 validatePhoneNumber 传本地号总是 false
国家码表里的正则把国家码写在模式内部,例如中国是 r'^\+86[0-9]{11}$'。传入 13812345678 永远不匹配。两个选择:
// 1. 调用前补国家码
final ok = IsValid.validatePhoneNumber(
phoneNumber: '+86${input.trim()}',
countryCode: CountryCode.cn,
);
// 2. 自己判本地号,把国家码表当参考
final ok = RegExp(r'^1[3-9]\d{9}$').hasMatch(input);
还有一处命名容易写错:枚举成员是 CountryCode.cn(中国)与 CountryCode.ch(瑞士),CountryCode.In(印度)首字母大写因为 in 是 Dart 关键字。写成 CountryCode.ch 会拿到瑞士的 +41 规则。
9.7 MissingPluginException(No implementation found for method getPlatformVersion on channel is_valid)
说明插件没注册成功。按顺序排查:
example/ohos/entry/src/main/ets/plugins/GeneratedPluginRegistrant.ets里有没有new IsValidPlugin();没有就重新flutter pub get+ 重新构建(该文件是构建期生成的);pubspec.yaml的ohos.pluginClass与ohos/index.ets的默认导出、类名是否三者一致;IsValidPlugin.ets里的通道名是否也是"is_valid";- 安装后是否真的启动了新包(
bundleName相同导致被别的示例覆盖时,跑的其实是另一个应用)。
注意:这个异常不影响任何校验功能,因为 49 个方法全是纯 Dart。它只会在业务代码真的去查版本时出现。
9.8 编译成功但安装失败
error: install failed due to grant request permissions failed.
或
error: device is not compatible.
前者检查 module.json5 里有没有声明了但未在 Profile 中授权的权限;后者就是 compatibleSdkVersion 高于设备 API,改回 6.1.1(24) 即可。
9.9 AtomGit 依赖提示找不到分支或无权限
fatal: couldn't find remote ref 0.0.7-ohos-1.0.0
说明 TAG 没推上去。在仓库里执行 git push origin 0.0.7-ohos-1.0.0,再用 git ls-remote --tags origin 确认。如果是私有仓库,还需要配置 AtomGit 的 SSH key 或 access token。
9.10 flutter analyze 报了一堆 equal_keys_in_map,要不要改
不用改,也不建议在适配提交里改。这些 warning 分布在上游的电话国家码、邮政编码、证件长度等多个 map 里,电话表 204 个键去重后 202 个(cy、gt 重复),属于数据冗余。改它会让适配 diff 里混进大量与平台无关的改动,评审时反而看不出真正的适配内容。要改就单独开 PR 给上游。
9.11 uiInput text 输入中文没有反应
uitest uiInput text 只处理 ASCII,输入 张三@例子.中国 这类内容不会生效(命令返回成功,但输入框内容不变)。真机验证中文化场景需要改用系统输入法手动输入,或把验证点换成「输入含中文的字符串后 validateEmail 返回 false」这类逻辑验证。
本次真机验证因此没有覆盖中文邮箱/中文域名,文章里也不写成「已通过」。
相关链接
- 原始仓库:AbdallahAlsaifi/is_valid
- 配套仓库:oh-flutter/is_valid
- 问题反馈:oh-flutter/is_valid issues
- Flutter OH 环境搭建:flutter_samples/docs/ohos/getting-started
更多推荐



所有评论(0)