
近一年我一直在折腾 React Native for OpenHarmony前后做了几个小工具练手最完整、踩坑最多、最后能放心拿出去演示的就是这个数学练习 App。如果你正打算在 OpenHarmony 设备上跑 React Native 应用又不想从零啃 ArkTS 原生 UI那这篇实战总结应该能帮你省掉不少弯路。我会把项目从设计、初始化、核心功能到问题排查的整个流程拆开来讲重点不是贴官方文档而是把我实测过的方案、碰过的雷、改过的代码原样交代清楚。数学练习这个选题很适合作为 OpenHarmony 上 RN 实战的切入点业务逻辑清晰、界面交互集中、不依赖复杂系统能力又能充分用到 RN 的布局、状态管理、列表渲染和原生模块桥接。做完这一个项目你对 React Native for OpenHarmony 的开发手感基本就建立起来了。1. 项目背景与整体设计思路1.1 为什么在 OpenHarmony 上用 React Native先说结论不是为了炫技是因为团队之前的前端技术栈主要集中在 React Native 上如果 OpenHarmony 设备全部用 ArkTS 重写一遍人力成本和维护成本都吃不消。而 React Native for OpenHarmony 这个方向正好可以把已经写好的业务逻辑、状态管理、工具函数直接搬过来UI 层做少量适配就能跑。这里要澄清一个常见误解很多人以为把 RN 项目迁移到 OpenHarmony 就是改几个配置实际上 React Native for OpenHarmony 并不是 React Native 官方分支而是一个独立的框架实现。它复用了 React Native 的上层 JavaScript 架构但把渲染层、原生模块层全部对接到了 OpenHarmony 的 ArkUI 和 Native API 上。也就是说你写的 JS 代码大概率原封不动但底层组件映射、样式对齐、原生模块调用方式都需要重新验证。举一个最简单的例子RN 原生的ScrollView在 OpenHarmony 上对应的是 ArkUI 的Scroll组件两者滚动事件和嵌套路逻辑几乎一致但滚动条样式、惯性效果的参数并不完全一样。这种看似兼容、细节不同的情况在整个实战过程中比比皆是。所以选 RN for OpenHarmony 的前提是你愿意花力气去验证每个用到的组件和 API。1.2 数学练习 App 的功能拆解我做的这个数学练习 App目标用户是小学阶段的孩子核心场景是每天在家完成 10 到 20 道口算题家长能在事后看到正确率和耗时。功能可以拆成四个模块随机出题引擎支持加减乘除四种运算按难度控制运算数范围、是否出现负数、是否出现除法余数。练习会话流程连续答题、每题即时判定、显示得分和用时支持中途暂停。错题本与历史记录把错题单独归档记录每次练习的时间、题数、正确率和总耗时。简单的统计视图用列表展示最近七天的练习量趋势和正确率。从技术角度看这个功能集刚好覆盖了 RN 开发的常用能力面组件渲染、用户输入、定时器、列表、本地持久化以及一个自定义原生模块用来读写 OpenHarmony 侧的文件存储。没有复杂的动画和网络需求非常适合作为上手机型的第一个完整项目。1.3 整体架构与目录设计我采用的三层结构目录如下src/ ├── components/ # 纯 UI 组件 │ ├── QuestionCard.js │ ├── AnswerInput.js │ └── ScoreBoard.js ├── engine/ # 核心业务逻辑不依赖 RN │ ├── generator.js # 出题算法 │ ├── difficulty.js # 难度配置 │ └── validator.js # 答案校验 ├── storage/ # 数据持久化封装 │ ├── nativeBridge.js # 原生模块调用层 │ └── recordStore.js # 练习记录读写 ├── screens/ │ ├── PracticeScreen.js │ ├── HistoryScreen.js │ └── StatsScreen.js └── App.js这个设计的核心思想是把纯 JS 业务逻辑和 RN UI 层完全解耦。出题算法、答案校验这些模块不 import 任何react-native组件将来就算要移植到别的平台这批代码可以原样带走。storage层做了一层封装上层调用时只会看到saveRecord、loadHistory这类方法不会感知底层到底是走 AsyncStorage 还是走原生文件读写。2. 环境准备与工程初始化2.1 版本选型与踩坑项目初始化遇到的第一个坑就是版本对齐。React Native for OpenHarmony 对 OpenHarmony SDK、构建工具链、Node 版本都有明确要求不是随便配一套就能跑。我一开始用了较新的 Node 22结果构建时出现一串依赖编译报错翻了下社区讨论才发现当时框架对 Node 20 LTS 的兼容性最好。我的环境配置OpenHarmony SDK使用设备对应的正式发布版本不要用 Beta。Node.js20 LTS。DevEco Studio保持最新稳定版。框架包版本和 SDK 大版本配套的发布版本。构建工具使用工程自带的 hvigor版本号以工程模板为准。版本踩坑的关键教训先跑通官方提供的模板工程再往上叠加业务代码。如果你连 hello world 都起不来多半是环境问题而非代码问题此时优先检查版本组合而不是去改业务代码。2.2 OpenHarmony 工程搭建OpenHarmony 工程的创建比标准 React Native 工程多一个步骤你需要在 DevEco Studio 里先创建一个标准的 OpenHarmony 空工程然后手动接入 React Native 相关依赖。工程结构大概是这样的MyPracticeApp/ ├── entry/ # OpenHarmony 应用入口 │ ├── src/main/ │ │ ├── ets/ # ArkTS 原生代码 │ │ │ ├── MainAbility/ │ │ │ └── pages/ │ │ ├── resources/ │ │ └── module.json5 │ ├── build-profile.json5 │ └── oh-package.json5 ├── hvigor/ ├── node_modules/ ├── package.json └── App.js在oh-package.json5中添加依赖时需要把 React Native 相关的原生模块都加上包括主框架包、核心组件模块以及你想用的原生能力模块比如存储、网络。这里建议直接用官方模板生成的版本号不要手写手写版本极容易出现依赖解析失败。接入完成后原生侧还要做两步配置一是把 RN 的入口界面注册到 OpenHarmony 的路由表里二是在module.json5里声明相关权限比如需要在本地写文件就得声明对应的存储权限。我第一次漏掉了权限声明结果运行期写入数据时直接静默失败不会有报错弹窗排查了很久。2.3 RN 侧入口与启动配置RN 侧入口和标准 RN 项目基本一样// index.js import { AppRegistry } from react-native; import App from ./src/App; AppRegistry.registerComponent(MathPractice, () App);需要注意MathPractice这个名字必须和原生侧加载组件时使用的名字严格一致否则会出现请求的组件不存在的报错而且这个报错是发生在启动阶段页面会直接白屏。开发阶段我使用 Metro 作为 JS Bundle 的本地服务。OpenHarmony 真机/模拟器连接 Metro 时IP 地址要手动配置到原生工程里。启动命令不需要特殊魔改标准 RN 的npx react-native start就能跑。运行期框架会通过 Metro 拉取 JS Bundle因此要保证设备与开发机在同一个局域网并关闭防火墙限制。我踩过的一个典型现象是代码改了Metro 也热更新了但真机上界面毫无变化。后来发现是原生侧的 bundle 加载模式还停留在从本地文件加载而非从 Metro 服务加载。这个配置项位于原生代码的研发生命周期逻辑中必须改成 debug 模式才会请求 Metro。3. 数学题生成引擎实现3.1 难度分级模型数学练习 App 的核心竞争力在出题质量而不是 UI 花哨。我的出题引擎按三个难度等级设计基础easy20 以内加减法结果不出现负数适合幼小衔接。进阶medium100 以内加减法 乘法口诀表不出现负数结果不超过 100。挑战hard混合四则运算允许出现负数除法要求整除、无余数。用代码体现一个可配置的模型export const DIFFICULTY_LEVELS { easy: { operators: [, -], operandRange: [1, 20], allowNegative: false, maxResult: 100, }, medium: { operators: [, -, ×], operandRange: [2, 99], allowNegative: false, maxResult: 100, division: false, }, hard: { operators: [, -, ×, ÷], operandRange: [2, 99], allowNegative: true, maxResult: 200, divisionIntegerOnly: true, }, };这种配置驱动的方式有个好处后面想加难度等级比如两位数乘法只需要新增一个配置对象不需要改动生成算法的主逻辑。项目上线后我实际加过一个advanced等级整个改动不到十行代码。3.2 随机出题算法出题算法的核心不是简单的Math.random()而是需要满足几类约束减法结果不能为负、除法必须整除、乘法结果不能超过学段认知范围。我在generator.js里是这样处理的import { DIFFICULTY_LEVELS } from ./difficulty; function randBetween(min, max) { return Math.floor(Math.random() * (max - min 1)) min; } function pickOperator(operators) { return operators[Math.floor(Math.random() * operators.length)]; } export function generateQuestion(level medium) { const config DIFFICULTY_LEVELS[level]; const op pickOperator(config.operators); let operandA randBetween(config.operandRange[0], config.operandRange[1]); let operandB randBetween(config.operandRange[0], config.operandRange[1]); // 减法结果不为负 if (op - !config.allowNegative operandA operandB) { [operandA, operandB] [operandB, operandA]; } // 除法必须整除先随机出商再反推被除数 if (op ÷) { operandB randBetween(2, 9); operandA operandB * randBetween(2, 12); } const answer calculateAnswer(op, operandA, operandB); return { id: Date.now() Math.random(), text: ${operandA} ${op} ${operandB}, answer }; } function calculateAnswer(op, a, b) { switch (op) { case : return a b; case -: return a - b; case ×: return a * b; case ÷: return a / b; default: return 0; } }这里最关键的反直觉点在于除法题目的生成顺序是先定商再反推被除数而不是先随机两个数再做除法判断。前者能保证 100% 整除后者会产生大量需要丢弃的随机数而且当范围较大时效率很低生成的题目分布也不均匀。3.3 答案校验与防呆设计答案校验这块我踩过一个很经典的浮点坑。calculateAnswer里除法返回的是 JS 数字整除场景下如36 / 6 6是精确的但19 / 5这类不整除的题目在hard模式下并不允许出现所以我强制了divisionIntegerOnly。即便如此JS 的浮点运算仍然可能出现0.1 0.2 ! 0.3这类问题为了稳妥校验时我做了容差比较export function isAnswerCorrect(expected, userInput) { if (userInput || userInput undefined || userInput null) { return false; } const numericInput Number(userInput); if (Number.isNaN(numericInput)) { return false; } return Math.abs(numericInput - expected) 0.0001; }答题交互上的防呆设计也很重要。孩子做题的时候经常会在输入框里乱点、快速提交空数据或者输入前导零。我的处理是输入框的内容在提交前用String.trim()去掉空格。空输入、非数字输入直接判为错误但只做提示不计入错题本防止误统计。每题首次答错后允许再次尝试当次尝试仍算错题但最终得分以首次结果为准。提交按钮在收到响应前会短暂置灰300ms避免双击触发重复判定。这些看似很细的东西实际试运行几天后就会发现是刚需孩子的操作习惯和成人完全不一样防呆做得好不好直接决定 App 能不能真正被日常使用。4. UI 实现与组件适配实战4.1 RN 组件与 OpenHarmony 组件的映射关系React Native for OpenHarmony 在架构上做了一个关键的桥接层JS 侧的组件声明最终会映射到 ArkUI 的原生组件上。这就意味着你在写 RN 的View时原生侧渲染的其实是 ArkUI 的某个布局容器如果你对 ArkUI 组件体系有不了解的地方UI 适配时容易出现无法理解的细节偏差。我在项目里用到的组件映射关系如下React Native 组件OpenHarmony ArkUI 侧实现实际适配注意点ViewColumn / Row / StackflexDirection 的表现逻辑与 CSS 接近但默认主轴方向不同建议显式声明TextText字体渲染差异明显中文字号建议用默认值手写小字号会显示过小TextInputTextInput获取焦点、弹出键盘的行为需要原生侧配置 adjustResizeScrollViewScroll嵌套滚动、到达底部事件的命名和参数位置不同FlatListList 容器 ForEach大数据量滚动更接近原生 List但 item 更新逻辑需要手动做 key 规划Pressable触摸事件封装组件按压态样式需要传入额外的 state 映射ModalDialog / bindSheet弹出动画、遮罩层参数完全不一致尽量少用复杂弹窗特别注意FlatList的keyExtractor要保证 key 的唯一性和稳定性。我发现如果 key 里有日期字符串如2025-03-01只要列表跨天刷新key 不变但数据却变了会导致列表项内容不刷新的诡异问题。后面统一改用日期 索引 随机短码的组合才解决。4.2 练习页面的布局实现练习页面的布局逻辑顶部是进度条和得分栏中间是题目大卡片底部是答案输入区和操作按钮。核心布局如下View style{styles.container} View style{styles.header} Text style{styles.progressText}第 {currentIndex 1} / {totalCount} 题/Text Text style{styles.scoreText}得分{score}/Text /View View style{styles.questionCard} Text style{styles.questionText}{currentQuestion.text} ?/Text /View AnswerInput value{inputValue} onChangeText{setInputValue} onSubmit{handleSubmit} / View style{styles.buttonRow} Pressable style{styles.submitBtn} onPress{handleSubmit} Text style{styles.btnText}提交/Text /Pressable Pressable style{styles.nextBtn} onPress{handleNext} Text style{styles.btnText}下一题/Text /Pressable /View /View样式上有一个非常重要的适配细节flexDirection 一定要显式写。标准 RN 默认主轴是 column但 ArkUI 的 Row/Column 组件天然有主轴方向如果 RN 侧的样式没有明确声明某些版本会把布局方向搞反。我建议在每个容器上都显式声明flexDirection: column或row不要把默认值交给框架去猜。另一个适配细节是边距和尺寸单位。RN 的style中数值默认按逻辑像素理解OpenHarmony 侧使用的是 vpvirtual pixel实际映射基本是 1:1但某些系统默认字体大小会额外乘一个缩放系数导致 Text 实际渲染尺寸比设计稿大一圈。遇到文本溢出、卡片内文字截断的情况优先检查的不是布局代码而是系统字体设置。4.3 键盘交互与输入体验数学练习场景里数字输入是最高频的操作。我测试下来发现一个现象OpenHarmony 上 RN 的TextInput弹出系统键盘之后底部按钮被遮挡而 RN 自带的KeyboardAvoidingView在这套框架上的表现不稳定有时避让过度有时完全无效。我最终的方案是弃用KeyboardAvoidingView改用布局上的固定操作区 滚动内容区View style{styles.root} {/* 上方可滚动区域 */} ScrollView style{styles.scrollArea} contentContainerStyle{styles.scrollContent} {/* 题目卡片和提示 */} /ScrollView {/* 底部固定输入与提交区不参与 ScrollView 滚动 */} View style{styles.fixedBottomArea} TextInput ... / Pressable onPress{handleSubmit}提交/Pressable /View /View这样键盘弹出时固定区会被顶上去但不会和滚动区的内容重叠体验上反而比传统KeyboardAvoidingView更直接。代价是题目区域在键盘弹出时会缩小需要在scrollContent里设置足够的底部 padding。输入法还有个细节TextInput的keyboardType只设置numeric在部分定制键盘上不生效。我在hard模式允许负数时用户需要输入负号而数字键盘经常没有负号键。解决方法是额外提供一个 - 按钮点击后在前缀增加负号这样绕开了键盘类型限制。5. 状态管理与数据持久化5.1 练习会话状态管理数学练习的会话状态里需要跟踪的数据包括当前题目序号、用户输入内容、得分记录、错题列表、计时器秒数。状态量不算特别大但相互关联直接用多个useState会让逻辑变得很散。我用useReducer把会话状态收敛成单一对象const initialState { currentIndex: 0, questions: [], answerMap: {}, score: 0, wrongQuestions: [], elapsedSeconds: 0, isFinished: false, }; function practiceReducer(state, action) { switch (action.type) { case START_SESSION: return { ...initialState, questions: action.payload.questions }; case SUBMIT_ANSWER: { const { index, input, isCorrect } action.payload; const score (state.score || 0) (isCorrect ? 10 : 0); const wrongQuestions isCorrect ? state.wrongQuestions : [...state.wrongQuestions, { question: state.questions[index], userInput: input }]; return { ...state, score, wrongQuestions }; } case NEXT_QUESTION: return { ...state, currentIndex: state.currentIndex 1 }; case TICK: return { ...state, elapsedSeconds: state.elapsedSeconds 1 }; case FINISH: return { ...state, isFinished: true }; default: return state; } }使用useReducer的好处不只是代码看着清爽更重要的是所有状态变更都走同一个入口方便记录操作日志、调试、甚至做操作回放。后面我排查一个得分莫名丢失的问题时就是靠打印完整的 reducer action 列表迅速定位的。5.2 原生存储桥接React Native for OpenHarmony 内置的存储方案和标准 RN 的 AsyncStorage 并不完全相同。我刚开始直接使用了框架自带的存储封装结果发现它只能存轻量键值对而我需要保存的是结构化的练习历史 JSON。为了保险我选择写一个自定义原生模块桥接 ArkTS 侧的文件读写能力。原生侧 ArkTS 模块的核心逻辑import { fileIo } from kit.CoreFileKit; import { common } from kit.AbilityKit; export class StorageModule { private context: common.UIAbilityContext; constructor(context: common.UIAbilityContext) { this.context context; } writeFile(fileName: string, content: string): void { const filePath this.context.filesDir / fileName; const file fileIo.openSync(filePath, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); fileIo.writeSync(file.fd, content); fileIo.closeSync(file); } readFile(fileName: string): string { const filePath this.context.filesDir / fileName; const file fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY); const buffer new ArrayBuffer(4096); const readLen fileIo.readSync(file.fd, buffer); fileIo.closeSync(file); return String.fromCharCode.apply(null, new Uint8Array(buffer.slice(0, readLen))); } }RN 侧封装成 Promise 风格import { NativeModules } from react-native; const { StorageModule } NativeModules; export async function saveRecord(record) { try { const fileName practice_${record.date}.json; await StorageModule.writeFile(fileName, JSON.stringify(record)); } catch (e) { console.warn(保存记录失败: ${e?.message}); } } export async function loadRecords(dateRange) { // 查询指定日期范围内的所有练习记录 // 这里通过原生侧批量读取文件内容并解析 JSON }这条链路是打通JS 层业务逻辑 - 原生模块 - 文件系统的标准姿势。实际开发中RN 的NativeModules在 OpenHarmony 框架下的调用方法和标准 RN 差别不大但回调风格有区别。我建议统一用Promise风格并且在原生模块侧写清楚错误回调不要依赖框架自动兜底否则 JS 侧容易出现永久 pending 的 PromiseUI 就卡住了。5.3 历史记录与统计聚合每天一次练习产生一条记录字段结构如下{ date: 2025-03-15, level: hard, total: 20, correct: 17, elapsedSeconds: 325, wrongQuestions: [ { question: 56 - 89, userInput: -33, correctAnswer: -33 } ] }历史记录页用FlatList展示每条记录显示日期、难度、正确率、耗时。统计页则把最近七天的数据聚合成一组柱状图——我没有引入重型图表库而是用 View 的宽度百分比手绘简单柱子这在 OpenHarmony 的 RN 环境下更稳、更可控。统计聚合逻辑export function aggregateStats(records, days 7) { const result []; for (let i days - 1; i 0; i--) { const date formatDate(Date.now() - i * 86400000); const dayRecords records.filter(r r.date date); const total dayRecords.reduce((sum, r) sum r.total, 0); const correct dayRecords.reduce((sum, r) sum r.correct, 0); result.push({ date, total, correct, rate: total 0 ? 0 : Math.round((correct / total) * 100), }); } return result; }这里有个细节折线、柱状图这类视觉元素如果坚持用 RN View 拼图表越复杂维护成本越高。但对这种只展示两组数据的统计场景手绘方案反而比引入图表库省心得多因为 OpenHarmony 的 RN 图表库支持度参差不齐轻量数据不值得背这个兼容性包袱。6. 高频问题与排查实录6.1 页面白屏与 JS Bundle 加载失败这类问题在开发期出现频率最高。表现就是 App 启动之后直接白屏没有任何报错。排查步骤按顺序走先看 Metro 窗口是否正常打印了 bundle 请求日志如果没有请求说明设备根本没有访问到 Metro。确认原生工程的调试开关是否开启没开启时会直接尝试从本地路径读 bundle。确认 IP 配置是不是正确我踩过最蠢的一个坑是把 IP 填成了localhost模拟器上没问题真机上当然连不上。看设备侧的系统日志OpenHarmony 应用崩溃或加载失败通常会在日志里给出原因但需要打开对应的日志过滤关键词比如RnAbility、Metro等。白屏问题有一个特点一旦出现往往不是代码 bug而是配置或环境问题。如果代码逻辑有致命错误通常至少会看到部分渲染或者红屏错误而不是干净的白屏。所以白屏优先排查环境不要先怀疑自己的代码。6.2 键盘弹出导致布局错乱前面提到过KeyboardAvoidingView在 OpenHarmony 上的行为不稳定这里再补充一个细节我在调测时发现当键盘弹出时windowSoftInputMode如果没有设置成adjustResize整个窗口高度不会变化而 RN 框架却会重新计算Dimensions.get(window).height导致底部固定区不按预期移动。解决方案是双管齐下原生工程的页面配置里明确设置windowSoftInputMode adjustResize。JS 侧不依赖Dimensions的动态变化而是用固定的底部操作区布局方案配合ScrollView内容区 padding。如果这两个都做了还是有遮挡可以尝试用onLayout事件监听容器高度变化然后手动调整底部操作区的marginBottom。这个方案最粗暴但在 OpenHarmony 上反而最可靠。6.3 列表性能与包体积控制FlatList在渲染大量练习历史记录时首次滚动会出现明显卡顿。我分析后发现根因是每条记录中嵌套了错误题数组导致 item 渲染成本偏高。优化方案列表项中只展示摘要信息日期、正确率、耗时详情进入二级页面再展示错题数组避免在列表中序列化整个错误列表。getItemLayout显式声明每项高度跳过动态高度测量。removeClippedSubviews在部分版本上会导致滚动时白块我最后关闭了这个属性改用减少 item 复杂度来换流畅度。包体积方面这个项目没引入第三方大库所以整体体积可控。但我注意到 Release 构建时如果开启了多设备架构支持产物体积会明显增大。对于只跑一种设备的场景建议在构建配置里关掉多余架构产物能减少约三分之一体积。注意不要为了减小包体积把必要的原生模块拆掉。React Native for OpenHarmony 的模块注册是静态检查的你拆掉某个原生模块JS 侧调用时不会立即报错但运行到相关功能时会崩溃排查成本非常高。6.4 自定义原生模块无法调用的排查自定义原生模块是 OpenHarmony 上 RN 项目的常见扩展点但很容易出现JS 侧明明已经NativeModules.XXX了运行却拿不到模块的情况。我的排查经验是确认原生模块类名是否与 JS 侧引用名严格一致注意大小写和命名空间。确认模块在原生侧是否被正确注册到框架的模块列表里不是写了类就完事还要在注册表中登记。确认方法名是否导出RN 的NativeModules只能访问导出过的方法。在原生侧方法入口打日志看方法到底有没有被调用。如果原生日志都没有说明调用链路在更早的地方断了如果原生有日志但 JS 拿不到返回值重点检查回调参数格式。7. 实测经验与调优心得这个项目从搭框架到能稳定跑完一整套练习流程大概用了两周半的时间。整体体验下来我对 React Native for OpenHarmony 的评价是能干活但别指望完全无缝迁移。如果你是从标准 React Native 项目过来JS 侧的代码手感非常熟悉框架最核心的心智模型没有变但组件映射、键盘交互、原生模块这些接缝位置必须花时间逐个验证。几个可以帮你少走弯路的具体建议新建项目第一天先把官方模板的所有内置示例跑一遍确认每个核心组件在目标设备上是真实可用的再开始写业务。出题算法这种平台无关模块一定要先用 Node 单独写一遍单元测试再接入 RN。我有好几次在 OpenHarmony 上排查半天最后发现问题其实是出题算法的纯逻辑 bug。数据持久化建议尽早做封装后面接入原生文件读写、换成自带存储影响面都被控制在storage目录下。调试期优先连真机模拟器的键盘行为、系统字体、滚动流畅度和真机有明显差异特别是键盘相关的问题模拟器上很可能复现不了。最后分享一个我实际踩过、也最容易被忽略的小技巧每天练习生成的 JSON 文件名里带上日期像practice_2025-03-15.json这样。这样做的好处不仅仅是方便按日期索引更重要的是当数据写坏、需要手动清理时你能在文件系统里一眼看出哪条文件是坏的。我在测试阶段曾经因为一条脏数据把整个历史记录列表搞崩当时就是靠这种可读文件名快速定位并删除问题数据的。这种为排查留后门的小习惯在原生与 RN 混合项目里非常值得养成。数学练习只是第一步这个 App 后续可以继续加的功能还挺多多用户切换、每日目标提醒、错题强化重练、家长端报告页。架构上因为已经把引擎、存储、UI 分离每加一个功能基本都是往固定位置填代码不会牵一发动全身。如果你也在做 OpenHarmony 上的 RN 项目或者正打算用这个方向练手希望这篇记录能把那些文档不会写、但实际一定会碰的坑提前展示给你。