React Native for OpenHarmony 实战:三方库 react-native-searchable-dropdown 的鸿蒙化适配指南
本文记录把 react-native-searchable-dropdown(可搜索下拉选择)适配到 HarmonyOS 的完整过程。
它做的事很实用:一个输入框 + 一个可搜索的列表。点输入框弹出全部选项,输入关键字实时过滤,点某一项就选中、列表收起;还支持多选 + chips 行(每个已选项是一个带 X 的小胶囊)。
结论:这属于"最省事"的一类库 —— 纯 JS、零依赖、没有一处已移除的 API。装完直接用。
★★ 一个"差点写错"的细节:render 不在原型上
第一次扫源码时就发现它的成员写法是混着的:
export default class SearchableDropDown extends Component {
constructor(props) { … } // ← 原型方法
renderFlatList = () => { … } // ← 箭头函数【类字段】
componentDidMount = () => { … } // ← 箭头函数【类字段】
searchedItems = searchedText => { … }
renderItems = (item, index) => { … }
renderListType = () => { … }
renderTextInput = () => { … }
render = () => { … } // ← ★★ 连 render 都是箭头字段!
renderSelectedItems() { … } // ← 原型方法
}
实测:
原型方法命中数(constructor / renderSelectedItems)= 2 ✓
7 个箭头类字段在 prototype 上的个数 = 0 ✓
★★ `prototype.render` = undefined ✓
⇒ 如果按"render 一定在原型上"去写断言,第一条就会挂 ✓(这类判断容易反复踩,所以写断言前先看源码写法已经成了固定动作)
★★★ 三处可以精确预言的几何/数值,实测全部命中
① defaultIndex ⇒ 输入框初值
componentDidMount = () => {
const listItems = this.props.items;
const defaultIndex = this.props.defaultIndex;
if (defaultIndex && listItems.length > defaultIndex) {
this.setState({ listItems, item: listItems[defaultIndex] }); // ← 取第 defaultIndex 项
} else {
this.setState({ listItems });
}
};
实测(items = [Apple, Banana, Cherry, Durian, Elderberry]、defaultIndex={2}):
TextInput#TextInput(94) [36,1572,1284,1694] "Cherry"
⇒ 初值正好是 items[2].name ✓;而不传 defaultIndex 的对照实例输入框是空的 ✓
② focus 决定列表是否渲染(天然 A/B)
renderFlatList = () => {
if (this.state.focus) { // ← 只有聚焦时才返回 FlatList
…return <FlatList … />;
}
};
实测两个方向:
| 状态 | dump 里的列表项 |
|---|---|
| 未聚焦 | 0 项(页面里根本没有列表文字) |
| 点一下输入框(聚焦) | 5 项全在:Apple / Banana / Cherry / Durian / Elderberry |
③ chips 的宽度有公式
// renderSelectedItems():multi + chip + selectedItems.length > 0 时才渲染
<View key={index} style={{
width: (item.name.length * 8) + 60, // ★ 宽度 = 名字长度 × 8 + 60(vp)
…
}}>
<Text>{item.name}</Text>
<TouchableOpacity onPress={…onRemoveItem…}><Text>X</Text></TouchableOpacity>
</View>
实测(selectedItems = [Apple, Durian],density 3):
| chip | 公式 | 预言宽度 | 实测 |
|---|---|---|---|
Apple(5 字符) | 5 × 8 + 60 = 100vp | 300px | x=51..351 (w=300) ✓ |
Durian(6 字符) | 6 × 8 + 60 = 108vp | 324px | x=381..705 (w=324) ✓ |
⇒ 两个 chip 的宽度都是公式的精确结果 ✓✓
★★ 交互也通了
点列表里的 Banana:
SELECT D1 name=Banana id=2
⇒ onItemSelect(item) 收到的条目完全正确 ✓;而且选完之后列表自动收起(源码里 setState({item, focus:false}) + Keyboard.dismiss())✓
设备侧断言:A 9 / 9 + B 4 / 4 = 13 / 13 纯断言全通过;交互组 2 条为测试脚手架问题(详见第六节,其中"点选"已单独取得证据)。

