在鸿蒙上用 React Native 开发应用时,列表往往是最核心也最容易出问题的部件。本文基于 React Native 0.84 与 RNOH 0.84.1,测试设备为 HarmonyOS 5.0,整理 FlatList 从基础列表到无限滚动的适配经验。先给结论:ScrollView + map 在数据少时简单直接,但在鸿蒙上数据超过 50 条后滑动开始掉帧;FlatList 采用虚拟化渲染,流畅度明显提升。
一、基础用法与 keyExtractor
FlatList 最基本的用法是 data 和 renderItem 两个属性。- const DATA = [
- { id: '1', name: '无线蓝牙耳机', price: '299.00' },
- { id: '2', name: '机械键盘', price: '459.00' },
- { id: '3', name: '手机支架', price: '39.90' },
- ];
- <FlatList
- data={DATA}
- renderItem={({ item }) => (
- <View style={styles.row}>
- <Text>{item.name}</Text>
- <Text>¥{item.price}</Text>
- </View>
- )}
- keyExtractor={item => item.id}
- />
复制代码 这段代码在鸿蒙上运行正常,列表渲染和滑动都没有问题。但 keyExtractor 必须提供,且每个 key 要唯一。如果不提供,FlatList 会用数组下标作为 key,数据变化时可能出现渲染错乱。下拉刷新时,新数据替换旧数据,但因为 key 是数组下标,FlatList 认为第 0 项还是第 0 项,没有触发重新渲染,页面显示的仍是旧数据。- // 错误:默认用数组下标,下拉刷新替换数据后可能不刷新
- <FlatList data={items} renderItem={renderItem} />
- // 正确:提供唯一 key
- <FlatList
- data={items}
- renderItem={renderItem}
- keyExtractor={item => item.id}
- />
复制代码 二、分割线、空状态与头尾组件
列表项分割线用 ItemSeparatorComponent,鸿蒙上的渲染与 iOS、Android 没有区别。建议用 StyleSheet.hairlineWidth 设置高度,保证不同设备上线条最细。空状态用 ListEmptyComponent,data 为空数组时渲染,不处理会让用户以为 App 崩了。ListHeaderComponent 和 ListFooterComponent 分别渲染头部和尾部,常用于标题、筛选条件、搜索框、加载指示器或没有更多了。需要注意的是,它们也会参与 FlatList 的虚拟化渲染,列表很长时头部组件可能被卸载;如果头部有输入框等状态,最好把状态提升到 FlatList 外部管理。
三、下拉刷新与上拉加载
下拉刷新通过 refreshControl 配合 RefreshControl 实现。- const [refreshing, setRefreshing] = useState(false);
- const handleRefresh = () => {
- setRefreshing(true);
- setTimeout(() => {
- setPagedData(generateProducts(20));
- setRefreshing(false);
- }, 1500);
- };
- <FlatList
- data={pagedData}
- renderItem={renderItem}
- keyExtractor={item => item.id}
- refreshControl={
- <RefreshControl
- refreshing={refreshing}
- onRefresh={handleRefresh}
- colors={['#0A59F7']}
- tintColor={'#0A59F7'}
- />
- }
- />
复制代码 鸿蒙上的下拉刷新动画接近 Android,都是 Material Design 风格的圆形加载指示器,colors 属性在鸿蒙上生效;回弹效果比 iOS 干脆,不影响功能但手感有差异。
上拉加载更多通过 onEndReached 实现,列表滚动到底部附近时触发。鸿蒙上有一个大坑:onEndReached 有时会连续触发多次。原因是 onEndReachedThreshold 设得太大,加上列表数据还没渲染完,FlatList 认为又到底部了。两个解决思路:用 loadingMore 状态锁住回调,避免重复请求;把 onEndReachedThreshold 调小,例如从默认值改成 0.1,离底部更近才触发。- const [loadingMore, setLoadingMore] = useState(false);
- const [page, setPage] = useState(1);
- const handleLoadMore = () => {
- if (loadingMore) return;
- setLoadingMore(true);
- setTimeout(() => {
- const newItems = fetchNextPage(page);
- setPagedData(prev => [...prev, ...newItems]);
- setPage(prev => prev + 1);
- setLoadingMore(false);
- }, 1000);
- };
- <FlatList
- data={pagedData}
- renderItem={renderItem}
- keyExtractor={item => item.id}
- onEndReached={handleLoadMore}
- onEndReachedThreshold={0.1}
- ListFooterComponent={
- loadingMore ? <ActivityIndicator size='small' color='#0A59F7' /> : null
- }
- />
复制代码 实际项目里可以两个方案同时用,宁可晚一点触发,也不要重复请求把服务器打爆。
四、横向滚动、多列布局与曝光监听
横向列表设置 horizontal,常用于商品推荐、标签选择。鸿蒙上横向滚动跟手,没有明显延迟。但如果横向列表嵌套在纵向 ScrollView 里,偶尔会出现手势冲突:用户想横向滑动,结果触发纵向滚动。这个问题 iOS、Android 也存在,不是鸿蒙特有;可通过 nestedScrollEnabled 或调整布局避免嵌套。多列布局用 numColumns,columnWrapperStyle 在鸿蒙上生效,可设置每行间距和对齐。注意多列布局要求每个列表项等高,FlatList 不支持瀑布流,需要不同高度网格得用第三方库或自己实现。曝光埋点用 onViewableItemsChanged,配合 viewabilityConfig 设置可见比例和最小可见时间。viewabilityConfig 只在组件初始化时读取一次,不能运行时修改,需要动态条件时用 useRef 保持引用。
五、性能优化与调试排障
如果列表项高度固定,用 getItemLayout 告诉 FlatList,避免动态测量。测试 500 条数据时,加上 getItemLayout 后首次渲染速度约快 20%,滑动也更跟手。- const ITEM_HEIGHT = 72;
- <FlatList
- data={products}
- renderItem={renderItem}
- keyExtractor={item => item.id}
- getItemLayout={(data, index) => ({
- length: ITEM_HEIGHT,
- offset: ITEM_HEIGHT * index,
- index,
- })}
- />
- <FlatList
- data={items}
- renderItem={renderItem}
- removeClippedSubviews={false}
- />
- 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,不同版本可能有差异,以实际测试为准。 |