本文记录把 react-native-virtualized-view 适配到 HarmonyOS 的完整过程。

这个库极小(全部实现只有 448 B),做的事也只有一件:让你能在"可滚动的容器"里安全地放 FlatList,不再报那条嵌套警告。

结论:完全可用,而且是少见的"能用行为级 A/B 对照证明其价值"的库 ——
我把"原生 ScrollView 会报警告"和"换成这个库就不报警告"放在同一页面的两个相位里,用 hilog 时间戳把因果定死了。

★★★★★ 一、它到底做了什么:448 B 的全部实现

src/ScrollView/index.tsx(也就 16 行):

const ScrollViewComponent = ({children, ...props}) => (
  <FlatList
    {...props}
    data={[]}                                          // 空数据
    keyExtractor={(_e, i) => 'dom' + i.toString()}
    ListEmptyComponent={null}
    renderItem={null}
    ListHeaderComponent={() => <>{children}</>}        // ★ 所有 children 塞进 ListHeaderComponent
  />
);

核心思路一句话:用一个"空的 FlatList"去冒充 ScrollView,把所有 children 放进它的 ListHeaderComponent。

为什么这样就不报警告了? 因为 RN 的那条警告针对的是
“VirtualizedList 被嵌套在 plain ScrollView 里” ——
而这里外层已经不是 plain ScrollView,而是一个 VirtualizedList(FlatList 本身就是 VirtualizedList 的封装)⇒ 警告条件不成立 ✓

★★★★★ 二、行为级 A/B 对照:用 hilog 时间戳把因果定死

这是本轮最有价值的部分。我没有笼统地说"换了这个库就好了",而是做了一次隔离实验:

页面结构(同一页里两个容器):

实例外层容器内容
E1原生 ScrollView(react-native)两个内层 FlatList(每个 3 项)
E2本库 ScrollView(react-native-virtualized-view)两个内层 FlatList(每个 3 项)

相位设计(隔离因果):

  • P0(t≈3.5s 起):只挂 E1 ⇒ 观察警告是否出现(基线)
  • P1(t≈10.5s 起):再挂 E2 ⇒ 观察警告计数是否不再增加

实测(hilog 是最可靠的证据):

hilog 里 VirtualizedLists 警告总数 = 1
10-17 15:19:20.530  9594  9765 E A0beef/#RNOH_JS:
  __█__ VirtualizedLists should never be nested inside plain ScrollViews with the same orientation bec…

时间戳对时:

时刻相位说明
15:19:20P0(只挂 E1 原生版)★ 警告出现在这一刻
15:19:30P1(已追加 E2 本库版)hilog 总数仍是 1 ⇒ 没有新增 ✓

⇒ ⇒ 因果清楚:警告来自原生 ScrollView;换成本库的 ScrollView 后不再产生新警告 ✓✓
⇒ 这比"我换了库、好像不报了"强得多 —— 有时间戳、有基线、有增量对照

同时的文本级印证(FlatList 的项有没有真的渲染出来):

P0(只 E1):E1 标记 = 6    E2 标记 = 0
P1(加 E2):E1 标记 = 6    E2 标记 = 4

⇒ E1 的 6 个标记恒定(2 个内层 FlatList × 3 项 = 6,精确)✓✓
⇒ E2 的标记从 0 变为 4 ⇒ 内容确实渲染了(还有 2 个在屏外未被布局 dump 列出 ✓)

★★★ 三、一个方法论发现:屏上 LogBox 节点数 ≠ 警告次数

我同时还统计了"屏上 LogBox 里出现 VirtualizedLists 文本的节点数",结果和 hilog 对不上:

P0:屏上 LogBox 里的警告节点数 = 1
P1:屏上 LogBox 里的警告节点数 = 2        ← 看起来像"多了一条"
而 hilog 里的警告总数 = 1                 ← 实际只有一条

⇒ ⇒ 结论:屏上 LogBox 的"节点计数"不等于"警告次数"(一条通知可能对应多个文本节点,也可能是 LogBox 的分页指示在变)✓
⇒ ⇒ 判"某条警告出现了几次",要以 hilog / 日志为准,不要数 LogBox 的节点 ✓✓
(而且 LogBox 过一会儿会被 dismiss,屏上就什么都查不到了 —— 见最后一步"屏上无 LogBox")

★★ 四、契约与预检(全 PASS)

