本文记录把 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 = 100vp300pxx=51..351 (w=300) ✓
Durian(6 字符)6 × 8 + 60 = 108vp324pxx=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✅ 都没有
mainindex.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 本次验证的边界

  1. ★ 向输入框注入文本未成功(uitest uiInput inputText 没有写进 RNOH 的 TextInput)⇒ onTextChange 回调与实时过滤未实测(见第六节);
  2. onBlur 后 value 变 undefined 的疑点未实测;
  3. resetValue 未验证;
  4. setSort 自定义匹配器未验证(只用了默认的);大小写不敏感只做了源码级确认;
  5. onRemoveItem(点 chip 的 X)未实测;
  6. listProps.style 被强制覆盖只做了源码级说明(没有对比两次渲染);
  7. textInputProps 的各种透传组合未穷举;
  8. Keyboard.dismiss() 的效果未做断言(只观察到列表收起);
  9. 多选题的 renderItems 分支(命中 selectedItems 时显示 X)未逐项核对;
  10. 长列表滚动/性能未测;
  11. 只在一台模拟器上验证(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 ⇒ 打包失败改成不含花括号的纯文本描述 ✓
2FlatList 的 typeof 猜成 objectRN 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 通过,并且有三处可精确预言的数值/几何全部命中。

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

  1. ★★★ defaultIndex 可以直接预言输入框初值。 实测 defaultIndex={2} ⇒ 输入框读到 "Cherry"(正好 items[2].name);不给 defaultIndex 的对照实例为空 ✓

  2. ★★★ focus 门控是一个天然 A/B。 renderFlatList() 里 if (this.state.focus) ⇒ 未聚焦 0 项 / 聚焦 5 项,两个方向都实测 ✓
    ⇒ 这类"开关型"行为最好验证:不用管动画、不用管时序,只要看 dump 里有没有东西。

  3. ★★★★ chips 的宽度有闭式公式,实测精确命中。
    width = name.length * 8 + 60(vp)⇒ Apple(5) 预报 300px、Durian(6) 预报 324px ⇒ 实测 w=300 与 w=324 ✓✓
    ⇒ 有公式的几何,先算再量,一比就知道对不对。

  4. ★★ 结构断言前必须先看"写法"。 这个库连 render 都是箭头类字段 ⇒ prototype.render === undefined。预扫阶段把它分成"2 个原型方法 + 7 个箭头字段"之后,断言一次就对 ✓

  5. ★ 交互验证要挑"不依赖输入法"的链路。 本轮的点选(onItemSelect)、聚焦门控、初值都拿到了实测证据;而向输入框注入文本两次都没成功 ⇒ 这一条如实标为未验证,而不是"推测它能过滤" ✓

  6. ★ 我又在 JSX 文本里写了 {…}(这已是第二次)⇒ 打包直接失败。⇒ JSX 文本位置的花括号必须包成字符串表达式。


本篇用到的库

项内容
三方库react-native-searchable-dropdown(上游 1.1.3 的鸿蒙适配版)
适配仓库https://atomgit.com/oh-react-native/react-native-searchable-dropdown
适配 TAG1.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 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
本次构建首次因 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

Logo

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

更多推荐