请添加图片描述

案例项目开源地址:https://atomgit.com/nutpi/wanandroid_rn_openharmony

列表是应用的骨架

打开任何一个资讯类 App,首页必然是一个列表。微信的聊天列表、微博的动态流、知乎的推荐页,都是列表。列表承载了应用最核心的内容展示功能。

我们的 WanAndroid 技术资讯应用也不例外。首页需要展示从 wanandroid.com 获取的技术文章列表,用户可以滚动浏览、下拉刷新、上拉加载更多。这一切的基础,就是 FlatList 组件。


为什么选择 FlatList

React Native 提供了几种列表组件:ScrollViewFlatListSectionList

ScrollView 最简单,但它会一次性渲染所有子元素。如果列表有 100 条数据,就会渲染 100 个组件。数据量大的时候,内存占用高,滚动卡顿。

FlatList 采用了"虚拟化"技术。它只渲染当前可见区域的元素,滚动时动态回收和复用。100 条数据,可能只渲染 10 个组件。内存占用低,滚动流畅。

对于文章列表这种数据量不确定、可能很多的场景,FlatList 是最佳选择。


首页的数据结构

先看看我们要展示什么数据。wanandroid 的首页文章接口返回的数据结构大概是这样:

interface Article {
  id: number;
  title: string;
  author: string;
  shareUser: string;
  niceDate: string;
  link: string;
  superChapterName: string;
  chapterName: string;
  collect: boolean;
  fresh: boolean;
  envelopePic: string;
  desc: string;
}

每篇文章有标题、作者、日期、链接、分类等信息。fresh 表示是否是新文章,collect 表示是否已收藏。


首页组件的状态管理

首页需要管理几个状态:文章列表、置顶文章、Banner、当前页码、加载状态、刷新状态。