一、先说结论
| 项 | 结果 |
|---|---|
| 上游最新版 | 1.1.3;上游 license 字段为空(交付包 spec.json 记为 ISC);上游 star 148 |
| 库类型 | 纯 JS 单文件组件库(index.js 9016 B,源码直发) |
| 是否需要原生适配 | 不需要(无 harmony/、无 NativeModules) |
| 运行时依赖 | ✅ 零 |
prepare / postinstall | ✅ 都没有 |
main | index.js |
| 需要 HAR / 权限 / ohpm | ❌ 都不需要 |
| ★ 是否可用 | ✅ 可以直接用(零老 API、零平台分支) |
| 设备侧断言 | ✅ 纯断言 13 / 13(A 9 + B 4);交互证据:点选回调 ✓、focus 门控 ✓ |
★★★ defaultIndex | 实测输入框初值 = items[2].name = "Cherry" ✓ |
★★★ focus 门控 | 未聚焦 0 项 / 聚焦 5 项 ✓ |
| ★★★★ chips 宽度 | name.length*8+60 ⇒ Apple 300px / Durian 324px,精确命中 ✓ |
| ★ 交互 | onItemSelect(name=Banana, id=2) ✓,选完列表自动收起 ✓ |
| ★ 结构要点 | render 是箭头类字段 ⇒ prototype.render 是 undefined |
| ⚠️ 未验证 | 向输入框注入文本(onTextChange / 实时过滤未实测) |
二、判定与接入
| 步 | 做法 | 结果 |
|---|---|---|
| ① | package.json 里有没有 harmony.autolinking | 没有 |
| ② | 仓库里有没有 harmony/ | 没有 |
| ③ | 代码里有没有 NativeModules | 全无 |
| ④ | import 图体检 | 只有 react + react-native ⇒ 零依赖 ✓ |
| ⑤ | scripts 扫 prepare 家族 | ✅ 没有 |
| ⑥ | ★ 老 API 预扫 | 只命中 import React, { Component } from 'react' ⇒ Component 仍存在 ⇒ 安全(不是 PropTypes) |
| ⑦ | ★ 平台分支预扫 | ✅ 空 |
| ⑧ | 原生痕迹预扫 | ✅ 空 |
| ⑨ | ★ 成员写法预扫 | 混用:2 个原型方法 + 7 个箭头类字段(含 render)⇒ 断言前先分组 ✓ |
npm install "git+https://atomgit.com/oh-react-native/react-native-searchable-dropdown.git#1.1.3-ohos-1.0.0" --ignore-scripts
装完即可用。
三、主要 props 与行为(读源码整理)
| prop | 作用 |
|---|---|
items | 数据源(形如 [{id, name}]) |
defaultIndex | 初值取 items[defaultIndex](要求 listItems.length > defaultIndex) |
multi / chip / selectedItems | 多选 + chips 行(宽度 name.length*8+60) |
onTextChange / onChangeText | 输入变化回调(onTextChange 优先;也支持放在 textInputProps 里);用 setTimeout(…, 0) 调用 |
onItemSelect | 选中回调(同样 setTimeout(…, 0)) |
onRemoveItem | 点 chip 上的 X 时回调 |
onFocus / onBlur | 聚焦/失焦;聚焦时列表恢复为 props.items 全量 |
setSort | 自定义过滤函数;默认 item.name.toLowerCase().indexOf(text.toLowerCase()) > -1(大小写不敏感的子串匹配) |
listProps | 透传给内部 FlatList;已有的键不被默认值覆盖,但 style 会被强制覆盖 |
resetValue | 选中后清空并重新聚焦 |
containerStyle / textInputStyle / itemsContainerStyle / itemStyle / itemTextStyle / placeholder / placeholderTextColor / underlineColorAndroid / textInputProps | 样式与透传 |
四、实测结果
4.1 断言 13 / 13(纯断言部分)
| 组 | 内容 | 结果 |
|---|---|---|
| A 契约与结构 | 默认导出是 class、只有 default 一键、2 个原型方法 + 7 个箭头类字段(含 render)、prototype.render === undefined、FlatList / Keyboard.dismiss 存在、4 条源码级事实 | 9/9 |
| B 渲染与几何 | 4 个实例渲染未抛错、标题安全区(vp ≥39)、三条判据与 chips 宽度预言 | 4/4 |
| C 交互与边界 | 输入回调 / 点选回调 / 4 条边界记录 | 1/3 ⚠️ 见第六节 |
4.2 关键观测值
默认导出的 typeof = function
原型方法命中数(constructor / renderSelectedItems)= 2
7 个箭头类字段在 prototype 上的个数 = 0
★★ prototype.render = undefined
D1(defaultIndex=2):TextInput [36,1572,1284,1694] "Cherry" ← items[2].name ✓
D1 未聚焦:dump 里 0 个列表项
D1 聚焦后:Apple / Banana / Cherry / Durian / Elderberry(5 项全在)✓
点 Banana:SELECT D1 name=Banana id=2 ← onItemSelect ✓
点选后:列表消失(focus=false + Keyboard.dismiss)✓
D3 chips:
Apple ⇒ Custom#View [51,117,351,131] w=300 (5×8+60=100vp=300px)✓
Durian ⇒ Custom#View [381,117,705,131] w=324 (6×8+60=108vp=324px)✓
UI 安全区:TITLE measureInWindow y=420(px ≥117 ✓;vp ≥39 ✓)