NOTE | 具名导出清单 = ScrollView
PASS | ★ 具名导出恰好 1 个(ScrollView)
PASS | ★★ 导出名是 ScrollView
PASS | ★★ ScrollView 是函数组件
NOTE | 默认导出(mod.default) = undefined
PASS | ★★ 没有默认导出(只能用 {} 具名导入)
PASS | ★ 是函数组件(无 prototype.render)
NOTE | react-native 的 FlatList 可用 = function
PASS | ★ react-native 的 FlatList 可用(本库依赖它)
SUMMARY | 合计 7/7 通过,失败 0

⇒ export { ScrollView } ⇒ 具名导出,import {ScrollView} from 'react-native-virtualized-view' 必须带花括号 ✓
(写成 import ScrollView from … 会拿到 undefined ⇒ 渲染时报 Element type is invalid —— 这是一类很常见的坑)

预检九项(全部干净):

预检项结果
dependencies✅ 空
peerDependencies✅ 只有 react: * / react-native: * ⇒ 无原生 peer
第三方 import✅ 只有 react + react-native
ListView(RN 已移除)✅ 空
isRequired / propTypes✅ 空
老写法九项✅✅ 全部为空
Platform.OS / measure() / onLayout / useNativeDriver / NativeModules✅ 全空

⚠️ 两个要注意的点:

  1. scripts.prepare = "bob build" ⇒ npm install 会跑它 ⇒ 安装时必须带 --ignore-scripts(否则可能因为缺少 react-native-builder-bob 而失败);
  2. star 只有 27 —— 是个小库(但功能确实成立,README 也说得很清楚:它就是为解决那条警告而生)。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

★★★ 五、它适合什么、不适合什么

场景是否适合
ScrollView 里要放 FlatList/SectionList✅ 正是为此而生
外层需要长列表虚拟化(成千上万行)❌ 不适合 —— 它把所有 children 塞进 ListHeaderComponent,等于一次性渲染全部内容
需要 ScrollView 的 refreshControl / stickyHeaderIndices⚠️ 要看 FlatList 是否支持同名 prop(refreshControl 支持;stickyHeaderIndices 不支持)
只是想要一个能滚的容器,内部没有虚拟列表⚠️ 用原生 ScrollView 更直接(不会有多余的虚拟化开销)

⇒ 一句话:它是一剂"针对嵌套警告的对症药",不是"更好的 ScrollView" ✓
⇒ 代价必须清楚:ListHeaderComponent 里的内容不会虚拟化 ⇒ 内容很多时性能等同于普通 ScrollView ✓


六、接入与预检(要点)

# ★ 必须带 --ignore-scripts:它的 scripts.prepare 是 "bob build"
npm install "git+https://atomgit.com/oh-react-native/react-native-virtualized-view.git#1.0.0-ohos-1.0.0" --ignore-scripts

七、交付包质量与基线说明

类别结果
交付包内容src/(3 个文件,共不到 700 B)+ lib/commonjs + lib/module + lib/typescript ⇒ 完整 ✓(27 文件)
spec.json⚠️ 缺 upstreamCommit
validation⚠️ "build-pass-device-pending"
⚠️ 声明的基线reactNative: "0.88.0" + rnoh: "0.84.4" ⇒ 两字段自相矛盾
备注实现零改动(本轮未修改库任何文件)

关于基线:本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
这个库无已移除写法、无平台分支、无原生依赖 ⇒ 结论跨基线成立 ✓


八、已知限制与本次验证边界

8.1 用这个库要知道的

事项说明
★★★★★ 它只是"空 FlatList 冒充 ScrollView"data={[]} + renderItem={null} + ListHeaderComponent={() => children}
★★★★★ 内容不会虚拟化所有 children 都在 ListHeaderComponent 里 ⇒ 一次性渲染;内容多时性能等同普通 ScrollView
★★★★ 具名导出,无默认导出import {ScrollView} from 'react-native-virtualized-view'(必须带花括号)
★★★ scripts.prepare = bob build安装必须 --ignore-scripts
★★ 不支持的 ScrollView props例如 stickyHeaderIndices(FlatList 没有对应能力)
★ star 只有 27小库;但它的 README 与实现完全一致,功能成立

