查看: 166|回复: 0

鸿蒙RNOH FlatList无限滚动重复触发与渲染优化

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在鸿蒙上用 React Native 开发应用时,列表往往是最核心也最容易出问题的部件。本文基于 React Native 0.84 与 RNOH 0.84.1,测试设备为 HarmonyOS 5.0,整理 FlatList 从基础列表到无限滚动的适配经验。先给结论:ScrollView + map 在数据少时简单直接,但在鸿蒙上数据超过 50 条后滑动开始掉帧;FlatList 采用虚拟化渲染,流畅度明显提升。
一、基础用法与 keyExtractor
FlatList 最基本的用法是 data 和 renderItem 两个属性。
  1. const DATA = [
  2.   { id: '1', name: '无线蓝牙耳机', price: '299.00' },
  3.   { id: '2', name: '机械键盘', price: '459.00' },
  4.   { id: '3', name: '手机支架', price: '39.90' },
  5. ];
  6. <FlatList
  7.   data={DATA}
  8.   renderItem={({ item }) => (
  9.     <View style={styles.row}>
  10.       <Text>{item.name}</Text>
  11.       <Text>¥{item.price}</Text>
  12.     </View>
  13.   )}
  14.   keyExtractor={item => item.id}
  15. />
复制代码
这段代码在鸿蒙上运行正常,列表渲染和滑动都没有问题。但 keyExtractor 必须提供,且每个 key 要唯一。如果不提供,FlatList 会用数组下标作为 key,数据变化时可能出现渲染错乱。下拉刷新时,新数据替换旧数据,但因为 key 是数组下标,FlatList 认为第 0 项还是第 0 项,没有触发重新渲染,页面显示的仍是旧数据。
  1. // 错误:默认用数组下标,下拉刷新替换数据后可能不刷新
  2. <FlatList data={items} renderItem={renderItem} />
  3. // 正确:提供唯一 key
  4. <FlatList
  5.   data={items}
  6.   renderItem={renderItem}
  7.   keyExtractor={item => item.id}
  8. />
复制代码
二、分割线、空状态与头尾组件
列表项分割线用 ItemSeparatorComponent,鸿蒙上的渲染与 iOS、Android 没有区别。建议用 StyleSheet.hairlineWidth 设置高度,保证不同设备上线条最细。空状态用 ListEmptyComponent,data 为空数组时渲染,不处理会让用户以为 App 崩了。ListHeaderComponent 和 ListFooterComponent 分别渲染头部和尾部,常用于标题、筛选条件、搜索框、加载指示器或没有更多了。需要注意的是,它们也会参与 FlatList 的虚拟化渲染,列表很长时头部组件可能被卸载;如果头部有输入框等状态,最好把状态提升到 FlatList 外部管理。
三、下拉刷新与上拉加载
下拉刷新通过 refreshControl 配合 RefreshControl 实现。
  1. const [refreshing, setRefreshing] = useState(false);
  2. const handleRefresh = () => {
  3.   setRefreshing(true);
  4.   setTimeout(() => {
  5.     setPagedData(generateProducts(20));
  6.     setRefreshing(false);
  7.   }, 1500);
  8. };
  9. <FlatList
  10.   data={pagedData}
  11.   renderItem={renderItem}
  12.   keyExtractor={item => item.id}
  13.   refreshControl={
  14.     <RefreshControl
  15.       refreshing={refreshing}
  16.       onRefresh={handleRefresh}
  17.       colors={['#0A59F7']}
  18.       tintColor={'#0A59F7'}
  19.     />
  20.   }
  21. />
复制代码
鸿蒙上的下拉刷新动画接近 Android,都是 Material Design 风格的圆形加载指示器,colors 属性在鸿蒙上生效;回弹效果比 iOS 干脆,不影响功能但手感有差异。
上拉加载更多通过 onEndReached 实现,列表滚动到底部附近时触发。鸿蒙上有一个大坑:onEndReached 有时会连续触发多次。原因是 onEndReachedThreshold 设得太大,加上列表数据还没渲染完,FlatList 认为又到底部了。两个解决思路:用 loadingMore 状态锁住回调,避免重复请求;把 onEndReachedThreshold 调小,例如从默认值改成 0.1,离底部更近才触发。
  1. const [loadingMore, setLoadingMore] = useState(false);
  2. const [page, setPage] = useState(1);
  3. const handleLoadMore = () => {
  4.   if (loadingMore) return;
  5.   setLoadingMore(true);
  6.   setTimeout(() => {
  7.     const newItems = fetchNextPage(page);
  8.     setPagedData(prev => [...prev, ...newItems]);
  9.     setPage(prev => prev + 1);
  10.     setLoadingMore(false);
  11.   }, 1000);
  12. };
  13. <FlatList
  14.   data={pagedData}
  15.   renderItem={renderItem}
  16.   keyExtractor={item => item.id}
  17.   onEndReached={handleLoadMore}
  18.   onEndReachedThreshold={0.1}
  19.   ListFooterComponent={
  20.     loadingMore ? <ActivityIndicator size='small' color='#0A59F7' /> : null
  21.   }
  22. />