五、已知限制与本次验证边界
5.1 用这个库要知道的
| 事项 | 说明 |
|---|---|
★ render 是箭头类字段 | prototype.render 是 undefined;fadeIn 之类的方法也走同一风格 |
★ defaultIndex 有效的前提 | listItems.length > defaultIndex(否则不生效) |
| ★ 只有聚焦才渲染列表 | renderFlatList() 里 if (this.state.focus) |
★ listProps.style 会被强制覆盖 | 其它键则是"未提供才补" |
| ★ 聚焦时列表恢复全量 | onFocus 里 listItems: this.props.items |
★ 回调是 setTimeout(…, 0) 里调的 | 不要在回调里假设"同步发生在点击的同一帧" |
| ★ 默认匹配器是子串匹配 | 想改成前缀/模糊匹配就传 setSort |
| ★ chips 宽度按字符数算 | name.length*8+60 ⇒ 中文/emoji 名字会偏窄(按字符数而非显示宽度) |
⚠️ onBlur 里 item 被设成 selectedItems(数组) | 下一帧 value = item.name 变成 undefined ⇒ 输入框可能变成非受控(本轮未实测) |
| ⚠️ 上游 license 字段为空 | npm 与仓库都没有 license 字段(交付包 spec.json 记为 ISC) |
5.2 本次验证的边界
- ★ 向输入框注入文本未成功(
uitest uiInput inputText没有写进 RNOH 的TextInput)⇒onTextChange回调与实时过滤未实测(见第六节); onBlur后value变undefined的疑点未实测;resetValue未验证;setSort自定义匹配器未验证(只用了默认的);大小写不敏感只做了源码级确认;onRemoveItem(点 chip 的 X)未实测;listProps.style被强制覆盖只做了源码级说明(没有对比两次渲染);textInputProps的各种透传组合未穷举;Keyboard.dismiss()的效果未做断言(只观察到列表收起);- 多选题的
renderItems分支(命中selectedItems时显示 X)未逐项核对; - 长列表滚动/性能未测;
- 只在一台模拟器上验证(
Pura X View,density 3,1320×2232),没有真机;未做跨平台对照。
未改动库代码。
六、★ 本轮没做成的一件事(如实记录)
我原本要把"输入关键字 → 实时过滤"也验掉,用 uitest uiInput 往输入框里写字,但两次尝试都没写进去:
点输入框(列表正常弹出 ⇒ 说明 onFocus 生效了 ✓)
uitest uiInput inputText an ⇒ 输入框文本没变、列表没过滤、onTextChange 没触发 ✗
再试逐字符输入 a / n ⇒ 同样没进去 ✗
⇒ 现象是"聚焦成功、但文本注入无效"。我没有继续深挖(可能是注入通道与 RNOH 的 TextInput 未打通,也可能是需要软键盘先起来)。
⇒ 所以这一条我不写成"已验证":onTextChange 与实时过滤在本轮是未实测 ✓
⇒ 而同一条链路的另一端(点选)是实测成功的(SELECT D1 name=Banana id=2 ✓),focus 门控也两个方向都实测了 ✓
★ 顺带说明:"输入文本"这类交互在模拟器上不一定可靠 —— 判断"功能是否正常"时,最好挑一条不依赖输入法的链路(本轮的
defaultIndex、focus门控、点选、chips 几何都属于这类)✓
七、我自己的两个失误
| # | 失误 | 现象 | 纠正 |
|---|---|---|---|
| 1 | 又在 JSX 文本位置写了花括号 | 我写 <Text>D4 给 listProps={{nestedScrollEnabled:true, style:{...}}}(…)</Text> ⇒ style:{...} 是非法 JS(空 spread) ⇒ Babel 报 Unexpected token ⇒ 打包失败 | 改成不含花括号的纯文本描述 ✓ |
| 2 | FlatList 的 typeof 猜成 object | RN 0.84 里是 function/组件 ⇒ 挂 1 条 | 改成断言"存在" ✓ |
失误 1 已经是同一类问题的第二次:JSX 的文本位置不能直接出现
{…}—— 会被当成表达式容器({{…}})甚至 spread child,导致语法/编译错误。想显示这种字面量,必须包成字符串表达式({'{…}'})。
八、常见问题
Q1:它在 RN 0.84 上能跑吗?
能。 纯 JS、零依赖、没有一处已移除的 API(唯一从 react 取的具名导入是 Component,仍然存在)。本工程实测:打包与构建都成功、页面正常渲染、defaultIndex / focus 门控 / 点选 / chips 几何全部符合预期 ✓
Q2:为什么我拿到的实例上 prototype.render 是 undefined?
因为它的 render 是箭头函数类字段(render = () => {…}),挂在实例上而不是原型上。同一风格还有 renderFlatList / componentDidMount / searchedItems / renderItems / renderListType / renderTextInput;只有 constructor 与 renderSelectedItems 是原型方法。
Q3:列表什么时候出现?
只有 state.focus 为真时才会渲染内部 <FlatList> —— 所以要点一下输入框;聚焦时列表会恢复成 props.items 全量。
Q4:defaultIndex 怎么用?
<SearchableDropDown items={ITEMS} defaultIndex={2} … /> // 初值 = ITEMS[2].name
⚠️ 它有个前提:listItems.length > defaultIndex。
Q5:chip(多选小胶囊)的宽度能改吗?
源码里是写死的公式:width = item.name.length * 8 + 60(vp)。想改宽度要自己覆盖样式或改源码 —— 注意它是按字符个数算的,中文/emoji 名字会偏窄。
Q6:listProps 里的样式为什么不生效?
因为库里有这几行:
if (!Object.keys(flatListPorps).includes(kv.key)) { flatListPorps[kv.key] = kv.val; }
else { if (kv.key === 'style') { flatListPorps['style'] = kv.val; } } // ← style 被强制覆盖
⇒ style 总是用 itemsContainerStyle;其它键(如 nestedScrollEnabled)你传了就不覆盖。
Q7:输入变化的回调是同步的吗?
不是 —— 源码用 setTimeout(…, 0) 调 onTextChange / onItemSelect。别在回调里假设"和点击同一帧"。
Q8:这个库需要鸿蒙侧做什么特殊处理吗?
不需要。 纯 JS、零依赖、无原生代码、无平台分支 —— 直接装、直接用 ✓
小结
react-native-searchable-dropdown 是纯 JS 的可搜索下拉(单文件 9016 B、零依赖)。设备侧纯断言 13 / 13 通过,并且有三处可精确预言的数值/几何全部命中。
最值得记下来的是这几条:
-
★★★
defaultIndex可以直接预言输入框初值。 实测defaultIndex={2}⇒ 输入框读到"Cherry"(正好items[2].name);不给defaultIndex的对照实例为空 ✓ -
★★★
focus门控是一个天然 A/B。renderFlatList()里if (this.state.focus)⇒ 未聚焦 0 项 / 聚焦 5 项,两个方向都实测 ✓
⇒ 这类"开关型"行为最好验证:不用管动画、不用管时序,只要看 dump 里有没有东西。 -
★★★★ chips 的宽度有闭式公式,实测精确命中。
width = name.length * 8 + 60(vp)⇒Apple(5) 预报 300px、Durian(6) 预报 324px ⇒ 实测w=300与w=324✓✓
⇒ 有公式的几何,先算再量,一比就知道对不对。 -
★★ 结构断言前必须先看"写法"。 这个库连
render都是箭头类字段 ⇒prototype.render === undefined。预扫阶段把它分成"2 个原型方法 + 7 个箭头字段"之后,断言一次就对 ✓ -
★ 交互验证要挑"不依赖输入法"的链路。 本轮的点选(
onItemSelect)、聚焦门控、初值都拿到了实测证据;而向输入框注入文本两次都没成功 ⇒ 这一条如实标为未验证,而不是"推测它能过滤" ✓ -
★ 我又在 JSX 文本里写了
{…}(这已是第二次)⇒ 打包直接失败。⇒ JSX 文本位置的花括号必须包成字符串表达式。
本篇用到的库
| 项 | 内容 |
|---|---|
| 三方库 | react-native-searchable-dropdown(上游 1.1.3 的鸿蒙适配版) |
| 适配仓库 | https://atomgit.com/oh-react-native/react-native-searchable-dropdown |
| 适配 TAG | 1.1.3-ohos-1.0.0 |
| 需要 HAR / 权限 / ohpm | 都不需要(纯 JS、零依赖) |
| 库类型 | 纯 JS 组件库:可搜索下拉选择(单选 / 多选 + chips) |
| 运行时依赖 | ✅ 零 |
prepare / postinstall | ✅ 都没有 |
| 许可证 | 交付包 spec.json 记为 ISC(⚠️ 上游 npm 与仓库的 license 字段为空) |
| 上游仓库 | https://github.com/zubairpaizer/react-native-searchable-dropdown(★148) |
| 宿主工程 | RNOH084Demo(测试页 rnAppKey = SearchableDropdownTestApp) |
# 从适配仓库安装(零依赖)
npm install "git+https://atomgit.com/oh-react-native/react-native-searchable-dropdown.git#1.1.3-ohos-1.0.0" --ignore-scripts
# 本地目录接入
npm install ../react-native-searchable-dropdown --ignore-scripts
import SearchableDropDown from 'react-native-searchable-dropdown';
// ① 单选 + 初值
<SearchableDropDown
items={[{id: 1, name: 'Apple'}, {id: 2, name: 'Banana'}, {id: 3, name: 'Cherry'}]}
defaultIndex={2} // 初值 = items[2].name ⇒ 实测输入框显示 "Cherry"
placeholder="搜索"
onTextChange={(text) => console.log('输入', text)} // 用 setTimeout(…, 0) 回调
onItemSelect={(item) => console.log('选中', item)} // 实测收到 {id:2,name:'Banana'}
onFocus={() => {}} onBlur={() => {}}
containerStyle={{padding: 8}}
textInputStyle={{padding: 10, borderWidth: 1, borderColor: '#aaa', color: '#111'}}
itemsContainerStyle={{maxHeight: 160}}
itemStyle={{padding: 10, backgroundColor: '#fff'}}
itemTextStyle={{color: '#111'}}
/>
// ② 多选 + chips(chip 宽度 = name.length*8+60,库内写死)
<SearchableDropDown
items={ITEMS} multi chip
selectedItems={[{id: 1, name: 'Apple'}, {id: 4, name: 'Durian'}]}
onRemoveItem={(item) => console.log('移除', item)}
…
/>
// ③ 自定义过滤(默认是大小写不敏感的子串匹配)
<SearchableDropDown setSort={(item, text) => item.name.startsWith(text)} … />
// ④ 透传 listProps(注意:style 会被 itemsContainerStyle 强制覆盖)
<SearchableDropDown listProps={{nestedScrollEnabled: true}} … />
// ⚠️ render / renderFlatList / componentDidMount 等都是【箭头类字段】(不在 prototype 上)
// ⚠️ 只有 state.focus 为真时才渲染内部 FlatList(聚焦时列表恢复为 items 全量)
// ⚠️ onBlur 里 item 会被设成 selectedItems(数组)⇒ value 变 undefined(本轮未实测)
# 换页启动测试页 —— force-stop 不能省
hdc shell aa force-stop com.rnoh084.demo
hdc shell aa start -b com.rnoh084.demo -a EntryAbility --ps rnAppKey SearchableDropdownTestApp
判据的复现方式
# ① 初值:直接读输入框文本(dump 里的 TextInput 节点带 value)
devecocli ui layout | Select-String 'TextInput#TextInput' # ⇒ "Cherry"
# ② focus 门控:对比"未聚焦/聚焦"两次 dump 里的列表项个数
devecocli ui layout | Select-String 'Apple|Banana|Cherry' # 未聚焦 ⇒ 0;聚焦 ⇒ 全部
# ③ chips 宽度:width = name.length*8 + 60(vp)⇒ ×density 得像素,再与 dump 里的 Custom#View 宽度比
devecocli ui layout | Select-String 'Custom#View' # ⇒ Apple w=300 / Durian w=324
# ④ 点选:点列表项,读回调日志
hdc shell "uitest uiInput click <x> <y>"
hdc shell "hilog -x | grep sdropdown-test" # ⇒ SELECT D1 name=Banana id=2
# ⚠️ 文本注入(uitest uiInput inputText)本轮未成功 ⇒ 想验"实时过滤"需另找注入方式
验证环境
| 项 | 版本 |
|---|---|
| React Native | 0.84.1 |
| React | 19.2.3 |
| RNOH(npm / ohpm) | @react-native-oh/react-native-harmony / @rnoh/react-native-openharmony 0.84.3 |
| Node.js / npm | v24.14.0 / 11.9.0 |
| DevEco Studio | 26.0.0.621 |
| HarmonyOS SDK | API 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 |
| 本次构建 | 首次因 JSX 花括号语法错误打包失败(未装机)⇒ assembleHap 3 分 55 秒(13/16)⇒ 4 分 14 秒 ⇒ A 9/9 + B 4/4 |
| 验证规模 | 设备侧 纯断言 13 / 13;初值 1 项("Cherry");focus 门控 2 方向(0 项 / 5 项);chips 宽度 2 项(300px / 324px 精确命中);点选回调 1 次(name=Banana id=2);4 张截图互异 |
相关社区与项目地址
| 项目 | 地址 |
|---|---|
| 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
更多推荐




所有评论(0)