8.2 本次验证的边界

  1. ★★★ 只验证了"嵌套警告不再产生"这一个行为,没验证其它 ScrollView 语义(滚动位置、onScroll 回调、contentOffset 等);
  2. ★★★ 未验证长内容下的性能(本次每个 FlatList 只有 3 项);
  3. ★★ 未验证 refreshControl / onEndReached 等透传 prop 的行为;
  4. ★★ 未验证 horizontal 方向(警告只在"同方向"嵌套时出现);
  5. 未验证 SectionList 嵌套场景;
  6. 未在 RN 0.88.0 / RNOH 0.84.4 上复验;
  7. 未测其它屏幕宽度;未做跨平台对照。

未改动库代码。


九、常见问题

Q1:它在 RN 0.84 上能跑吗?

能。 契约 7/7 通过;FlatList 正常渲染(E1 的 6 个标记、E2 的标记都出现了);
A/B 对照证明:原生 ScrollView + 内层 FlatList 会产生 VirtualizedLists should never be nested… 警告(hilog 时间戳 15:19:20,P0 阶段),换成本库的 ScrollView 后不再新增警告 ✓

Q2:为什么 import ScrollView from 'react-native-virtualized-view' 拿到 undefined?

因为它是具名导出(export {ScrollView}),没有默认导出(实测 mod.default === undefined)。
必须写 import {ScrollView} from 'react-native-virtualized-view'。
写错时的报错是 Element type is invalid … got: undefined,而这个错误信息不会告诉你"导入方式错了" ⇒ 记住这条。

Q3:装了之后 npm install 报错?

它的 scripts.prepare 是 bob build,安装时会执行。加 --ignore-scripts 即可。

Q4:它能替代 ScrollView 吗?

不能当作"更好的 ScrollView"用。 它把 children 全放进 ListHeaderComponent,内容不会虚拟化。
只在"ScrollView 里确实要放 FlatList"时才用它;否则直接用原生 ScrollView 更合适。

Q5:为什么 LogBox 上数出来 2 条,hilog 里只有 1 条?

屏上 LogBox 的节点数不等于警告次数(一条通知可能对应多个文本节点,分页指示也会变)。
判"警告出现几次"以 hilog/日志为准 ✓

Q6:怎么确认警告到底是谁产生的?

像我这样隔离:先用原生 ScrollView 复现(拿到基线与时间戳),再追加本库版本,看总数是否增加。
两步之间留足相位间隔,日志时间戳就能对上 ✓


小结

react-native-virtualized-view 是一个 448 B 的"对症药":用空 FlatList 冒充 ScrollView,
把 children 放进 ListHeaderComponent,从而消除"虚拟列表嵌套在 plain ScrollView"的警告。
零依赖、零原生 peer、老写法九项全空、具名导出、无默认导出。

最值得记下来的是这几条:

  1. ★★★★★ 用"隔离 + 追加"的方式做 A/B,而不是笼统地说"换了库就好了"。
    P0 只挂原生版 ⇒ hilog 在 15:19:20 出现 1 条警告;
    P1 追加本库版 ⇒ hilog 总数仍是 1 ⇒ 不再新增 ✓✓
    ⇒ 有时间戳、有基线、有增量 —— 这才叫证明。

  2. ★★★★★ 屏上 LogBox 节点数 ≠ 警告次数。
    屏上从 1 变 2,而 hilog 只有 1 条 ⇒ 数 LogBox 会被误导 ✓
    ⇒ 判"警告几次"一律看 hilog/日志。

  3. ★★★★ 极小的库,判据反而更硬。
    448 B 的源码一次读完 ⇒ 能精确知道它"就是用空 FlatList 冒充 ScrollView" ⇒
    于是判据可以直指核心(警告是否新增),而不是泛泛地"跑起来了没崩" ✓

  4. ★★★ 具名导出 / 无默认导出,是一类隐蔽的坑。
    写错导入方式只会得到 Element type is invalid … got: undefined,报错不会提示"你导入方式错了" ⇒
    预检阶段就该把"导出形态"断言清楚(本次实测 mod.default === undefined)✓

  5. ★★★ 一定要说清"它的代价":
    内容不虚拟化 ⇒ 它是"治嵌套警告的药",不是"更好的 ScrollView" ✓

  6. ★★ scripts.prepare 会阻塞安装 ⇒ --ignore-scripts 是这类库的标准姿势 ✓

  7. ★ star 只有 27,但功能成立 ——
    star 低不等于不能用;关键看"它声称解决的问题是否真的解决" ✓


本篇用到的库

