新闻详情

新闻详情

首页 / 资讯中心 / 详情

Flutter在OpenHarmony上的电子合同搜索模块实战解析

发布时间:2026/9/29 18:12:01来源:尧图网络
Flutter在OpenHarmony上的电子合同搜索模块实战解析
把 Flutter 应用跑到 OpenHarmony 设备上这个动作已经淘汰掉一批准备不足的团队而要在电子合同签署App里把合同搜索做到又快又准又会淘汰掉一批只会写列表页的开发者。我上个月刚完成公司“电子合同签署App”的 OpenHarmony 适配第一模块就是合同搜索。解决好它再在 OpenHarmony 上做其他跨端业务心里都会踏实不少。这篇文章我会把整个搜索模块拆开讲为什么在 OpenHarmony 上选 Flutter、工程怎么搭、合同数据模型怎么建、搜索匹配和状态过滤怎么写、结果页怎么高亮、2 万条数据下怎么保证不卡。如果你刚把 Flutter 跑上 OpenHarmony或者正在做某个行业 App 的跨端改造这篇可以直接照着落地。1. 为什么在 OpenHarmony 上选 Flutter两条路线的实际对比1.1 当前 OpenHarmony 应用开发的四条主流路线OpenHarmony 生态发展到现在应用开发路线基本分成四类ArkUI 原生开发、Flutter 适配、RN 适配、H5/WebView 套壳。我最后选了 Flutter 适配版 SDK也就是社区常说的 flutter_ohos不是因为赶潮流而是被团队现状和业务场景逼出来的。先说 ArkUI 原生。如果是从零起步、只做 OpenHarmony 单端ArkUI 确实是最稳妥的选择官方支持力度最大组件和动画性能也最直接。但问题是我们团队已经有一套完整的 Flutter 代码库里面包含合同模块、审批流模块、电子签名板、文件预览等业务代码重新用 ArkUI 写一遍等于把整个业务复盘成本不是翻倍是直接翻三四倍。RN 那边我也调研过。RN 的社区适配在 OpenHarmony 上确实在推进但三方原生库的适配进度差别很大很多常用的原生模块要么自己写桥接要么等社区维护。到了合同这种对权限、文件、签名板依赖很重的场景我实在不想赌每个依赖都有人持续适配。H5/WebView 套壳是最省事的但只适合内容展示型应用。电子合同的搜索要求离线可用、快速过滤、多条件组合而且用户会输入合同编号、相对方公司名、签署状态这些在 WebView 里做交互手感和性能都压不住。四条路线的对比我整理成了一张表方便你根据自己团队情况判断路线投入成本性能上限三方库生态适合场景ArkUI 原生高业务重写高持续增长只做 OpenHarmony 单端、有原生团队Flutter 适配中保留业务层中高纯 Dart 包可用原生包需确认适配已有 Flutter 代码库、重视端侧一致性RN 适配中中社区维护强度不一已有 RN 代码库、以内容展示为主H5/WebView低中低强依赖浏览器内核轻交互、弱离线、快速上线1.2 电子合同搜索场景对框架的三个硬要求电子合同 App 的搜索和资讯类 App 的搜索完全不同。资讯搜索的核心是“召回更多内容”合同搜索的核心是“精确找到某一笔业务”。这个差异决定了框架选型和技术方案。第一个硬要求是离线优先。很多用户在出差路上、地下车库、电梯里翻合同网络环境根本不稳定。搜索必须基于本地已经落地的合同数据完成不能每次都去请求后端接口。Flutter 的内存对象模型和本地存储配合得很好我可以把合同列表项常驻内存搜索就是一次内存过滤。第二个硬要求是高频且不可停顿。合同签署人一天说“找一下那份合同”好几次搜索首帧超过 300 毫秒用户就会明显感觉卡。Flutter 的 UI 线程模型只要你不乱写同步计算保持流畅是没问题的。第三个硬要求是多端一致。同一套 Flutter 代码要在手机、平板上保证筛选、高亮、排序行为完全一致。Flutter 在这方面天然占优因为 UI 和业务逻辑都是跨端共享的不需要我为不同屏幕尺寸写两套搜索页。2. 先把开发环境跑通Flutter for OpenHarmony 工程搭建2.1 工具链准备与版本搭配环境搭建是很多人的第一个坎。OpenHarmony 的 Flutter 适配版并不是 Flutter 官方主分支直接支持的你需要拿到对应的适配 SDK并配合官方 IDE 来完成签名、编译、安装。我的建议是分成三步走安装官方配套 IDE用它完成 OpenHarmony SDK 管理。OpenHarmony 的 SDK 组件、工具链、签名配置都在这个 IDE 里统一管不要自己手工下载碎片化工具。准备 Flutter 稳定版和 flutter_ohos 适配版。实际项目中我直接使用三方库中心托管的 flutter_ohos SDK这是 Flutter 代码库在 OpenHarmony 上的适配分支。版本选型以适配版官方 release 页的配套矩阵为准不要盲目追最新的 Flutter 版本因为新版本引入的渲染引擎改动可能还没合入适配分支。配置完成后在 IDE 里创建一个空的 OpenHarmony 工程先把签名和实机安装流程跑通。这一步绝对不能跳过如果你等到 Flutter 工程跑起来再去配签名遇到问题时很难分辨是 Flutter 适配的问题还是签名的问题。跑完这三步再用flutter doctor -v看输出如果能识别出 OpenHarmony 相关条目说明工具链已经接上了。2.2 创建一个带 ohos 平台的 Flutter 工程Flutter 工程支持 OpenHarmony 平台之后创建方式和传统 Flutter 工程基本一致。我是用命令行生成的flutter create --platformsohos --org com.example.signing .生成后的工程目录会多出一个ohos目录和android、ios平级。这个ohos目录就是 OpenHarmony 原生工程的入口里面有 entry 模块、build-profile.json5等原生工程文件。后续如果要对鸿蒙设备做原生定制比如调用系统签名板、接入相机都要在这个目录里动手。如果你用的是旧版本 Flutter 工程命令行不支持--platformsohos也不用慌。最简单的方式是从官方模板里拷贝一份ohos目录到现有工程手动改一下包名和应用 ID再重新构建。两种方式我都试过新工程直接用命令行最省事老工程手工拷贝也能跑。2.3 跑通 hello world 之后先验证的三件事很多人在 OpenHarmony 上写完一个 hello world 就觉得环境没问题了等到接真实业务才发现处处是坑。我跑通之后先做了三个专项验证第一个验证是 hot reload 是否真的可靠。OpenHarmony 适配版的热重载在首次连接后通常会慢几秒这是正常的。但如果每次改代码都要等十秒以上甚至直接断连就要检查 USB 调试模式和 IDE 里的连接状态。热重载是 Flutter 开发效率的命根子这个不靠谱后面调搜索 UI 会让你崩溃。第二个验证是三方库是否真的可用。我跑了一个flutter pub add path_provider然后调用getApplicationDocumentsDirectory()看能不能拿到目录。很多纯 Dart 包在 OpenHarmony 上直接就能用但依赖原生通道的插件就不一定了。如果你一调用原生能力就报MissingPluginException说明这个插件还没有适配 OpenHarmony需要另找方案或者自己写桥接。第三个验证是 EventChannel 是否打通。我写了一个小的 demo从 Dart 侧调用原生返回一个设备型号字符串。电子合同签署场景后面要调原生签名板、拍照、文件预览这些都得靠原生能力通道。通道早一点确认可用后面接业务就不会被卡住。3. 合同数据模型与搜索空间设计3.1 一个够用的 ContractListItem 模型搜索功能的地基是数据模型。第一版我图省事直接搜数据库表结果每次输入一个关键字都要重新查库又慢又容易出并发问题。后来改成把搜索需要的字段全部放进一个轻量列表模型常驻内存搜索就变成一次内存过滤。enum ContractStatus { draft, // 草稿 pending, // 待签署 signing, // 签署中 completed, // 已完成 expired, // 已过期 revoked, // 已撤销 } class ContractListItem { final String id; final String contractNo; final String title; final String counterparty; final int amountInFen; // 金额单位用分避免浮点误差 final ContractStatus status; final DateTime signTime; final DateTime updateTime; const ContractListItem({ required this.id, required this.contractNo, required this.title, required this.counterparty, required this.amountInFen, required this.status, required this.signTime, required this.updateTime, }); }这里有几个细节值得说一下。合同编号和标题一定要分开存。编号是唯一键常用来做精确匹配标题是用户记忆里的“南京那份设备采购合同”需要模糊匹配。相对方公司名也是高频搜索项用户经常说“找一下某某公司的合同”所以我把它也作为一个独立字段冗余进来。金额我用int存分不用double。合同金额涉及到财务对账浮点数运算的误差在电子合同场景是不能接受的。存分的方式在 UI 层展示时再换算成元搜索排序时也能避免浮点比较的坑。合同分页存储是另一个关键决策。搜索时只需要这个列表项模型不需要加载完整合同正文。我把合同正文和签名记录存在本地存储里按id懒加载。这样即使本地有 2 万份合同搜索时内存里也只保留列表项不会把几 GB 的正文全部拖进内存。3.2 状态枚举与搜索条件的组合合同搜索很少只靠一个关键字搞定。用户的真实操作习惯是输入“南京”再勾选“待签署”和“签署中”有时候还会限定签署时间范围。所以搜索条件不能只是一个字符串我封装了一个查询参数类class ContractSearchQuery { final String keyword; final SetContractStatus statuses; final ContractSort sort; final DateTime? startTime; final DateTime? endTime; const ContractSearchQuery({ this.keyword , this.statuses const {}, this.sort ContractSort.signTimeDesc, this.startTime, this.endTime, }); bool get hasFilter keyword.isNotEmpty || statuses.isNotEmpty || startTime ! null || endTime ! null; }状态用SetContractStatus而不是单个枚举是因为用户经常同时勾选多个状态比如“待签署 签署中”一起搜。如果你用单个字段就只能让用户在“待签署”和“签署中”之间二选一这个产品就废了。3.3 生成两万条模拟合同的土办法做搜索功能最怕的是拿 30 条数据调 UI一上线就卡。我压测时直接造了 2 万条合同数据。为什么是 2 万条一般企业的合同归档量在 1 万到 3 万条之间2 万是一个比较有代表性的压测水位。生成数据的核心是让标题、编号、相对方公司名有一定的规律和重复度这样才能模拟真实用户输入关键词的场景。ListContractListItem generateContracts(int count) { final random Random(42); final cityPool [南京, 上海, 北京, 深圳, 杭州]; final typePool [设备采购, 技术服务, 框架协议, 劳动合同, 保密协议]; return List.generate(count, (i) { final city cityPool[random.nextInt(cityPool.length)]; final type typePool[random.nextInt(typePool.length)]; return ContractListItem( id: c_$i, contractNo: HT2024${i.toString().padLeft(6, 0)}, title: $city$type合同, counterparty: $city示例科技有限公司, amountInFen: random.nextInt(100000) * 100, status: ContractStatus.values[random.nextInt(ContractStatus.values.length)], signTime: DateTime(2023, 1, 1).add(Duration(days: random.nextInt(700))), updateTime: DateTime(2023, 1, 1).add(Duration(days: random.nextInt(700))), ); }); }这个生成器最大的价值是让搜索场景有“可预期性”。比如我知道合同编号前缀是HT2024那我搜ht2024应该命中全部合同搜南京应该命中一部分搜南京 设备采购应该命中交叉部分。有了这些预期测试搜索逻辑就非常方便。4. 合同搜索核心实现三个维度层层递进4.1 搜索入口独立路由而不是 SearchDelegate搜索入口我放在了合同列表页的顶栏点击后进入一个独立的SearchPage。我没有用 Flutter 自带的SearchDelegate原因有三点第一SearchDelegate的内部样式在 OpenHarmony 的字体渲染下需要大量覆盖它的搜索框状态也不是完全可控。第二我需要一个“最近搜索”区块方便用户一键复用历史关键词SearchDelegate那套流程塞进去很别扭。第三电子合同搜索经常会搭配状态筛选和时间筛选独立页面能加载更复杂的过滤 UI。跳转方式很简单Futurevoid _openSearch(BuildContext context) { Navigator.of(context).push( MaterialPageRoute(builder: (_) const SearchPage()), ); }4.2 文本匹配归一化、模糊匹配、多关键词搜索匹配是整个模块的地基。我踩过一个大坑用户输入HT2024数据库里存的是ht2024如果直接String.contains做比较大小写不一致就漏数据。所以第一步必须是归一化。String _normalize(String input) { return input .toLowerCase() .trim() .replaceAll(RegExp(r\s), ); }归一化之后再做多关键词匹配。用户经常会输入“南京 设备”这种带空格的搜索词意思是这些词都要出现在合同信息里。我实现的是 AND 逻辑bool _matchContract(ContractListItem item, String normalizedKeyword) { if (normalizedKeyword.isEmpty) return true; final keys normalizedKeyword.split( ); final haystack _normalize(${item.title} ${item.contractNo} ${item.counterparty}); return keys.every(haystack.contains); }把标题、编号、相对方拼成一个 haystack好处是两个关键词可以跨字段命中。比如用户输入“南京 HT2024”第一条命中的可能就是“南京设备采购合同”而编号是HT2024123456这种跨字段的 AND 匹配特别符合真实搜索习惯。这里有一个很常见的 bug如果关键词之间包含中文全角空格split( )切不开。我归一化时用RegExp(r\s)把连续任意空白字符压成半角空格就是为了处理这个场景。4.3 状态过滤与排序查询条件如何组合文本匹配只是第一层真正的搜索体验在于状态过滤和排序。我的搜索函数是一个纯 Dart 函数不依赖任何 Flutter 上下文方便单元测试ListContractListItem _search( ListContractListItem source, ContractSearchQuery query, ) { final keyword _normalize(query.keyword); final filtered source.where((item) { if (!_matchContract(item, keyword)) return false; if (query.statuses.isNotEmpty !query.statuses.contains(item.status)) { return false; } if (query.startTime ! null item.signTime.isBefore(query.startTime!)) { return false; } if (query.endTime ! null item.signTime.isAfter(query.endTime!)) { return false; } return true; }).toList(); filtered.sort((a, b) { final exactA a.contractNo query.keyword; final exactB b.contractNo query.keyword; if (exactA ! exactB) return exactA ? -1 : 1; return b.updateTime.compareTo(a.updateTime); }); return filtered; }排序里最值得学习的是“精确匹配优先”。当用户输入一个完整的合同编号时搜索意图非常明确就是要找那一份合同。这个时候如果还按签署时间倒序用户可能要在第一屏翻半天。所以我把“合同编号完全等于关键字”的记录提到最前面。这样做还有一个隐藏好处用户从历史记录里点了一个“HT2024123456”搜索页回到列表页时那份合同一定排在首位产品体验非常顺。4.4 300ms 防抖与异步竞态搜索最忌讳每敲一个字符就跑一次全量检索。用户在中文输入法下打“南京”键盘联想会触发一长串 onChange如果每次都搜索300ms 内能同时发起五六次查询不仅浪费性能还会出现结果乱跳。我的做法是 300ms 防抖Timer? _debounce; void _onKeywordChanged(String value) { _debounce?.cancel(); _debounce Timer(const Duration(milliseconds: 300), () { _loadResults(value); }); }为什么是 300ms 而不是 100ms我测试过中文输入法在 100ms 量级还在联想过程中过早触发会把用户没打完的词拿去搜索结果当然不对。300ms 是比较稳妥的平衡点。防抖之外还要处理一个问题异步请求的竞态。用户输入“南京”触发了一次 300ms 后的查询 A但在查询 A 返回前用户又输入了“南京设备”触发了查询 B。如果 A 比 B 晚返回结果就会用旧关键词覆盖新关键词。我用了序列号机制来丢弃过期结果int _querySeq 0; Futurevoid _loadResults(String keyword) async { final seq _querySeq; final result await _searchAsync(keyword); if (seq ! _querySeq) return; // 过期结果直接丢弃 setState(() _results result); }这个序列号方案在搜索场景里足够用不需要引入复杂的状态管理库。5. 搜索结果页 UI高亮、空态和 OpenHarmony 输入法适配5.1 关键词高亮逐词切分 TextSpan搜索结果如果没有高亮用户就得在列表里肉眼找关键词体验大打折扣。高亮的实现并不复杂核心是把一句话按关键词切分成多个片段命中片段用高亮样式未命中用普通样式。ListTextSpan _highlightSpans( String text, SetString keys, TextStyle base, TextStyle highlight, ) { final spans TextSpan[]; final lower text.toLowerCase(); final sortedKeys keys.toList()..sort((a, b) b.length - a.length); int start 0; while (start text.length) { int? hitIndex; String? hitKey; for (final key in sortedKeys) { final idx lower.indexOf(key.toLowerCase(), start); if (idx 0 (hitIndex null || idx hitIndex)) { hitIndex idx; hitKey key; } } if (hitIndex null || hitKey null) { spans.add(TextSpan(text: text.substring(start), style: base)); break; } if (hitIndex start) { spans.add(TextSpan(text: text.substring(start, hitIndex), style: base)); } spans.add(TextSpan( text: text.substring(hitIndex, hitIndex hitKey.length), style: highlight, )); start hitIndex hitKey.length; } return spans; }这里有三点容易踩坑。排序关键词按长度倒序是必须的。比如用户搜“公”但相对方公司名是“南京示例科技有限公司”如果处理不好会把“公司”里的“公”高亮得七零八落。按长词优先匹配可以避免短词把长词内部结构切开。indexOf的起始位置必须是start否则会死循环。我之前犯过一个错误每次从头找同一个关键词start永远前进不了结果while循环把内存拖爆。高亮样式要克制。我用的是主题色加粗不加背景色。电子合同场景偏商务专业大红大绿的背景色会让结果页显得很不正式。5.2 空状态与最近搜索无结果时最差的做法是只显示一个“暂无搜索结果”的文案。用户心里会想“我明明记得有这份合同怎么搜不到”这时候要给用户出口。我做了两个按钮“清空筛选条件”和“查看全部合同”。“清空筛选条件”解决的是“筛选条件太严格导致无结果”的问题“查看全部合同”解决的是“搜索词不对但合同确实存在”的问题。最近搜索是搜索体验的重要一环。我把它存在本地存储里最多保留 10 条Futurevoid _saveRecent(String keyword) async { final box await Hive.openBoxString(recent_search); final list box.get(keywords) ?? []; list.remove(keyword); list.insert(0, keyword); if (list.length 10) list.removeRange(10, list.length); await box.put(keywords, list); }这里有一个安全上的细节我要特别提醒合同数据是敏感业务数据搜索结果为空时绝对不要做网络联想也不要调用任何线上接口去“猜用户想搜什么”。否则用户输入的合同关键词可能会被第三方接口记录这在电子合同场景里是不可接受的风险。5.3 OpenHarmony 适配中的三个输入法细节OpenHarmony 设备的输入法和标准 Android 有差异我在适配时踩了三个坑。第一个是键盘遮挡问题。OpenHarmony 自带输入法在部分版本上会直接盖住 TextField 下方的“最近搜索”区块。解决办法是在 build 时读取MediaQuery.of(context).viewInsets.bottom动态调整底部 padding。这个值在键盘弹出时会变化所以必须放在build方法里读不能缓存。第二个是中英文切换的匹配问题。输入法从英文切到中文时搜索框的controller.text本身不会出问题但如果你监听了onChanged搜索词的归一化就很重要。我前面写的_normalize会把所有字符转小写同时处理空白字符这样“HT 2024”和“ht2024”才能命中同样的数据。第三个是搜索动作的提交。在 OpenHarmony 上TextField 的默认动作在不同输入法里差异很大。我显式指定TextField( textInputAction: TextInputAction.search, onSubmitted: (value) _onKeywordChanged(value), )指定TextInputAction.search后键盘上的搜索按钮才会比较稳定地触达 Flutter 的提交回调。如果省略这一步部分输入法只在失去焦点时才触发提交用户会以为搜索按钮没反应。6. 性能优化从 300 条到 2 万条合同的实测记录6.1 首帧卡顿的根因是我自己写的同步搜索第一版在 300 条测试数据上跑得飞快把数据量加到 2 万条之后每次输入一个字符都会卡一会儿输入法都跟不上。定位后发现罪魁祸首是我在 UI 线程同步执行了_search和_highlightSpans。Flutter 的 UI 线程要负责渲染、动画、输入法响应当主线程被一个耗时的sort或大量字符串操作塞满用户看到的就直接是卡顿和掉帧。所以第一步优化很明确把“搜索 排序”移出主线程UI 线程只负责接收结果和渲染高亮。6.2 compute 不是万能的常驻 isolate 才是很多 Flutter 开发者会第一时间想到compute函数因为它在 Flutter 里用起来最简单。但我很快发现在 2 万条数据的搜索场景下compute反而更慢。原因在于compute的隔离模型。每次搜索都要新建一个 isolate把 2 万条合同对象拷贝进去搜索完还要把结果拷贝回主线程。isolate 的创建、预热、销毁这些开销叠加起来比搜索本身还大尤其在低端 OpenHarmony 设备上表现更明显。正确的做法是启动一个常驻后台 isolate让它一直活着随时接收搜索请求void searchIsolateMain(SendPort sendPort) { final receivePort ReceivePort(); sendPort.send(receivePort.sendPort); receivePort.listen((message) { final request message as SearchRequest; final result _search(request.source, request.query); request.replyPort.send(result); }); }主 isolate 侧通过SendPort发请求后台 isolate 处理完通过replyPort把结果传回来。这样避免了两万条数据反复拷贝的 overhead实测下来 2 万条合同的搜索加排序稳定在 60ms 以内。需要提醒的是常驻 isolate 的数据来源必须在启动后一次性下发并缓存在 isolate 内部。如果每次请求都通过SendPort传 source那等于又把拷贝成本捡回来了。6.3 预计算拼音与首字母索引中文场景最麻烦的是拼音搜索。用户记不住合同全称只会输“南京那笔服务合同”或者直接打首字母“njfw”。纯中文contains匹配完全处理不了这些输入。我的方案是为每个合同预计算一份拼音索引搜索时同时匹配原始中文、完整拼音、首字母缩写class ContractSearchIndex { final String id; final String fullPinyin; // nanjingshishebeicaigouhetong final String abbrPinyin; // njsbcght }拼音转换我用的是 pub 上的纯 Dart 包。选择纯 Dart 的原因很明确很多拼音包依赖原生 ICU 库而 ICU 在 OpenHarmony 原生侧的支持情况不完全一致一旦遇到NotImplementedError就很难排查。纯 Dart 方案虽然在转换速度上略慢但兼容性可控预计算本来就在后台 isolate 里做慢一点无感。预计算不是边搜索边算。我在 App 冷启动后用一个后台任务生成全量索引生成期间不阻塞用户操作。1 万条合同的预计算时间大约在 1.5 秒左右完全可接受。6.4 缓存搜索结果与滚动位置用户输入“南京”得到结果点击进入详情再返回搜索页这时候如果因为状态重建把结果清空用户会非常恼火。我在SearchPage的状态里保存了_lastQuery和_lastResults页面恢复时直接重新渲染不需要重新搜索。滚动位置丢失是另一个隐蔽问题。搜索结果列表很长的场景下用户滑到了第 30 条去详情页看了一份合同退回来发现又从头开始了。这个问题的解法是给ListView一个稳定的 keyListView.builder( key: const PageStorageKeyString(search_result_list), itemBuilder: ..., )PageStorageKey本质是给列表一个持久化身份Flutter 的PageStorage会自动保存它的滚动偏移量。这个方案成本极低效果却很直接。7. 组件通信与状态优化搜索页、列表页、详情页怎么联动7.1 用同一个 Cubit 管理搜索条件和列表页数据合同搜索不是一个孤立的页面它和列表页、详情页之间有很强的联动。最差的做法是用Navigator.pop的返回值把搜索条件传回列表页。这样做的坏处是列表页无法感知搜索页内部的实时状态而且回调层层嵌套很难维护。我的方案是用同一个ContractListCubit管理两边的状态class ContractListCubit extends CubitContractListState { ContractListCubit(this.repository) : super(const ContractListState()); void updateQuery(ContractSearchQuery query) { emit(state.copyWith(query: query, dirty: true)); } Futurevoid refresh() async { final list await repository.fetchContractList(); emit(state.copyWith(list: list, dirty: false)); } }搜索页只负责把ContractSearchQuery发布到 Cubit列表页监听 Cubit 状态自动刷新。这样有一个非常关键的收益从搜索页返回列表页时列表数据和筛选条件永远是一致的不会出现“搜索结果显示 3 条回到列表页却看到全部合同”的割裂感。7.2 为什么用 Cubit 而不是 ChangeNotifier我在这个项目里最终选了 flutter_bloc 全家桶里的 Cubit而不是一开始用的 ChangeNotifier原因是在合同搜索这种多状态场景里Cubit 的状态机边界更清晰。ChangeNotifier的问题是通知粒度太粗。它只负责“告诉所有监听者我变了”但监听者不知道到底哪个字段变了。搜索场景里有 keyword、statuses、startTime、endTime、loading、dirty 多个状态维度ChangeNotifier 的模型很难区分“筛选条件变化”和“列表刷新完成”。Cubit 强制你定义完整的ContractListState每次 emit 一个新状态对象所有变化都是可追溯的。调试时我只要看状态对象就知道搜索页当前处于什么阶段。在 OpenHarmony 上跨设备调试本来就比 Android 麻烦状态管理越可追溯问题定位越快。7.3 搜索过程中新增合同怎么办电子合同场景有一个很现实的并发问题用户正在搜索后台推送了一条新的待签署合同。如果搜索结果是纯快照这条新合同就漏掉了用户可能会错过重要的签署任务。我的处理思路是引入版本号。底层 Repository 维护一个version字段每次合同列表刷新后version。搜索页拿到结果时检查版本号如果发现已经变化就自动发起一次重新搜索final currentVersion _repository.version; final result await _searchAsync(request); if (currentVersion ! _repository.version) { _loadResults(state.keyword); // 数据源已变化重新查一次 return; } setState(() _results result);这个方案不追求实时同步但能保证搜索结果的“最终一致性”。用户不会在搜索页看到一条已经被签完但状态还停留在“待签署”的合同体验是符合业务预期的。8. 一块合同搜索模块做完之后的复盘建议整个模块做下来我最想分享的是这几个实操层面的建议。第一用三条数据量先调业务逻辑再用两万条数据做压测。不要一上来就追求极限优化否则你在 300 条数据上改的高亮逻辑可能在 2 万条量级又变成性能瓶颈。第二把_matchContract、_highlightSpans、_search这些函数全部设计成纯函数。纯函数不依赖 Flutter 上下文可以直接写单元测试覆盖大小写、空格、多关键词、编号精确命中这些边界情况。电子合同这种业务测试覆盖率比普通 App 重要得多。第三多设备适配不能只在模拟器上看。我在 OpenHarmony 手机和开发板上都跑了搜索模块重点看输入法弹出、系统字体缩放、返回手势这三个点。模拟器上正常不等于真机正常尤其是输入法和滚动手感这种强交互体验必须真机验证。第四如果合同量后续突破二十万本地内存过滤就该让位于服务端检索。我在设计 Repository 接口时预留了扩展位后续切换数据源不用动搜索 UI。这个扩展点建议大家都留上谁也不知道业务量什么时候会翻倍。这块搜索模块做完之后我对 Flutter 在 OpenHarmony 上的落地又多了一层信心。框架本身能做的事其实比想象中多真正决定体验高度的还是数据模型和检索策略这些基本功。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

无头Linux服务器远程可视化:TurboVNC与VirtualGL配置实战 2026/9/29 19:18:05

无头Linux服务器远程可视化:TurboVNC与VirtualGL配置实战

在数据中心里摸过机器人仿真的朋友,大概率经历过这种场景:手里只有一台能 SSH 的服务器,GPU 是 A5000 还是 4090 都验过了,nvidia-smi 输出正常,Isaac Sim 也能跑起来,但画面只能靠日志描述——因为这台机器…

阅读更多 →
Yakit Webfuzzer自动化测试中验证码识别插件的接入与热加载脚本实现 2026/9/29 19:18:05

Yakit Webfuzzer自动化测试中验证码识别插件的接入与热加载脚本实现

1. 自动化测试卡在验证码上的真实困境做过Web端自动化测试的人,大概率都经历过这样一个场景:接口逻辑跑通了,参数构造没问题,Webfuzzer的请求包也调好了,结果一发包,返回的全是“验证码错误”。尤其是登录、…

阅读更多 →
Source Insight 高效阅读 Linux 内核源码:符号跳转、引用查找与宏脚本实战 2026/9/29 19:18:05

Source Insight 高效阅读 Linux 内核源码:符号跳转、引用查找与宏脚本实战

简介:这份资源是面向嵌入式与Linux内核源码阅读者的Source Insight使用教程文档,适合刚接触大型C/C项目、希望摆脱vim与emacs复杂配置的开发者。教程围绕Source Insight这一Windows平台共享软件展开,讲解如何将Linux系统源码迁移至Windows分区…

阅读更多 →
C6678多核DSP开发实战:CCS环境搭建、工程创建与启动模式配置 2026/9/29 19:18:05

C6678多核DSP开发实战:CCS环境搭建、工程创建与启动模式配置

DSP开发这件事,入门门槛其实不在写算法,而在"让芯片先跑起来"。我见过太多人拿到C6678的开发板,CCS装了三遍,工程建了五遍,最后卡在"程序烧进去没反应"这一步——不是代码写错了,是启动…

阅读更多 →
在Dify中构建hindsight复盘机制:AI应用闭环实战 2026/9/29 19:18:05

在Dify中构建hindsight复盘机制:AI应用闭环实战

如果你做过一段时间的 AI 应用开发,应该见过这种让人头大的场景:客服机器人在同一类问题上反复翻车,代码生成助手改了第一处错误却带出第二处错误,内容助手永远记不住上次被你打回的那个格式问题。问题通常不在模型能力&#xff0…

阅读更多 →
AnythingLLM 本地优先 AI 智能体:从 RAG 知识库到 Ollama 模型对接实战 2026/9/29 19:17:58

AnythingLLM 本地优先 AI 智能体:从 RAG 知识库到 Ollama 模型对接实战

本地优先的 AI 智能体工具这两年冒出来不少,但真正能让我愿意在主力机器上长期跑、并且敢推荐给身边非技术朋友的,AnythingLLM 算一个。它解决的核心问题很朴素:你手里有一堆文档、笔记、PDF、网页剪藏,想让大模型基于这些私有内容…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