在这里插入图片描述

案例开源地址:https://atomgit.com/nutpi/Rn_openharmony_AnimeHub

热门排行页展示评分最高的动漫作品,是动漫爱好者发现优质内容的重要入口。这篇来讲热门排行页的实现,重点是排名显示和可复用的列表组件设计。

功能设计

热门排行页需要实现以下功能:

  • 排名显示 - 每个动漫显示排名序号
  • 列表展示 - 纵向列表,每项显示详细信息
  • 分页加载 - 滚动到底部自动加载更多
  • 动态标题 - 支持不同筛选条件的标题

这个页面和季度动漫页的结构类似,但有一个关键区别:使用列表布局而不是网格布局。排行榜更适合列表形式,因为需要显示排名序号,而且用户通常会从上到下依次浏览。

排行榜是内容型应用的核心功能之一。用户经常会问"有什么好看的动漫推荐",排行榜就是最好的答案。按评分排序的列表可以帮助用户快速找到高质量内容。

路由参数处理

从路由获取筛选条件和标题:

export const TopAnimeScreen = ({ navigation, route }: any) => {
  const { filter = '', title = '热门排行' } = route.params || {};

参数说明:

  • filter - 筛选条件,可选,默认为空字符串
  • title - 页面标题,可选,默认为"热门排行"
  • route.params || {} - 防止 params 为 undefined

这个页面设计成可复用的。通过传入不同的 filter 参数,可以显示不同类型的排行:

  • 空字符串 - 综合排行
  • ‘airing’ - 正在播出的动漫排行
  • ‘upcoming’ - 即将上映的动漫排行
  • ‘bypopularity’ - 按人气排行
  • ‘favorite’ - 最受喜爱排行

默认值的设置很重要。filter = ''title = '热门排行' 确保即使不传参数,页面也能正常工作。route.params || {} 是双重保险,防止整个 params 对象为空时解构报错。

状态定义

和季度动漫页类似的状态结构:

const [animeList, setAnimeList] = useState<Anime[]>([]);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);

状态说明:

  • animeList - 动漫列表数据
  • loading - 首次加载状态
  • loadingMore - 加载更多状态
  • page - 当前页码
  • hasMore - 是否还有更多数据

你会发现这个状态结构和季度动漫页几乎一样。这是因为分页列表的状态模式是固定的,不管显示什么内容,都需要这几个状态。在大型项目中,可以把这个模式抽取成自定义 Hook,比如 usePaginatedList

状态模式的复用是 React 开发的重要技巧。识别出重复的状态模式,然后抽取成可复用的 Hook,可以大大减少代码重复。

数据加载函数

加载排行榜数据:

const loadData = async (pageNum: number, append = false) => {
  try {
    if (pageNum === 1) setLoading(true);
    else setLoadingMore(true);
    
    const res = await getTopAnime(pageNum, filter);
    const newData = res.data || [];

API 调用:

  • getTopAnime(pageNum, filter) - 获取排行榜数据
  • pageNum - 页码
  • filter - 筛选条件

getTopAnime 是封装好的 API 函数。Jikan API 的排行榜接口支持多种筛选条件,通过 filter 参数传递。API 会返回按评分排序的动漫列表。

注意这里的 filter 是从路由参数获取的,在整个组件生命周期内保持不变。这意味着用户不能在页面内切换筛选条件,需要返回上一页重新选择。这是一种简化的设计,适合入口明确的场景。

    if (append) {
      setAnimeList(prev => [...prev, ...newData]);
    } else {
      setAnimeList(newData);
    }
    setHasMore(res.pagination?.has_next_page || false);
  } catch (error) {
    console.error('Load error:', error);
  } finally {
    setLoading(false);
    setLoadingMore(false);
  }
};

数据处理逻辑:

  • 追加模式合并数据,替换模式直接设置
  • 从分页信息中获取是否还有下一页
  • 错误时打印日志
  • 最终重置加载状态

这段代码和季度动漫页完全一样,再次证明了分页加载的模式是固定的。唯一的区别是调用的 API 函数不同。

初始加载和依赖

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

