尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Flutter for OpenHarmony 手语学习App:练习记录模块的架构设计与实现
做过一阵子移动端开发的人应该都有同感UI 写完、数据流跑通真正决定一个产品能不能留下用户的往往是那些不起眼的模块。手语学习 App 就是典型——词库再全、演示动画再流畅用户学完一遍之后没有任何记录、没有反馈、没有连续性的激励这个 App 很快就会沦为手机里一个占内存的图标。所以这次做手语学习项目的时候我把练习记录放到了和词库内容几乎同等的优先级上并且把 Flutter for OpenHarmony 作为整套技术路线来落地。这个项目做下来最大的体会是练习记录不是一个加张表、写个列表的小功能它本质上是学习类产品的隐形骨架。本文会从选题动机、环境搭建、词库设计一直拆到练习记录模块的表结构、存储选型和统计实现最后再复盘几个真机运行时的坑。如果你正准备在 OpenHarmony 上跑 Flutter或者正在设计练习、打卡、错题回顾这类功能这篇应该能给你一些可以直接抄作业的参考。1. 这个项目的起点手语学习需要记录而不是浏览1.1 用户需求与核心场景手语学习和普通语言学习最大的不同在于普通语言有文字、有发音碎片时间扫一眼就能获得输入手语是视觉语言要看手部动作、面部表情和口型的配合学习本身对整块时间的要求更高。很多初学者最初是被新鲜感驱动的翻了几十个词条之后心里其实没有锚点——今天学了哪几个词哪些词总是搞混上周练过的动作还能不能比划出来没有记录的学习基本等于没学。我在设计阶段把目标用户定义成两类。第一类是日常有沟通需求的听障人士家属、朋友或同事他们不需要考级只需要快速掌握高频生活词汇比如问候、就餐、出行、就医这些场景。第二类是自己有听力障碍、想系统学习手语表达的群体他们更需要练习的连续性和成就感。两类人的共同点是都希望用最短的路径进入高频词 互动练习的循环而不是被动地在一个巨大的词库里翻看。所以整个 App 的核心场景被切成三段浏览学习、主动练习、回顾统计。浏览学习解决看到并理解主动练习解决记住并能比划出来回顾统计解决知道自己到底练了多久、错在哪里、有没有进步。三段缺一段这个 App 就只是一个词典而不是学习工具。1.2 技术路线选择的理由技术选型上当时摆在面前的有三条路线OpenHarmony 原生 ArkUI、Flutter for OpenHarmony、纯 Web 套壳。原生 ArkUI 在 OpenHarmony 平台上最稳社区文档也相对完整但有一个现实问题这个项目后期大概率要同时覆盖其他移动平台至少在手机端和平板端都要跑起来。如果直接上原生UI 要写两套用户数据层代码也基本要重写。纯 Web 套壳开发最快但视频演示、手势动画、本地记录这类偏原生能力的交互体验会明显打折而且离线能力弱对听障用户的使用场景不友好。Flutter for OpenHarmony 是 OpenHarmony 社区维护的 Flutter 适配分支它的核心价值在于Dart 代码和 Widget 层基本不绑定具体平台OpenHarmony 只是其中一个渲染后端。这意味着我可以在标准 Flutter 工程里正常写业务逻辑编译时切到适配分支构建出 OpenHarmony 应用包后续如果要在其他平台复用业务层几乎不用动。当然适配分支不是官方主线更新节奏比主 Flutter 慢版本锁定的要求很严格。这个代价我完全能接受因为对个人项目来说可控的、稳定的技术栈比永远最新的技术栈更值钱。1.3 MVP 功能边界为了避免项目失控第一版的功能范围我卡得非常紧核心列表如下手语词库分类浏览按生活场景分类支持关键词搜索词条详情短语演示视频、文字释义、动作要点提示学习状态标记未学 / 学习中 / 已掌握练习模式从当前分类出题用户自我判断对错练习记录每次练习的会话、明细、正确率、连续天数统计页今日进度、最近七日趋势、历史练习列表视频上传和云同步在第一版里坚决不做。原因很直接练习记录这种用户私有数据第一版用本地存储就够了只要数据结构设计成一个可替换的 repository 接口以后上云同步只是换一个实现的问题视频资源如果一开始就想着搞后台管理内容生产流程会拖垮整个项目的节奏。做个人项目范围控制比功能堆叠重要得多。2. Flutter for OpenHarmony 环境搭建版本、分支与第一个能跑的 Demo2.1 工具链与版本组合环境搭建是这类项目的第一个门槛因为 Flutter for OpenHarmony 不是装上官方 Flutter 就能直接用的。央视三套不相关。我当时锁定的工具链组合大致如下组件选择说明Flutter 分支OpenHarmony 社区维护的 flutter_flutter 适配分支不是官方 main 分支版本必须锁死OpenHarmony SDK对应 LTS 版本和适配分支有版本对应关系构建工具hvigor 构建工具需要独立安装类似 Android 世界的 GradleDart SDK跟随 Flutter 分支自带不需要单独装本地属性手工配置 SDK 路径local.properties 里写 sdk.dir这里最容易被忽略的是版本对齐。Flutter 适配分支会对应 OpenHarmony SDK 的某个版本范围两个版本对不上编译阶段会报一堆非常绕的错误。我当时吃过这个亏后来养成一个习惯把版本组合写进项目 README 的最前面固定住避免隔几个月回来继续开发时完全想不起当时用的是哪个版本。2.2 工程初始化两条路线初始化工程有两条路线。路线 A先用标准 Flutter 建工程再切到适配分支构建。执行flutter create hand_sign_app把 lib 目录写好然后用适配分支构建链生成 OpenHarmony 平台目录。路线 B直接从社区提供的 OpenHarmony Flutter 样例工程拷贝骨架清掉样例里的业务代码再开始写。路线 B 能快速看到效果但样例工程经常带了一堆自定义依赖清理不干净会一直报错而且你不太清楚哪些代码是可以删的。我实际选用的是路线 A但额外做了一步先用一个空工程把 OpenHarmony 构建链完整跑通确认环境没问题再开始写业务代码。这一步看起来浪费一两个小时实际上非常值得。如果上来直接写业务代码写完一大片再构建一旦环境有问题你根本分不清报错到底来自环境还是来自自己的代码排查难度翻倍。2.3 第一次构建的坑第一次构建踩的坑值得单独列一下。第一个是 OpenHarmony SDK 路径没被构建脚本探测到。适配分支的构建脚本虽然会自动探测但探测失败时需要手工在工程的 local.properties 里写sdk.dir路径里不要带中文和空格否则解析阶段就会出现很诡异的字符错误。第二个是 JDK 版本。OpenHarmony 的构建链对 JDK 版本很敏感我一开始用了较新的大版本结果构建时直接提示某个 Task 类不兼容。换成构建链要求的对应版本后问题立刻消失。所以遇到构建工具类报错先排查 JDK这是成本最低的一步。第三个是签名配置。构建 OpenHarmony 应用包需要配置签名文件调试阶段如果不签名装到开发板或模拟器上会被拒绝安装。当时我用的是调试证书但证书和 profile 文件路径都必须填到构建配置里才能通过。这三个问题单个都不难麻烦的是报错信息很绕网上能查到的资料也少。我的经验是遇到看不懂的构建错误先怀疑版本组合再怀疑配置路径最后才去查业务代码。环境问题首轮开发至少占了三成时间但排干净之后后面写代码就非常顺。3. 词库与学习流程数据模型先行3.1 词条数据模型设计手语学习 App 里最核心的数据是词条。我把词条模型设计成可扩展的字段如下id语义化字符串比如greet_hello而不是自增数字方便跨分类引用categoryId所属分类title中文名称subtitle释义说明mediaUrl演示视频地址或图片地址tips动作要点比如双手掌心相对向上摆动difficulty1 到 3 级的难度status学习状态未学 / 学习中 / 已掌握有个设计细节很关键status 是用户维度的数据而不是词库本身的属性。同一个词条不同用户的学习进度完全不同。如果把 status 直接放进词条表后续同步、筛选、统计都会变得非常别扭。所以词库本身只保留静态内容用户相关状态全部放在独立的表里通过词条 id 关联。3.2 分类浏览与学习详情页首页分类浏览用标准的 ListView 实现。数据来源第一版直接写死在一个本地 JSON 文件里结构就是分类 - 词条数组。首版不接网络拉词库是因为手语学习的使用场景里离线比在线重要得多而且词库资源文件本身不大打进应用包里完全没问题。听障用户在户外、地铁这些网络不稳定场景下打开 App离线可用是刚需。学习详情页是整个 App 的视觉重点。手语是动作语言单词页最重要的部分是演示。我以演示视频为主图片为辅。视频比静态图能更完整地表达手语的连贯性但视频文件体积和播放器兼容性都需要处理。这里我留了一个降级策略如果某个词条的视频不可用页面用三到五张关键帧图片拼成一个简单动画。虽然不如视频精准但能保证基础学习功能不挂。这种保底方案在真实项目里非常实用不要为了追求完美体验把所有功能都绑死在单一资源上。3.3 学习状态与收藏逻辑学习状态我定义为三种未学、学习中、已掌握。用户在详情页点一下标记为已掌握这个词条的 status 就会更新到本地数据库。练习模式会优先抽取学习中状态的词条因为这些词正处在遗忘边缘是练习价值最高的部分。收藏功能和学习状态是两套数据。我单独建了一张 favorites 表绑定词条 id避免每次查询都要拼一堆条件。首版收藏只是在详情页放一个心形按钮没有做单独的收藏夹页面。但表已经建好了后面如果要做我的收藏夹分组学习就是非常自然的功能延伸不需要动现有结构。4. 练习记录模块从表设计到统计展示的完整实现4.1 要记录哪些字段设计练习记录模块时我反复问自己三个问题用户什么时候开始练习练习过程中发生了什么练习结束之后要看到什么这三个问题直接映射成数据字段。一次练习是一个会话 session会话有开始时间和结束时间会话过程中每个词条的答题情况是一条明细 item会话结束后要能算出正确率、总题数、用时和连续天数。字段设计如下会话主表id主键started_at开始时间毫秒时间戳finished_at结束时间戳mode练习模式比如普通练习、错题重练total_count总题数correct_count正确数明细表id主键session_id关联会话主表word_id词条 idis_correct是否正确duration_ms该词条耗时created_at记录时间我没有在会话主表里冗余存一个正确率字段因为正确率随时可以由 total_count 和 correct_count 算出来。冗余字段最大的问题是更新时会话数据如果没写全页面展示就会出现两个数对不上的尴尬情况。能算出来的指标就不要存。4.2 存储方案选型与表结构练习记录的数据量会随使用时间持续增长而且统计页需要按日期做聚合查询所以我第一反应就是关系型数据库而不是 shared_preferences 之类的键值存储。在 OpenHarmony 环境下我原本计划直接用 Flutter 生态里常用的 sqflite但实测发现底层 sqlite3 的适配在这个分支上还不稳定。最后做了一个折中表结构仍然按 SQL 设计数据库访问全部封装在一个 repository 接口后面。底层优先用本地 SQLite 兼容方案如果目标设备上插件跑不通就退回 JSON 文件存储只保证 repository 接口不变。这个 repository 抽象是这次项目里最值得的一步。因为存储方案的调整没有影响到任何页面代码页面只依赖接口不依赖具体实现。SQLite 建表语句如下CREATE TABLE practice_sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, started_at INTEGER NOT NULL, finished_at INTEGER NOT NULL, mode TEXT NOT NULL DEFAULT normal, total_count INTEGER NOT NULL DEFAULT 0, correct_count INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE practice_items ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id INTEGER NOT NULL, word_id TEXT NOT NULL, is_correct INTEGER NOT NULL, duration_ms INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL ); CREATE INDEX idx_practice_items_session ON practice_items(session_id); CREATE INDEX idx_practice_sessions_time ON practice_sessions(started_at);索引不是可选项。统计页经常做按时间段的聚合查询如果没有索引记录多了以后查询速度下降非常明显。首版可能只有几百条数据用户感受不到差异但一旦用户真的坚持每天练习几个月积累下来就是上千条记录这时候有没有索引完全是两种体验。4.3 练习交互闭环练习模块的交互我把它设计成回忆—确认—记录三步闭环。第一步页面展示一个词条的文字释义和动作提示用户在心里回忆手语动作第二步用户点击显示动作看演示视频或者自己比划一遍第三步用户点击答对或答错系统记录该词条是否正确以及从题目出现到用户做出判断的时长然后进入下一个词条。这个流程交互上非常简单但对应的是主动回忆的学习机制。如果直接放视频让用户看效果和浏览学习没有区别练习就失去了意义。耗时字段也有大用答对但耗时很长的词条说明还不够熟练以后要做薄弱词条列表duration_ms 就是最重要的判据。核心状态管理我用了 Riverpod。相比手写 setStateRiverpod 最大的好处是练习页、结果页、统计页能共享同一个状态源。练习会话结束之后结果页可以直接从同一个状态源拿数据不需要通过路由参数或者全局单例传递。核心控制器的大致逻辑如下class PracticeSessionController { final PracticeRepository _repository; final ListWordItem _roundWords; int _index 0; int _correctCount 0; int _totalCount 0; int _roundStartAt 0; void submitAnswer(bool isCorrect, int durationMs) { if (isCorrect) _correctCount; _totalCount; // 实时记录明细避免中途退出丢数据 _repository.logAnswer( _roundWords[_index].id, isCorrect, durationMs, ); _index; } FuturePracticeSummary finish() async { final summary PracticeSummary( totalCount: _totalCount, correctCount: _correctCount, duration: DateTime.now().millisecondsSinceEpoch - _roundStartAt, ); return _repository.saveSession(summary); } }这里有一个很重要的细节明细是实时插入的不是等一轮练习结束再批量写入。移动端场景里用户很可能在练习中切走甚至应用被系统杀掉。如果等到结束才写库中途完成的答题记录就全丢光了。实时插入虽然多几次数据库写操作但对几十题一轮的体量来说完全没有压力换来的是数据安全。4.4 连续天数与正确率统计统计页最有价值的两个指标是连续练习天数和正确率走势。连续天数就是常见的打卡统计算法不复杂但有一个非常容易踩坑的地方日期必须按本地时区算不能直接用 UTC 时间戳比较。举个例子用户在北京时间的晚上十一点练习换算成 UTC 时间可能是同一天下午三点也可能因为时区偏移变成前一天。如果直接拿时间戳换算出 UTC 日期来判断今天是否已经练习一次正常的晚间练习很可能被归到错误的日期里连续天数直接就断了。所以要先统一转成本地日期再比较代码逻辑如下int calculateStreak(ListDateTime practiceDates, DateTime today) { final dateSet practiceDates .map((d) DateTime(d.year, d.month, d.day)) .toSet(); var streak 0; var cursor DateTime(today.year, today.month, today.day); while (dateSet.contains(cursor)) { streak; cursor DateTime(cursor.year, cursor.month, cursor.day - 1); } return streak; }正确率走势的核心也在日期处理上。把每天的 total_count 和 correct_count 按天分组聚合就能画出一条七日趋势。这个图表我是用自绘画布组件实现的没有引入第三方图表库。因为首版统计维度就两个引入图表库反而增加包体积和维护成本不划算。4.5 练习历史与今日进度展示练习历史列表页按时间倒序展示所有会话记录每条记录显示日期、练习模式、题数和正确率。点击某一条可以展开当轮的明细看到每个词条当时的答题结果。这个列表把整个记录形成闭环用户先浏览学习再主动练习最后回到统计页回顾。今日进度我用一个简单的进度环展示在统计页顶部。数据源来自当天所有会话的累计值。这里需要注意一个用户当天可能练了好几轮统计今日数据时不能只取最后一条会话必须把 started_at 落在当天范围内所有会话都聚合起来。历史记录列表则另外负责展示每一次会话的独立数据二者维度不同。5. 真机运行实测与一次完整的踩坑复盘5.1 在 OpenHarmony 设备上跑通的过程业务代码写完之后真正拿到 OpenHarmony 设备上运行是另一回事。我的节奏是先跑空工程做能力验证再逐步把业务模块加回来。第一次在设备上看到词条列表正常滑动、演示视频能播放、练习记录能写进本地数据库确实比看到 UI 一蹴而就还有成就感因为这一步验证的是整条技术链路的可行性。但运行实测也暴露了一个问题Flutter for OpenHarmony 分支在渲染视频时的兼容性不够稳定。标准 video_player 插件在这个分支下如果没有经过适配播放会出现黑屏或者首帧延迟明显。我的处理思路是先查清楚插件适配范围把不强依赖视频内核的功能优先做扎实。这正好印证了方案决策阶段的判断——练习记录模块本身不依赖视频即使视频组件临时出问题记录和统计功能不会跟着崩。5.2 练习记录模块的三个边界问题真正容易出问题的全在模块边界上。第一个是应用在练习中途被系统回收后的数据一致性。因为明细是实时写入的但如果会话主记录要等到 finish 才创建就会出现明细存在、却找不到对应 session 的孤儿数据。我的解决方式是在一轮练习开始时就主动创建一条 session 记录状态标记为进行中后续每条明细都挂在同一个 session_id 下面finish 时再回填 total_count、correct_count 和结束时间。这样即使中途退出至少能从明细数据里恢复出半个会话不至于彻底丢。第二个边界是数据库结构升级。首版上线之后如果要加字段比如给 practice_items 表增加一个是否由错题重练产生的标记直接在旧库上读新字段一定会崩。所以 repository 层必须支持数据库版本号每次结构变更把 version 加一在 onUpgrade 回调里执行增量迁移语句。为这个项目写一个完整的迁移逻辑比临时改表结构安全得多。第三个边界是时区问题就是前面连续天数里提到的情况。这个坑在开发自测时特别容易被忽略因为测试都发生在本地时区代码在本地永远是正确的可一旦用户跨时区使用问题马上暴露。所以记录模块所有时间字段统一存毫秒时间戳展示层再转本地时间格式。千万不要存 2025-01-01 10:00 这种字符串字符串格式在排序、比较、聚合时都会成为隐患。5.3 对后续开发有用的三个习惯这个项目做完我总结出三个值得固化的开发习惯。第一数据访问一律走 repository 层不允许在页面组件里直接写 SQL 或文件操作。哪怕第一版只有本地存储这个抽象也能保证后续添加网络同步、换数据库实现时上层页面代码完全不动。第二所有时间字段统一存毫秒时间戳。存字符串格式的日期时间相当于给自己埋雷排序、跨时区、聚合查询每一样都会出问题。第三每次构建前固定检查版本组合。Flutter for OpenHarmony 这类适配分支对版本太敏感升级任何一个组件之前都必须先确认兼容关系否则你会发现一个很小的依赖升级会引发整个工程构建失败而且报错信息还特别绕。最后再分享一点实际开发完的体会。练习记录模块看起来是加个表、写个列表的小活但它其实是整个学习类 App 的隐形骨架。词库内容决定用户最初是否打开这个 App而记录和反馈决定用户能不能留下来。尤其是手语学习这种需要长期坚持的场景一次练习后的正确率、一条连续打卡记录比任何精心设计的启动页都更能留住人。如果后续继续做这个方向我会优先把错题重练和薄弱词条推荐补上让已经积累下来的练习数据真正开始指导用户的学习路径而不是只停留在展示层面。
RELATED

相关推荐

TCP可靠传输原理与粘包拆包实战:从滑动窗口到消息边界

TCP可靠传输原理与粘包拆包实战:从滑动窗口到消息边界

如果你写网络程序时只把注意力放在业务逻辑上,迟早会在“可靠性保障”和“粘包拆包实战”这两个问题上栽跟头。我在一个跨平台系统的联调阶段,遇到客户端明明调用了两次send,服务端一次recv却吐出拼接好的两条消息,排查了很久才发…

📅 2026/10/14 3:55:02
轻量网页监控系统实践:Windows本地部署与站点可用性巡检

轻量网页监控系统实践:Windows本地部署与站点可用性巡检

做网站运维、后端开发或个人站长的朋友,大概都有过这样的经历:半夜网站突然挂了,第二天早上才从用户投诉里知道;或者某个页面标题被篡改、挂了不良广告,等搜索引擎收录了才后知后觉。我们平时盯着服务器日志、看着监控…

📅 2026/10/14 3:50:02
企业级AI客服五层架构:RAG+工具+记忆+流式+兜底实战

企业级AI客服五层架构:RAG+工具+记忆+流式+兜底实战

1. 这不是又一个“AI客服Demo”,而是一套可落地的企业级智能服务骨架最近在某金融类客户现场做系统升级支持时,团队反复被一个问题卡住:客服知识库更新频繁,但传统规则引擎关键词匹配的响应准确率始终卡在72%上不去;更…

📅 2026/10/14 3:50:02
MORE NEWS

更多资讯

📰

PyPTO入门:用Hello World理解结构化步骤调用与批量处理

1. 为什么直播主题要叫“PyPTO: Hello, World!”而不是“PyPTO 入门”1.1 PyPTO 到底在解决什么问题先说清楚背景。日常写脚本,很多人都是这样:定义一个函数,传参,调用,print,完事。逻辑简单的时候没问题&a…

📰

化工仪表与控制工程师手册:从选型到故障排查的案头工具

简介:《流程工业仪表工程师手册》是一份面向流程工业领域仪表与控制工程师的专业工具书资源,由王发兵担任主任委员、张同科等任副主任委员的编委会合力编著,编委成员均来自仪表工程一线,具备丰富的实践经验与技术专长。手册内容覆…

📰

CTF-MISC从零到一:核心题型分类、工具链与解题思维全解析

1. 从零拆解CTF-MISC:这个方向到底在考什么刚接触CTF那会儿,我一度觉得MISC是所有方向里最“不讲道理”的一个。Web有漏洞类型可循,Reverse有汇编逻辑可推,Crypto有数学公式可算,唯独MISC,每次拿到题目都像…

📰

AnyPS5:PS5存储与备份的实用运维工具集

要说最近这段时间我最常被问到的折腾向话题,一个是“主机存储满了怎么办”,另一个是“存档怎么才能不丢”。很多人聊起PS5,第一反应都是游戏阵容和画质,但真到了自己玩的阶段,一台主机要面对的事情远不止“开机玩游戏”…

📰

Figma-Forge、OpenCAD-G、TestRover:让AI生成可嵌入工程链路的结构化交付物

1. 这不是“AI画画”那么简单:为什么标题里三个工具能真正撬动专业工作流最近在几个技术社区和设计团队的内部分享会上,我反复听到同一个问题:“我们试过MidJourney、DALLE,也用过Copilot写代码,但为什么一到出施工图、…

📰

FineReport报表服务API调用实践:告别爬虫,拥抱RESTful

1. 这不是“爬虫”,是报表服务的合规调用接口实践很多人看到标题里的“爬虫”两个字,第一反应就是“绕过登录”“模拟点击”“解析HTML表格”——这恰恰是本项目最需要先划清界限的地方。我带过的几个模拟项目X中,有团队在初期就误入歧途&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