React Native 里做列表下拉刷新,RefreshControl 是常用组件。它本身 API 不复杂,但在鸿蒙上使用时,真正需要关注的是平台差异:鸿蒙继承 Android 这一套属性,和 iOS 的 tintColor、title 等并不通用;同时 colors、下拉阈值、嵌套滚动、refreshing 状态重置等细节,容易导致指示器不出现、颜色不生效或 loading 一直转。下面按实际适配与排障过程整理。
一、基本用法:refreshing 是受控状态- import { useState, useCallback } from 'react';
- import { ScrollView, RefreshControl, Text } from 'react-native';
- function MyList() {
- const [refreshing, setRefreshing] = useState(false);
- const onRefresh = useCallback(async () => {
- setRefreshing(true);
- await fetchNewData();
- setRefreshing(false);
- }, []);
- return (
- <ScrollView
- refreshControl={
- <RefreshControl
- refreshing={refreshing}
- onRefresh={onRefresh}
- />
- }
- >
- <Text>列表内容...</Text>
- </ScrollView>
- );
- }
复制代码 关键只有两个属性:refreshing 控制是否显示 loading,onRefresh 是下拉触发回调。要注意 refreshing 是受控属性,必须在 onRefresh 中手动设为 true,请求完成后再设为 false。如果忘了设为 false,loading 会一直转。这个问题在鸿蒙真机上同样常见,调试时先检查状态是否复位。
二、FlatList 的两种接入方式
FlatList 也可以用 RefreshControl,而且有两种写法:- // 方式一:通过 refreshControl 属性
- <FlatList
- data={items}
- renderItem={renderItem}
- refreshControl={
- <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
- }
- />
- // 方式二:通过专门的 refreshing 和 onRefresh 属性
- <FlatList
- data={items}
- renderItem={renderItem}
- refreshing={refreshing}
- onRefresh={onRefresh}
- />
复制代码 两种方式效果一样,选哪种看个人习惯。方式一可以顺便设置颜色等 RefreshControl 属性,在鸿蒙适配时更灵活。
三、颜色属性的平台差异
默认指示器是灰色,某些 App 里不够醒目。RefreshControl 支持自定义颜色,但平台差异明显:- <RefreshControl
- refreshing={refreshing}
- onRefresh={onRefresh}
- colors={['#8B5CF6', '#EC4899']}
- tintColor='#8B5CF6'
- title='正在刷新...'
- titleColor='#8B5CF6'
- />
复制代码 colors 只在 Android 和鸿蒙上生效,而且必须传数组,可以多个颜色轮播。传字符串不会生效。tintColor、title、titleColor 是 iOS 专用。多平台兼容时,两边都要写。鸿蒙侧主要关注 colors 和 progressViewOffset。
四、鸿蒙上的常见踩坑
1. colors 不生效
写了 colors={['#8B5CF6']},指示器仍是灰色,可能是鸿蒙版本较低,该属性在某些版本上不支持。升级系统后恢复正常。因此颜色设置无效时,先检查鸿蒙版本。
2. 下拉距离不够
用户反馈“下拉了但没触发刷新”,通常是因为下拉距离未超过 RefreshControl 阈值。这个阈值由系统控制,无法通过属性调整,只能通过视觉提示告诉用户“再拉一点”。
3. ScrollView 嵌套冲突
如果 ScrollView 内又嵌套 ScrollView,RefreshControl 可能失效。外层 ScrollView 先处理了下拉手势,内层收不到。解决方式是避免嵌套,或只在最外层使用 RefreshControl。
4. refreshing 状态卡住
网络请求失败时,如果忘了在 catch 里把 refreshing 设为 false,loading 会一直转。建议用 try-finally:- const onRefresh = useCallback(async () => {
- setRefreshing(true);
- try {
- await fetchData();
- } catch (error) {
- console.error('刷新失败:', error);
- } finally {
- setRefreshing(false);
- }
- }, []);
复制代码 5. 固定头部遮挡指示器
页面有固定头部或导航栏时,下拉指示器可能被挡住。可以用 progressViewOffset 让指示器向下偏移:- <RefreshControl
- refreshing={refreshing}
- onRefresh={onRefresh}
- progressViewOffset={100}
- />
复制代码 这个属性只在 Android 和鸿蒙上生效,iOS 只能接受系统默认位置。
五、封装通用 RefreshableList
真实项目中,通常会把刷新状态、颜色、空列表、头尾组件封装起来:- import React, { useState, useCallback, ReactNode } from 'react';
- import { FlatList, RefreshControl, View } from 'react-native';
- type Props<T> = {
- data: T[];
- renderItem: (item: T, index: number) => ReactNode;
- onRefresh?: () => Promise<void>;
- keyExtractor?: (item: T, index: number) => string;
- ListEmptyComponent?: ReactNode;
- ListHeaderComponent?: ReactNode;
- ListFooterComponent?: ReactNode;
- };
- export function RefreshableList<T>({
- data,
- renderItem,
- onRefresh,
- keyExtractor,
- ListEmptyComponent,
- ListHeaderComponent,
- ListFooterComponent,
- }: Props<T>) {
- const [refreshing, setRefreshing] = useState(false);
- const handleRefresh = useCallback(async () => {
- if (!onRefresh) return;
- setRefreshing(true);
- try {
- await onRefresh();
- } finally {
- setRefreshing(false);
- }
- }, [onRefresh]);
- return (
- <FlatList
- data={data}
- keyExtractor={keyExtractor ?? ((_, i) => String(i))}
- renderItem={({ item, index }) => renderItem(item, index)}
- refreshControl={
- onRefresh ? (
- <RefreshControl
- refreshing={refreshing}
- onRefresh={handleRefresh}
- colors={['#8B5CF6']}
- tintColor='#8B5CF6'
- />
- ) : undefined
- }
- ListEmptyComponent={ListEmptyComponent ? <View>{ListEmptyComponent}</View> : null}
- ListHeaderComponent={ListHeaderComponent ? <View>{ListHeaderComponent}</View> : null}
- ListFooterComponent={ListFooterComponent ? <View>{ListFooterComponent}</View> : null}
- />
- );
- }
复制代码 封装后使用更简单:- <RefreshableList
- data={items}
- renderItem={(item) => <ItemCard item={item} />}
- onRefresh={async () => {
- const newItems = await fetchItems();
- setItems(newItems);
- }}
- ListEmptyComponent={<EmptyState text='暂无数据' />}
- />
复制代码
六、下拉刷新与上拉加载的区别
下拉刷新是拉到顶部继续下拉,触发刷新并获取最新数据;上拉加载是拉到底部继续上拉,触发加载更多并获取下一页。RefreshControl 只负责下拉刷新,上拉加载通常用 FlatList 的 onEndReached:- <FlatList
- data={items}
- onEndReached={loadMore}
- onEndReachedThreshold={0.5}
- refreshControl={
- <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
- }
- />
复制代码
七、商品列表刷新逻辑
电商商品列表是典型场景。刷新时应重新请求第一页,再替换旧数据;加载更多才追加下一页,否则刷新后数据会重复:
[code]
import { useState, useCallback, useEffect } from 'react';
import { FlatList, RefreshControl } from 'react-native';
function ProductList() {
const [products, setProducts] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const [page, setPage] = useState(1);
useEffect(() => {
loadProducts(1);
}, []);
const loadProducts = async (pageNum: number) => {
const res = await fetch('/api/products?page=' + pageNum);
const data = await res.json();
if (pageNum === 1) {
setProducts(data.list);
} else {
setProducts((prev) => [...prev, ...data.list]);
}
setPage(pageNum);
};
const onRefresh = useCallback(async () => {
setRefreshing(true);
try {
await loadProducts(1);
} finally {
setRefreshing(false);
}
}, []);
const onEndReached = useCallback(async () => {
await loadProducts(page + 1);
}, |