ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

OpenHarmony上React Native深浅色主题切换实战

OpenHarmony上React Native深浅色主题切换实战 做跨端开发这些年我的一个体会是换一个运行时很多“理所当然”的写法都要重新验证一遍。最近我在OpenHarmony设备上跑React NativeRN拿一个经典到不能再经典的TodoList当练手项目顺手把深色浅色主题切换完整做了一遍。这个项目看起来就是列表加勾选加删除但真正落地时牵涉到RN的样式系统、Context状态管理、系统外观监听、本地持久化、组件重渲染性能还有老架构和新架构对高频更新的不同表现。如果你是第一次在鸿蒙上接触RN或者想在RN里把主题切换做得更规范这篇实战记录应该能让你少走不少弯路。1. 项目思路与方案选型1.1 为什么选RN for OpenHarmony做这个项目先说结论TodoList特别适合用来验证RN在OpenHarmony上的完整链路。一个TodoList用到View、Text、TextInput、TouchableOpacity、FlatList这些最基础的组件还涉及键盘处理、本地存储、应用生命周期几乎把日常业务开发会碰到的交互类型覆盖了一遍。如果这些基础能力在鸿蒙适配层上都能稳定表现那换一个复杂业务也只是组装方式的问题。还有一个关键点主题切换是典型的“JS层状态影响全局UI”的需求。它要求一套颜色变量贯穿所有组件要求系统深浅色变化能被实时感知要求用户手动选择能覆盖系统默认值还要求选择结果在重启后不丢失。这套链路在Web端有CSS Variables在原生端有资源限定符在RN里就得靠Context加状态管理自己搭。搭建过程能把RN的渲染模型、刷新机制、存储方案都摸一遍比单纯写列表有价值得多。1.2 TodoList功能范围拆解我把项目边界控制得很小只保留五个能力添加任务输入文字支持键盘提交列表展示用FlatList渲染新任务排在最前完成切换点击圆点或复选框切换完成态删除任务点击删除按钮简单直接数据持久化任务列表和主题偏好都存本地重启后恢复主题切换没有做成独立的设置页而是在页面顶部放一个主题切换区提供“跟随系统”“浅色”“深色”三个选项。这样用户既可以用系统控制的深浅色也可以手动强制覆盖效果直观代码上正好覆盖了我想要的三种主题状态。刻意去掉的功能是编辑任务、拖动排序、多级分类、消息提醒。原因是这些功能要么引入拖拽库要么引入本地通知会干扰对主题系统本身的验证。新手做练手项目时也建议先做减法核心链路稳定后再逐步加功能否则排查问题时要同时面对好几个变量。1.3 主题切换的三种主流方案对比在RN里做主题切换常见思路有三种方案实现方式优点缺点静态常量导出定义lightTheme和darkTheme组件里按条件取用最简单适合一两个页面全局状态变化时组件不会自动更新Context Theme Tokens用ThemeProvider注入颜色对象组件通过useTheme取值自动更新、类型安全、可维护性好需要一定的代码组织成本CSS-in-JS库如styled-components用样式库的ThemeProvider包裹应用写法优雅适合大型项目额外依赖在OpenHarmony适配层上存在兼容不确定性我最终选择了Context加Theme Tokens的组合。原因很实际TodoList虽小但要验证的正是“全局状态变化驱动UI更新”的机制。Context是RN自带能力不需要引入任何额外库而且和Hooks配合起来类型提示完整后续迁移到其他状态管理方案也很顺手。Web前端Element Plus这类组件库做主题切换通常走CSS VariablesRN里没有CSS但“设计令牌”的思路完全通用。把颜色统一收敛到tokens里组件不感知具体色值只知道取colors.background还是colors.text主题切换就是换一套tokens组件代码几乎不用动。2. 环境准备与工程搭建2.1 在鸿蒙工程里集成RN运行时RN for OpenHarmony不是把安卓或iOS的RN运行时直接搬过来而是通过社区适配层把React的渲染树映射到ArkUI的节点上。实际工程里通常是先用DevEco Studio创建一个标准的OpenHarmony应用工程然后通过ohpm安装RN运行时依赖在ArkTS侧放一个RN容器组件JS侧继续写React组件。我建议API版本选择当前稳定版的较高版本因为我实测发现低版本API在部分手势事件和键盘避让上存在差异。创建工程时选择Empty Ability模板就可以后面再调整module配置。集成完成后整个应用是“原生壳 RN业务页”的结构原生壳负责生命周期、权限、系统能力RN页负责业务UI。这个过程有几个地方容易踩坑。第一ohpm安装依赖时要注意适配层版本与RN版本配套不能拿适配层最新版去配老版本RN否则会出现运行时找不到Native Module的报错。第二工程里需要正确指定bundle资源路径RN的JS包可以放在rawfile目录下也可以从网络加载生产环境建议本地打包避免启动时白屏等网络。2.2 目录结构划分我把工程分成两个大区域ArkTS原生的entry模块以及RN侧的App目录。RN侧代码如下App/ src/ components/ TodoItem.tsx ThemeToggle.tsx screens/ TodoScreen.tsx theme/ tokens.ts ThemeProvider.tsx hooks/ useTheme.ts utils/ storage.ts index.jsentry/src/main目录下保留entry/src/main/ ets/ entryability/EntryAbility.ets pages/Index.ets resources/ rawfile/ bundle/ index.js assets/之所以把theme单独抽成文件夹是因为主题切换不是某个页面独享的功能。后续如果加入设置页、详情页甚至把主题tokens扩展到全局组件这套结构可以直接复用。组件层只依赖useTheme这个hook不直接操作Context这样底层换了状态管理方案组件层也不需要改。2.3 ArkTS容器加载JS Bundle的过程ArkTS侧的核心工作是把RN容器挂载到页面上。下面这段是简化示意代码不同版本的适配层导出的组件名称略有差异实际开发以你安装的SDK文档为准Component export struct RNContainer { State isReady: boolean false; build() { Column() { if (this.isReady) { // RN容器组件不同SDK版本名称可能不同 RNSurface({ bundlePath: bundle/index.js, entryPoint: App }) } else { LoadingProgress() } } .width(100%) .height(100%) } aboutToAppear() { RNHelper.initialize() .then(() { this.isReady true; }) .catch((err) { console.error(RN init failed: JSON.stringify(err)); }); } }这里有一个重要细节RN初始化不是瞬时完成的。如果直接在aboutToAppear里把RNSurface挂上有可能出现容器已经渲染、但JS层还没启动完成的间隙。我习惯用一个isReady状态做门闩等RN运行时初始化成功后再挂载真正的RNSurface初始化期间展示LoadingProgress体验会稳很多。3. TodoList核心功能实现3.1 数据模型与Reducer设计任务数据模型很直接type Todo { id: string; text: string; completed: boolean; createdAt: number; };状态管理我用的useReducer。TodoList规模小useState其实也够用但useReducer把增删改的逻辑集中在一起后续要接撤销重做、批量操作这些更复杂的行为会更有优势。Reducer定义如下type Action | { type: add; payload: { text: string } } | { type: toggle; payload: { id: string } } | { type: delete; payload: { id: string } } | { type: hydrate; payload: { todos: Todo[] } }; function reducer(state: Todo[], action: Action): Todo[] { switch (action.type) { case add: return [{ id: Date.now().toString(), text: action.payload.text, completed: false, createdAt: Date.now(), }, ...state]; case toggle: return state.map(item item.id action.payload.id ? { ...item, completed: !item.completed } : item ); case delete: return state.filter(item item.id ! action.payload.id); case hydrate: return action.payload.todos; default: return state; } }注意id生成用的是Date.now().toString()。生产环境任务数量多时建议换成时间戳加随机数或者用nanoid这类工具避免同一毫秒创建多个任务导致key冲突。练习项目里用时间戳完全够用但思维上要清楚它不是严格唯一的。3.2 列表渲染与任务操作页面结构分成三段输入区、主题切换区、任务列表区。输入区用TextInput加onSubmitEditing按回车直接添加任务const [text, setText] useState(); const addTodo () { const trimmed text.trim(); if (!trimmed) return; dispatch({ type: add, payload: { text: trimmed } }); setText(); }; TextInput value{text} onChangeText{setText} onSubmitEditing{addTodo} placeholder输入新任务 returnKeyTypedone /列表部分用FlatList渲染TodoItem组件接受item、onToggle、onDelete三个参数。FlatList data{todos} keyExtractor{(item) item.id} renderItem{({ item }) ( TodoItem item{item} onToggle{() dispatch({ type: toggle, payload: { id: item.id } })} onDelete{() dispatch({ type: delete, payload: { id: item.id } })} / )} /我用的是FlatList而不是直接map数组。道理很简单任务数量多的时候FlatList会根据可视区域做窗口化渲染不会一次把所有行都建出来直接map虽然看着简单一旦列表超过几十项滚动性能就会有可感知的下降。练习项目虽然只有几条任务但应该从一开始就按正确的方式写。3.3 本地持久化与启动恢复持久化选的是AsyncStorage。任务列表序列化成JSON字符串存进本地启动时再读出来回填。这里有一个非常容易踩的坑如果没有“已恢复”标志App启动时todos为空数组的初始值会触发一次写入把本地已有的数据覆盖掉。所以必须做双重处理const [todos, dispatch] useReducer(reducer, []); const [hydrated, setHydrated] useState(false); useEffect(() { AsyncStorage.getItem(STORAGE_TODOS_KEY) .then((saved) { if (saved) { dispatch({ type: hydrate, payload: { todos: JSON.parse(saved) } }); } setHydrated(true); }) .catch(() { setHydrated(true); }); }, []); useEffect(() { if (!hydrated) return; AsyncStorage.setItem(STORAGE_TODOS_KEY, JSON.stringify(todos)); }, [todos, hydrated]);第一个useEffect负责从本地读取第二个负责在todos变化时写入。关键在第二个useEffect里的if (!hydrated) return它保证只有恢复完成后才允许写入。没有这个保护App冷启动时会先把空数组写进本地用户上次保存的任务直接消失。4. 深浅色主题切换完整实现4.1 主题令牌Theme Tokens如何定义主题切换的第一步是把所有颜色收敛到一套tokens里。我定义了两套主题浅色和深色结构和键名完全一致export const themes { light: { colors: { background: #F5F7FA, surface: #FFFFFF, primary: #4A6CF7, text: #1F2937, textSecondary: #6B7280, border: #E5E7EB, danger: #EF4444, completed: #9CA3AF, overlay: rgba(0, 0, 0, 0.3), }, spacing: { xs: 4, s: 8, m: 12, l: 16, xl: 24 }, radius: { sm: 6, md: 12, lg: 20 }, }, dark: { colors: { background: #111827, surface: #1F2937, primary: #60A5FA, text: #F9FAFB, textSecondary: #9CA3AF, border: #374151, danger: #F87171, completed: #6B7280, overlay: rgba(0, 0, 0, 0.5), }, spacing: { xs: 4, s: 8, m: 12, l: 16, xl: 24 }, radius: { sm: 6, md: 12, lg: 20 }, }, } as const; export type AppThemeMode keyof typeof themes; export type AppColors typeof themes.light.colors;间距和圆角也放进tokens虽然深浅色切换时它们不变但统一管理能让组件代码更干净。组件里不写死间距数字而是取spacing.l后续要调整全局间距只改一处。这里有一条经验特别重要深色主题不要用纯黑。纯黑在视觉上会显得很死板而且和表面色、卡片色拉不开层次。我用的背景色是#111827表面色是#1F2937这样卡片区域能看出细微的浮起感比纯黑配灰的效果舒服很多。4.2 ThemeProvider与useTheme Hook实现主题注入用Context实现。这里面的核心是ThemeProvider组件它负责分发当前主题的颜色、当前模式、切换方法。useTheme则是一个便捷hook让组件不用直接useContext。type ThemePreference system | light | dark; type ThemeContextValue { mode: AppThemeMode; preference: ThemePreference; colors: AppColors; setPreference: (pref: ThemePreference) void; toggleTheme: () void; }; const ThemeContext createContextThemeContextValue | null(null);ThemeProvider里需要处理一个关键的逻辑层次preference保存用户选择mode是实际生效的主题真正决定mode的是系统深浅色加用户选择export function ThemeProvider({ children }: { children: React.ReactNode }) { const systemScheme useColorScheme(); const [preference, setPreference] useStateThemePreference(system); const [hydrated, setHydrated] useState(false); const activeMode: AppThemeMode preference system ? (systemScheme dark ? dark : light) : preference; useEffect(() { AsyncStorage.getItem(STORAGE_THEME_KEY).then((saved) { if (saved light || saved dark || saved system) { setPreference(saved); } setHydrated(true); }); }, []); useEffect(() { if (!hydrated) return; AsyncStorage.setItem(STORAGE_THEME_KEY, preference); }, [preference, hydrated]); const value useMemo(() ({ mode: activeMode, preference, colors: themes[activeMode].colors, setPreference, toggleTheme, }), [activeMode, preference]); return ( ThemeContext.Provider value{value} {children} /ThemeContext.Provider ); }这里用了useMemo缓存context value原因后面会在性能部分详细说。先记住一点context value每次都是新对象时所有消费了这个context的组件都会重渲染即使它们只关心颜色的某个字段。useTheme hook要加一个空值保护export function useTheme() { const ctx useContext(ThemeContext); if (!ctx) { throw new Error(useTheme must be used within ThemeProvider); } return ctx; }对新手来说这个保护能帮你更早发现问题。如果某个组件忘记包在ThemeProvider里就直接用useTheme它会立刻报错而不是等运行时渲染到一半才出现颜色奇怪的问题。4.3 跟随系统与手动覆盖的融合很多主题切换案例只做了“跟随系统”或“手动切换”中的一种但这两种需求在实际项目里经常同时存在。我的做法是让用户有三个选项跟随系统、浅色、深色。跟随系统依赖的是RN自带的useColorScheme。这个hook会订阅系统外观变化当用户从浅色切到深色时useColorScheme返回的值会更新ThemeProvider会重新计算activeMode整个页面自动跟着变。在OpenHarmony的适配层上这个hook的可用性与Appearance API的适配程度有关所以我在代码里留了systemScheme dark ? dark : light的回退逻辑即使系统返回null也不至于崩溃。手动覆盖的逻辑是preference为light或dark时直接忽略系统值用用户指定的主题。用户点击“跟随系统”时再恢复系统控制权。我还在切换器里放了一个比较贴心的交互当前处于“跟随系统”模式时如果系统恰好是浅色点击“浅色”选项不会产生任何视觉变化但点击“深色”选项就会立刻切到深色并固定住。这套逻辑看起来简单实际代码里要分清preference和effective mode两个概念不然很容易写出“点击一次没反应点击两次才切换”的bug。完整切换代码如下function ThemeToggle() { const { colors, preference, setPreference, mode } useTheme(); const options: { key: ThemePreference; label: string }[] [ { key: system, label: 跟随系统 }, { key: light, label: 浅色 }, { key: dark, label: 深色 }, ]; return ( View style{{ flexDirection: row, gap: 8 }} {options.map((option) { const selected preference option.key; return ( TouchableOpacity key{option.key} onPress{() setPreference(option.key)} style{{ paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, backgroundColor: selected ? colors.primary : colors.surface, }} Text style{{ color: selected ? #FFFFFF : colors.textSecondary }} {option.label} /Text /TouchableOpacity ); })} /View ); }在组件里展示当前生效模式也是一个好习惯。比如“跟随系统”模式下如果系统当前是深色可以在旁边加一个小字“当前深色”这样用户能明确知道系统状态是什么避免误以为按钮坏了。4.4 状态栏、动画与组件改造主题切换不只是把页面背景换成深色状态栏文字颜色也必须跟着变。RN里用StatusBar组件的barStyle属性控制StatusBar barStyle{mode dark ? light-content : dark-content} backgroundColor{colors.background} /在OpenHarmony上有一个补充建议如果RN层的StatusBar控制得不完整可以在ArkTS侧同步设置状态栏背景。做法是在EntryAbility的onWindowStageCreate阶段读取主题偏好再调用系统状态栏接口设置颜色。练手项目里可以先只依赖RN层真机上发现问题再补原生侧逻辑。切换动画方面RN的LayoutAnimation可以在布局变化时自动产生过渡效果。主题切换会改变所有组件背景色、文字色本身不太适合做位移动画但文字颜色和背景颜色的突变会让人感觉生硬。我尝试过用LayoutAnimation配合颜色切换实测在列表较小时过渡效果还不错import { LayoutAnimation, UIManager, Platform } from react-native; // 在入口处启用注意Platform判断以适配层为准 if (Platform.OS android) { UIManager.setLayoutAnimationEnabledExperimental?.(true); } // 切换主题前配置动画 const handleSetPreference (nextPref: ThemePreference) { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setPreference(nextPref); };不过要注意LayoutAnimation在OpenHarmony适配层上的支持情况需要实测。部分版本Worklet动画可用但LayoutAnimation只能触发布局变化纯色值变化理论上不归它管实际效果可能只在部分属性上生效。我的建议是优先保证切换后颜色正确动画属于锦上添花不要为了动画引入额外复杂度。TodoItem组件改造后长这样const TodoItem memo(function TodoItem({ item, onToggle, onDelete, }: { item: Todo; onToggle: () void; onDelete: () void; }) { const { colors } useTheme(); return ( View style{{ flexDirection: row, alignItems: center, padding: colors.spacing?.l ?? 16, backgroundColor: colors.surface, borderRadius: colors.radius?.md ?? 12, }} TouchableOpacity onPress{onToggle} Text style{{ color: item.completed ? colors.primary : colors.textSecondary, fontSize: 20, }} {item.completed ? ✓ : ○} /Text /TouchableOpacity Text style{{ flex: 1, marginLeft: 12, color: item.completed ? colors.completed : colors.text, textDecorationLine: item.completed ? line-through : none, }} {item.text} /Text TouchableOpacity onPress{onDelete} Text style{{ color: colors.danger }}删除/Text /TouchableOpacity /View ); });这里我刻意淡化了StyleSheet.create的使用因为涉及主题的动态颜色必须内联取colors。静态布局部分仍然可以抽到StyleSheet里动态颜色部分再用内联两者混用即可。关键原则不要在组件里写死任何颜色值。5. 常见问题排查与避坑实录5.1 首帧闪烁和读取竞态最典型的问题App冷启动后页面先亮成浅色再突然变成深色看起来就像闪了一下。原因就是主题偏好从AsyncStorage读取是异步的首帧渲染时还没读到数据只能先用默认浅色顶上去。解决办法就是前面写的hydrated门闩。在ThemeProvider里读取完成之前不渲染任何业务页面等preference恢复后再让页面见光。代码层面就是if (!hydrated) { return View style{{ flex: 1, backgroundColor: #111827 }} /; }注意这里返回的占位背景色要写死成应用主色调的默认值不能从主题tokens里取因为tokens还没确定。从用户体验来看这个占位页最多存在几十毫秒肉眼几乎感知不到但能彻底消灭“先浅后深”的闪烁感。任务列表的恢复也是同理。我见过不少开发者只给主题加了hydrated标志任务列表却没加结果启动时能看到列表从空到满的变化过程。处理方式完全一致任务恢复完成前hold住列表渲染。5.2 系统外观变化监听失效怎么办在RN标准实现里useColorScheme会在系统切换深浅色时自动触发更新。但在OpenHarmony适配层上这个能力依赖于适配层是否完整实现了Appearance API。我实际测试时模拟器上切换系统深色模式页面确实能跟着变但个别版本的适配层更新有延迟甚至需要页面重新聚焦才触发。如果遇到监听不灵敏的情况排查路径是这样先在ArkTS侧直接调用系统接口确认系统能收到外观变化事件然后看RN适配层日志确认事件有没有透传到JS侧最后再考虑用原生事件手动转发的方式绕过适配层缺口。手动转发的大致思路是ArkTS侧订阅系统颜色变化事件调用RN的DeviceEventEmitterJS侧注册监听。写法如下import { DeviceEventEmitter } from react-native; useEffect(() { const sub DeviceEventEmitter.addListener(onUIAppearanceChanged, () { // 触发一次状态更新强制重新计算activeMode setRefreshToken((v) v 1); }); return () sub.remove(); }, []);新增刷新标记后useColorScheme拿到的还是旧值的话这个方案也救不了只能提升适配层版本。所以项目一开始就要选一个较新的适配层版本旧版本这类API缺口比较多。5.3 主题切换后的性能问题新老架构对比主题切换会瞬间改变所有组件的颜色如果列表很长性能瓶颈会直接暴露。新旧架构在这个场景下有明显差异维度老架构Bridge新架构Fabric TurboModuleJS与原生通信异步序列化消息同步调用能力更强UI更新指令批量转发排队执行渲染器直接调度高频更新场景容易出现掉帧更平滑OpenHarmony适配成熟度相对稳定部分能力还在演进老架构的Bridge是异步的JS侧把UI更新指令序列化后丢给原生侧原生侧再执行。主题切换引发的大量UI变更会挤在同一个消息队列里列表长的时候能明显感觉到卡顿。新架构的Fabric渲染器可以直接调度UI更新TurboModule也支持同步调用理论上能显著改善这类“全量刷新”场景。在OpenHarmony上能否启用新架构取决于适配层版本和构建配置不是所有RN版本都默认开启。练手项目里如果暂时用不了新架构有两条优化手段第一给列表和行组件加memo。比如前面TodoItem用memo包裹并且在传给FlatList的renderItem时确保onToggle和onDelete引用稳定避免父组件重渲染导致每行都跟着重渲染。第二用useMemo稳定context value。ThemeProvider里的colors对象如果每次渲染都是新引用memo里的浅比较会用不了导致memo失效。我的做法是value通过useMemo缓存依赖项只有activeMode和preference这两个值不变时context value引用稳定。5.4 其他细碎但影响体验的坑还有几个小坑值得注意。深色模式下阴影会变得一团黑如果卡片用了boxShadow建议深色主题下改用浅色边框代替阴影视觉效果更干净。透明弹层在深色下要加深遮罩透明度否则弹窗底部透出来的内容太多层次感会变差。字体颜色也要注意对比度。我在调色板里把深色文本从纯白改成#F9FAFB把次要文本改成#9CA3AF就是为了保证在#1F2937背景上不会有刺眼的发光感。对比度过低也不行尤其是完成态文字颜色我试过浅灰在深色下几乎看不清最后选了#6B7280既能看出是禁用感又不至于完全消失。真机和模拟器在主题切换上的表现可能完全不同。我遇到过模拟器切得飞快真机上总慢半拍的情况这通常与广告版本或系统色域管理有关。练手阶段可以在模拟器上把逻辑调通但最后一定要拿真机验证一遍颜色准确性和切换流畅度。6. 后续扩展从主题切换到原生能力6.1 调用系统电话与图片裁剪的通用思路TodoList把主题切换做完后我已经验证了RN逻辑层在OpenHarmony上能跑通但这只是跨端开发的一半。实际业务里还有很多场景需要触达系统能力比如“调用电话功能”会让人误以为RN可以直接拨号实际上RN侧不能直接操作系统电话必须通过原生模块包一层。正确做法是在ArkTS侧封装一个拨号能力通过NativeModules暴露给JS侧RN代码里调用import { NativeModules } from react-native; NativeModules.PhoneHelper?.openDialer(10086);图片裁剪库也是如此。RN生态里现成的裁剪库不少但OpenHarmony的渲染和文件系统与Android、iOS差异很大直接搬第三方库经常遇到底层依赖缺失。比较稳定的策略是先用系统原生裁剪页面做一版跑通了再考虑替换成图片处理库。这类原生扩展会用到权限申请、Intent拉起、ActivityResult回调和主题切换完全是两个链路但都是RN for OpenHarmony混合开发中的常见需求。如果后续要深入RN和鸿蒙的混合开发建议找几个系统能力和业务强相关的小项目逐个验证比如相册选择、文件上传、定位服务。每验证一个能力就往自己的原生能力清单里记一笔沉淀下来的东西比单独学API文档有用得多。6.2 主题系统向更大工程演进的建议现在这套主题切换方案在TodoList上运行良好但往更大工程迁移时有三个地方可以提前进化。第一拆分Context。现在ThemeContext一个对象打包了mode、preference、colors、setPreference、toggleTheme。大型应用里如果很多组件只需要colors而不关心preference的变化它们会因为context value整体变化而频繁重渲染。更细的做法是拆成ThemeModeContext和ThemeColorsContext让依赖面小的组件只订阅自己关心的部分。第二引入更适合复杂更新的外部状态方案。Zustand、Jotai这类轻量状态库对主题切换的支持很完善可以做selector级别的订阅避免Context方案中“一改全改”的重渲染问题。TodoList因为小Context完全够用但大型项目里这类优化会显著影响列表性能。第三沉淀设计规范。主题tokens只包含colors、spacing、radius还远远不够。实际做组件库时还应该定义字体大小、行高、间距比例、动效时长这五类基础令牌。主题切换不只是换颜色深色下字重可以更轻卡片圆角可以更小这些细微差别才是专业App和练手项目的分水岭。我做这个项目的最大感受是主题切换在RN里不算大难题但“RN跑在OpenHarmony上”这个大前提会让所有看似简单的事情都多一次验证。建议新手先把主题tokens和Context结构敲定再补持久化最后再搞动画。先在模拟器上把深浅两套色调节稳定再上真机验收真机上的状态栏颜色、字体渲染和系统外观变化响应往往跟预览器和模拟器展现的是两回事。这套链路走一遍你会对RN的渲染模型、跨端能力边界、原生桥接方式都有一个更清晰的判断。
返回列表