复制代码
实际项目里可以两个方案同时用,宁可晚一点触发,也不要重复请求把服务器打爆。
四、横向滚动、多列布局与曝光监听
横向列表设置 horizontal,常用于商品推荐、标签选择。鸿蒙上横向滚动跟手,没有明显延迟。但如果横向列表嵌套在纵向 ScrollView 里,偶尔会出现手势冲突:用户想横向滑动,结果触发纵向滚动。这个问题 iOS、Android 也存在,不是鸿蒙特有;可通过 nestedScrollEnabled 或调整布局避免嵌套。多列布局用 numColumns,columnWrapperStyle 在鸿蒙上生效,可设置每行间距和对齐。注意多列布局要求每个列表项等高,FlatList 不支持瀑布流,需要不同高度网格得用第三方库或自己实现。曝光埋点用 onViewableItemsChanged,配合 viewabilityConfig 设置可见比例和最小可见时间。viewabilityConfig 只在组件初始化时读取一次,不能运行时修改,需要动态条件时用 useRef 保持引用。
五、性能优化与调试排障
如果列表项高度固定,用 getItemLayout 告诉 FlatList,避免动态测量。测试 500 条数据时,加上 getItemLayout 后首次渲染速度约快 20%,滑动也更跟手。
  1. const ITEM_HEIGHT = 72;
  2. <FlatList
  3.   data={products}
  4.   renderItem={renderItem}
  5.   keyExtractor={item => item.id}
  6.   getItemLayout={(data, index) => ({
  7.     length: ITEM_HEIGHT,
  8.     offset: ITEM_HEIGHT * index,
  9.     index,
  10.   })}
  11. />
  12. <FlatList
  13.   data={items}
  14.   renderItem={renderItem}
  15.   removeClippedSubviews={false}
  16. />
  17. keyExtractor={(item, index) => `page-${page}-${item.id}-${index}`}
复制代码
FlatList 继承自 PureComponent,只在 props 变化时重新渲染。如果 renderItem 依赖 data 以外的状态,比如选中状态,必须通过 extraData 传入,否则状态变化不会触发列表刷新。renderItem 里不要创建新的对象或函数,样式用 StyleSheet 预定义,renderItem 用 useCallback 包裹,避免不必要的重渲染。
鸿蒙上还有一个隐蔽的坑:removeClippedSubviews 在 Android 上默认开启用于优化内存,但在鸿蒙上有时会导致快速滑动时列表项空白,停下来一会儿又恢复。数据量大、滑动快时偶现,解决方案是显式设置 removeClippedSubviews={false}。另外,分页加载时后端返回的 id 可能重复,导致 key 冲突、列表项闪烁。keyExtractor 不能直接用 item.id,要加入页码或索引确保唯一。
六、架构取舍与鸿蒙平台差异
数据量小于 20 条时,用 ScrollView + map 简单直接;大于 20 条建议 FlatList。这个数字不是绝对,如果列表项包含图片、动画等重组件,可能 10 条就得换 FlatList。需要下拉刷新或上拉加载时,也建议直接用 FlatList,因为它开箱即用,ScrollView 要自己写不少逻辑。
与鸿蒙原生 ArkTS 的 List 组件相比,原生 List 支持 divider、listDirection、space 等属性,写起来也方便。RN FlatList 的优势在跨平台:一套代码跑三个平台,不用维护三套列表逻辑,API 设计成熟,社区最佳实践丰富。
鸿蒙上还有几个平台差异:滚动惯性接近 iOS,手指离开屏幕后列表继续滚动一段距离,Android 惯性相对弱,这会影响 onEndReachedThreshold 的设置,Android 上 0.5 能正常触发,鸿蒙上可能需要 0.3;滚动条默认隐藏,需要设置 showsVerticalScrollIndicator={true} 才显示;弹性效果与 iOS 不同,幅度和手感有差异。完整示例代码在 src/pages/FlatListDemoPage.tsx,包含基础列表加分割线、下拉刷新加上拉加载、横向滚动、多列网格、空列表状态。最后提醒,本文基于 React Native 0.84 + RNOH 0.84.1、HarmonyOS 5.0,不同版本可能有差异,以实际测试为准。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-9-16 15:04 , Processed in 0.024405 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部