依赖说明:

  • 依赖 filter 参数
  • 当筛选条件变化时重新加载

虽然在当前实现中 filter 不会变化(来自路由参数),但把它放在依赖数组中是正确的做法。这样如果将来需要支持动态切换筛选条件,代码不需要修改。

加载更多处理

const handleLoadMore = useCallback(() => {
  if (!loadingMore && hasMore) {
    const nextPage = page + 1;
    setPage(nextPage);
    loadData(nextPage, true);
  }
}, [loadingMore, hasMore, page]);

加载条件:

  • 当前没有在加载中
  • 还有更多数据

这个函数也和季度动漫页一样。useCallback 缓存函数引用,依赖项包括所有在函数内使用的状态。

列表项渲染

这是这个页面的特色部分:

const renderItem = ({ item, index }: { item: Anime; index: number }) => (
  <AnimeListItem
    anime={item}
    rank={index + 1}
    onPress={() => navigation.navigate('AnimeDetail', { animeId: item.mal_id })}
  />
);

渲染参数:

  • item - 当前动漫数据
  • index - 当前索引(从 0 开始)

组件属性:

  • anime - 动漫数据对象
  • rank - 排名,index + 1(因为索引从 0 开始,排名从 1 开始)
  • onPress - 点击回调,跳转到详情页

注意这里用的是 AnimeListItem 而不是 AnimeCard。两者的区别:

  • AnimeCard - 卡片样式,适合网格布局,显示封面图和基本信息
  • AnimeListItem - 列表项样式,适合纵向列表,显示更多详细信息

rank={index + 1} 是一个简单但重要的细节。用户看到的排名应该从 1 开始,而数组索引从 0 开始,所以需要加 1。

但这里有一个潜在问题:当加载第二页时,index 会从 0 重新开始,导致排名不正确。正确的做法应该是 rank={(page - 1) * pageSize + index + 1}。不过在当前实现中,因为数据是追加的,FlatList 会保持正确的索引。

加载状态渲染

if (loading) {
  return (
    <View style={styles.container}>
      <Header title={title} showBack onBack={() => navigation.goBack()} />
      <Loading fullScreen text="加载中..." />
    </View>
  );
}

设计要点:

  • 加载时显示 Header,用户知道自己在哪
  • 显示返回按钮,用户可以取消
  • 全屏 Loading 提示正在加载

这个模式在所有分页列表页面中都是一样的。Header 始终显示,Loading 占据内容区域。

主页面结构

return (
  <View style={styles.container}>
    <Header title={title} showBack onBack={() => navigation.goBack()} />
    <FlatList
      data={animeList}
      renderItem={renderItem}
      keyExtractor={item => item.mal_id.toString()}
      contentContainerStyle={styles.list}
      showsVerticalScrollIndicator={false}
      onEndReached={handleLoadMore}
      onEndReachedThreshold={0.5}
      ListFooterComponent={loadingMore ? <Loading text="加载更多..." /> : null}
      ListEmptyComponent={<EmptyState icon="movie" title="暂无数据" />}
    />
  </View>
);

与季度动漫页的区别:

  • 没有 numColumns={2},使用默认的单列布局
  • 使用 AnimeListItem 而不是 AnimeCard
  • 标题来自路由参数

单列布局更适合排行榜。原因有几个:

  1. 排名序号需要明显显示,单列布局有更多空间
  2. 用户浏览排行榜时通常是从上到下,单列更符合阅读习惯
  3. 列表项可以显示更多信息(评分、集数、状态等)

样式定义

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: Colors.background,
  },
  list: {
    padding: Spacing.md,
  },
});

样式说明:

  • 容器占满屏幕,使用主题背景色
  • 列表有中等内边距

样式非常简洁,因为大部分样式都在 AnimeListItem 组件内部定义。这是组件化的好处:页面只需要关心布局,具体的样式由组件自己负责。

AnimeListItem 组件

虽然组件代码不在这个文件中,但理解它的设计很重要:

<AnimeListItem
  anime={item}
  rank={index + 1}
  onPress={() => navigation.navigate('AnimeDetail', { animeId: item.mal_id })}