export const HomePage = () => {
  const {theme} = useTheme();
  const [articles, setArticles] = useState<Article[]>([]);
  const [topArticles, setTopArticles] = useState<Article[]>([]);
  const [banners, setBanners] = useState<Banner[]>([]);
  const [page, setPage] = useState(0);
  const [loading, setLoading] = useState(false);
  const [refreshing, setRefreshing] = useState(false);

  useEffect(() => {
    loadData(true);
  }, []);

page 从 0 开始,这是 wanandroid API 的约定。loading 用于上拉加载更多时显示加载指示器,refreshing 用于下拉刷新时显示刷新动画。

组件挂载时调用 loadData(true)true 表示这是一次刷新操作,需要重置页码并重新加载所有数据。


加载数据的逻辑

const loadData = async (refresh = false) => {
  if (refresh) {
    setRefreshing(true);
    setPage(0);
  } else {
    setLoading(true);
  }

  try {
    if (refresh) {
      const [bannerRes, topRes, listRes] = await Promise.all([
        homeApi.getBanners(),
        homeApi.getTopArticles(),
        homeApi.getArticles(0),
      ]);
      if (bannerRes.errorCode === 0) setBanners(bannerRes.data);
      if (topRes.errorCode === 0) setTopArticles(topRes.data);
      if (listRes.errorCode === 0) setArticles(listRes.data.datas);
    }
  } catch (e) {
    console.error('加载首页失败', e);
  }

  setLoading(false);
  setRefreshing(false);
};

刷新时,用 Promise.all 并行请求三个接口:Banner、置顶文章、普通文章列表。并行请求比串行快,用户等待时间更短。

每个接口返回的数据都有 errorCode 字段,0 表示成功。这是 wanandroid API 的统一约定。


加载更多的逻辑

const loadMore = async () => {
  if (loading) return;
  setLoading(true);
  const nextPage = page + 1;
  try {
    const res = await homeApi.getArticles(nextPage);
    if (res.errorCode === 0) {
      setArticles([...articles, ...res.data.datas]);
      setPage(nextPage);
    }
  } catch (e) {}
  setLoading(false);
};

加载更多时,先检查是否正在加载,避免重复请求。然后请求下一页数据,成功后追加到现有列表末尾。

注意 setArticles([...articles, ...res.data.datas]) 这种写法,用展开运算符创建新数组,而不是直接 push。这是 React 状态更新的正确姿势,直接修改原数组不会触发重新渲染。


合并置顶文章和普通文章

const allArticles = [
  ...topArticles.map(a => ({...a, isTop: true})),
  ...articles,
];

置顶文章要显示在列表最前面,所以把它们放在数组开头。给置顶文章加一个 isTop: true 标记,渲染时可以显示"置顶"标签。


FlatList 的核心配置

return (
  <FlatList
    data={allArticles}
    keyExtractor={(item, index) => `${item.id}-${index}`}
    renderItem={({item}) => (
      <View>
        {(item as any).isTop && (
          <View style={[styles.topTag, {backgroundColor: theme.danger}]}>
            <Text style={styles.topText}>置顶</Text>
          </View>
        )}
        <ArticleCard item={item} />
      </View>
    )}
    ListHeaderComponent={<BannerSection banners={banners} />}
    contentContainerStyle={styles.list}
    showsVerticalScrollIndicator={false}
    refreshControl={
      <RefreshControl refreshing={refreshing} onRefresh={() => loadData(true)} tintColor={theme.accent} />
    }
    onEndReached={loadMore}
    onEndReachedThreshold={0.3}
    ListFooterComponent={loading ? <ActivityIndicator color={theme.accent} style={{padding: 20}} /> : null}
    ListEmptyComponent={
      !loading ? (
        <View style={styles.empty}>
          <Text style={styles.emptyIcon}>📰</Text>
          <Text style={[styles.emptyText, {color: theme.subText}]}>暂无文章</Text>
        </View>
      ) : null
    }
  />
);

逐个解释这些属性:

data

列表的数据源,是一个数组。FlatList 会遍历这个数组,为每个元素调用 renderItem

keyExtractor

为每个列表项生成唯一的 key。React 用 key 来追踪元素的变化,优化渲染性能。

这里用 ${item.id}-${index} 而不是单独用 item.id,是因为置顶文章和普通文章可能有相同的 id(置顶文章也会出现在普通列表里),加上 index 可以避免 key 重复。

renderItem

渲染每个列表项的函数。接收一个对象参数,包含 item(当前数据)、index(索引)等。

我们检查 isTop 标记,如果是置顶文章就显示一个红色的"置顶"标签,然后渲染 ArticleCard 组件。

ListHeaderComponent

列表头部组件,显示在所有列表项之前。我们把 Banner 轮播图放在这里。

refreshControl

下拉刷新控件。RefreshControlReact Native 内置组件,refreshing 控制刷新动画的显示,onRefresh 是触发刷新时的回调。

tintColor 设置刷新指示器的颜色,我们用主题的强调色。

onEndReached 和 onEndReachedThreshold

onEndReached 是滚动到列表底部时的回调,用于触发加载更多。

onEndReachedThreshold 是触发阈值,0.3 表示距离底部还有 30% 的距离时就触发。设置一个提前量,用户滚动时数据就开始加载了,体验更流畅。

ListFooterComponent

列表底部组件。加载更多时显示一个 ActivityIndicator 转圈动画,告诉用户正在加载。

ListEmptyComponent

列表为空时显示的组件。注意要判断 !loading,否则首次加载时也会显示空状态。


样式定义

const styles = StyleSheet.create({
  list: {paddingBottom: 100},
  topTag: {paddingHorizontal: 8, paddingVertical: 2, borderRadius: 4, alignSelf: 'flex-start', marginBottom: 4},
  topText: {color: '#fff', fontSize: 10, fontWeight: 'bold'},
  empty: {alignItems: 'center', paddingVertical: 60},
  emptyIcon: {fontSize: 64, marginBottom: 16},
  emptyText: {fontSize: 16},
});

listpaddingBottom: 100 是为了给底部 Tab 栏留出空间,避免最后一条数据被遮挡。

topTagalignSelf: 'flex-start' 让标签宽度自适应内容,而不是撑满整行。


API 封装

首页用到的 API 封装在 src/services/api.ts 里:

export const homeApi = {
  getBanners: () => api.get('/banner/json'),
  getTopArticles: () => api.get('/article/top/json'),
  getArticles: (page: number) => api.get(`/article/list/${page}/json`),
};

三个接口:

  • /banner/json 获取 Banner 数据
  • /article/top/json 获取置顶文章
  • /article/list/{page}/json 获取文章列表,page 从 0 开始

把 API 调用封装成函数,调用方不需要关心具体的 URL 和请求方式,代码更清晰。


性能优化建议

FlatList 还有一些性能优化的属性,虽然我们的代码里没有用到,但值得了解:

initialNumToRender

首次渲染的元素数量,默认是 10。如果首屏能显示 5 条,可以设置成 5,减少首次渲染的工作量。

maxToRenderPerBatch

每次渲染的最大元素数量,默认是 10。数值越小,渲染越平滑,但滚动快时可能出现空白。

windowSize

渲染窗口的大小,默认是 21(可见区域的 21 倍)。数值越大,滚动时空白越少,但内存占用越高。

getItemLayout

如果每个列表项高度固定,可以提供这个函数,FlatList 就不需要动态测量高度,滚动更流畅。

我们的文章卡片高度不固定(有的有描述,有的没有),所以没有用这个属性。


小结

FlatListReact Native 中最常用的列表组件,通过虚拟化技术实现高性能滚动。配合 RefreshControl 实现下拉刷新,配合 onEndReached 实现上拉加载更多,就能构建一个完整的资讯列表页面。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