项内容
三方库react-native-virtualized-view(上游 1.0.0 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-virtualized-view
适配 TAG1.0.0-ohos-1.0.0
需要 HAR / 权限 / ohpm✅✅ 都不需要
库类型容器(空 FlatList 冒充 ScrollView,解决嵌套警告)
运行时依赖✅ dependencies 为空;peer 只有 react / react-native
导出形态⚠️ 具名导出 ScrollView,无默认导出
scripts.prepare⚠️ bob build ⇒ 安装需 --ignore-scripts
许可证MIT
上游仓库https://github.com/hoaphantn7604/react-native-virtualized-view(★27,pushed 2024-01)
宿主工程RNOH084Demo(测试页 rnAppKey = VirtualizedViewTestApp)
# ★ 必须 --ignore-scripts(它的 prepare 是 bob build)
npm install "git+https://atomgit.com/oh-react-native/react-native-virtualized-view.git#1.0.0-ohos-1.0.0" --ignore-scripts
// ★ 具名导出(没有默认导出)
import {ScrollView} from 'react-native-virtualized-view';
import {FlatList} from 'react-native';

// 用法:外层用它,内层照常放 FlatList
<ScrollView>
  <FlatList data={a} renderItem={…} />
  <FlatList data={b} renderItem={…} />
</ScrollView>

// 它等价于(全部实现):
// <FlatList data={[]} renderItem={null} ListHeaderComponent={() => <>{children}</>} />

// ⚠️ 具名导出,写成 import ScrollView from … 会拿到 undefined ⇒ Element type is invalid
// ⚠️ children 不会虚拟化(都在 ListHeaderComponent 里)⇒ 内容多时性能等同普通 ScrollView
// ⚠️ 安装必须带 --ignore-scripts(prepare = bob build)
// ⚠️ stickyHeaderIndices 这类 ScrollView 专属 prop 不可用
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey VirtualizedViewTestApp

判据的复现方式

# ① 契约:具名导出 / 无默认导出
hdc shell "hilog -x | grep vvv-test" | Select-String 'PASS|FAIL|SUMMARY|NOTE'
# ② ★ 核心 A/B:数 hilog 里的警告(不要数 LogBox 节点)
hdc shell "hilog -x | grep -c VirtualizedLists"
#    ⇒ P0(只挂原生 ScrollView 版)时为 1;P1(追加本库版)后仍为 1
# ③ 文本级:内层 FlatList 的项确实渲染
devecocli ui layout | Select-String 'VVV 列表项'
#    ⇒ P0 时只有 E1 的标记;P1 时 E2 的标记也出现

验证环境

项版本
React Native0.84.1
React19.2.3
RNOH(npm / ohpm)@react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3
Node.js / npmv24.14.0 / 11.9.0
DevEco Studio26.0.0.621
HarmonyOS SDKAPI 26(26.0.0.32)
设备HarmonyOS 7.0.0(26.0.0) Beta2 模拟器 Pura X View(ohos-x64),1320×2232,density 3
本机状态栏高度117px = 39vp
宿主依赖✅ 无需任何额外依赖
宿主 HAP 产物entry-default-signed.hap
本次构建assembleHap 3 分 39 秒 ⇒ 一次成功(打包与构建退出码都检查了)
验证规模契约 7/7 全 PASS;hilog 时间戳级 A/B 对照(警告只在 P0 出现 1 次、P1 不新增);文本级印证(E1 标记恒 6、E2 标记 0→4);4 张截图互异

⚠️ 基线说明:交付包 spec.json 声明 RN 0.88.0 + RNOH 0.84.4(两字段自相矛盾),本次验证在 RN 0.84.1 + RNOH 0.84.3 上完成。
本库无任何已移除写法、无平台分支、无原生依赖 ⇒ 结论跨基线成立。


相关社区与项目地址

项目地址
React Native 鸿蒙官方社区(CPF-RN)https://atomgit.com/CPF-RN
RNOH 框架仓库https://atomgit.com/openharmony-RN/ohos_react_native
RNOH 三方库使用文档https://atomgit.com/OpenHarmony-RN/usage-docs
React Native for OpenHarmony 组织https://atomgit.com/oh-react-native
RN 三方库鸿蒙适配清单https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview

欢迎加入 CPF-RN 鸿蒙社区:https://atomgit.com/CPF-RN

React Native for OpenHarmony 组织:https://atomgit.com/oh-react-native

RN 三方库鸿蒙适配清单:https://atomgit.com/oh-react-native/rn-ohos-adaptation-overview

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