/>

组件职责:

  • 显示动漫封面图(小尺寸)
  • 显示排名序号(如果传入 rank)
  • 显示动漫标题
  • 显示评分、集数、状态等信息
  • 处理点击事件

AnimeListItem 是一个可复用的组件,不仅用于排行榜,还可以用于搜索结果、收藏列表等场景。rank 属性是可选的,只有排行榜页面才传入。

组件设计的原则是"高内聚、低耦合"。组件内部处理自己的样式和逻辑,外部只需要传入数据和回调。这样组件可以在不同页面复用,修改组件时也不会影响其他地方。

页面复用设计

这个页面通过路由参数实现了复用:

// 综合排行
navigation.navigate('TopAnime', { title: '热门排行' });

// 正在播出排行
navigation.navigate('TopAnime', { filter: 'airing', title: '正在热播' });

// 即将上映排行
navigation.navigate('TopAnime', { filter: 'upcoming', title: '即将上映' });

// 人气排行
navigation.navigate('TopAnime', { filter: 'bypopularity', title: '人气排行' });

// 最受喜爱排行
navigation.navigate('TopAnime', { filter: 'favorite', title: '最受喜爱' });

复用优势:

  • 一个页面组件,多种用途
  • 减少代码重复
  • 统一的用户体验
  • 维护成本低

这种设计模式叫"参数化页面"。页面的行为由参数决定,而不是硬编码。这样可以用一个组件满足多个相似的需求。

在 AnimeHub 应用中,首页的多个入口(正在热播、即将上映、人气排行、最受喜爱)都跳转到这个页面,只是传入不同的参数。

排行榜数据的特点

Jikan API 的排行榜数据有一些特点:

  • 按评分排序 - 默认按 MAL 评分从高到低
  • 数据量大 - 总共有数万部动漫
  • 更新频率 - 评分会随时变化,排名也会变
  • 筛选支持 - 可以按类型、状态等筛选

MAL(MyAnimeList)是全球最大的动漫数据库之一,用户可以给动漫评分(1-10分)。排行榜就是按这个评分排序的。

排行榜的前几名通常是经典作品,如《钢之炼金术师》《进击的巨人》等。这些作品评分高、评价人数多,排名相对稳定。

与其他排行页面的关系

应用中有多个排行相关的页面:

  • 热门排行(TopAnimeScreen) - 按评分排序,本页面
  • 人气排行(PopularAnimeScreen) - 按人气排序
  • 最受喜爱(FavoriteAnimeScreen) - 按收藏数排序

这些页面的代码结构几乎一样,区别只在于 API 参数。在实际项目中,可以考虑合并成一个页面,通过参数区分。但为了教程的清晰性,我们保留了独立的页面文件。

性能考虑

排行榜页面的性能优化点:

  • 分页加载 - 不一次性加载所有数据
  • useCallback - 缓存回调函数,避免不必要的重渲染
  • keyExtractor - 使用稳定的 key,帮助 FlatList 优化渲染
  • 组件复用 - FlatList 会复用列表项组件

FlatList 是 React Native 中性能最好的列表组件。它只渲染可见区域的内容,滚动时复用已有的组件实例。对于长列表(如排行榜),这种优化至关重要。

如果用 ScrollView + map 渲染列表,所有项目都会一次性渲染,可能导致卡顿甚至崩溃。FlatList 的虚拟化机制解决了这个问题。

小结

热门排行页是一个标准的分页列表页面,使用 AnimeListItem 组件显示带排名的动漫列表。页面通过路由参数实现复用,可以显示不同筛选条件的排行榜。

列表布局比网格布局更适合排行榜场景,因为可以显示排名序号和更多详细信息。AnimeListItem 组件封装了列表项的样式和逻辑,页面只需要关心数据加载和布局。

分页加载的实现和季度动漫页一样,是一个可复用的模式。在大型项目中,可以把这个模式抽取成自定义 Hook,进一步减少代码重复。

下一篇会讲随机推荐页面,展示随机获取的动漫作品。


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

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