RN for OpenHarmony AnimeHub项目实战:人气排行页面开发

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

评分高 ≠ 人气高

在动漫圈有个有趣的现象:评分最高的作品和最火的作品往往不是同一批。

评分榜前几名通常是《钢之炼金术师》《银魂》这样的经典老番,它们经过时间检验,口碑极佳。但要说"人气",可能当季的热门新番更高,因为正在播出,讨论度高,观看人数多。

所以 AnimeHub 提供了两个不同的排行:

  • 热门排行(按评分)- 质量最好的作品
  • 人气排行(按人气)- 最多人看的作品

这篇讲的是人气排行页面。

人气是怎么算的

MyAnimeList 的人气(popularity)是根据"有多少用户把这部动漫加入了自己的列表"来计算的。不管是"想看"、"在看"还是"看过",只要加入列表就算。

这个指标反映的是"知名度"和"关注度",而不是"好不好看"。一部作品可能评分一般,但因为话题性强、宣传到位,人气很高。

一分钟看完核心代码

人气排行页面的实现非常简单,和热门排行页面几乎一样,只是 API 参数不同:

typescript 复制代码
// 热门排行(按评分)
const res = await getTopAnime(pageNum, '');

// 人气排行(按人气)
const res = await getTopAnime(pageNum, 'bypopularity');

就这一个参数的区别。'bypopularity' 告诉 API 按人气排序而不是按评分排序。

逐段解析

导入和类型

typescript 复制代码
import React, { useEffect, useState, useCallback } from 'react';
import { View, FlatList, StyleSheet } from 'react-native';
import { Colors, Spacing } from '../../theme';
import { Anime } from '../../types';
import { getTopAnime } from '../../api/jikan';
import { AnimeListItem } from '../../components/anime';
import { Header, Loading, EmptyState } from '../../components/common';

没什么特别的,标准的分页列表页面导入。

组件和状态

typescript 复制代码
export const PopularAnimeScreen = ({ navigation }: any) => {
  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);

分页列表的标准五件套,之前讲过很多次了。

数据加载

typescript 复制代码
  const loadData = async (pageNum: number, append = false) => {
    try {
      if (pageNum === 1) setLoading(true);
      else setLoadingMore(true);
      
      const res = await getTopAnime(pageNum, 'bypopularity');
      const newData = res.data || [];
      
      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);
    }
  };

唯一的区别就是 getTopAnime(pageNum, 'bypopularity') 这里的第二个参数。

副作用和回调

typescript 复制代码
  useEffect(() => {
    loadData(1);
  }, []);

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

组件挂载时加载第一页,滚动到底部时加载更多。

渲染列表项

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

显示排名,用 index + 1。点击跳转详情页。

条件渲染

typescript 复制代码
  if (loading) {
    return (
      <View style={styles.container}>
        <Header title="人气排行" showBack onBack={() => navigation.goBack()} />
        <Loading fullScreen text="加载中..." />
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <Header 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="users" title="暂无数据" />}
      />
    </View>
  );
};

空状态用了 users 图标,暗示"人气"是由用户数量决定的。

样式

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

两个样式,简洁明了。

代码复用的思考

看到这里你可能会想:这代码和热门排行、正在热播几乎一模一样啊,就改了个参数和标题,为什么要写成独立的页面?

这是个好问题。有两种处理方式:

方式一:独立页面(当前做法)

每个排行榜一个独立的页面文件。

优点:

  • 代码直观,每个文件职责单一
  • 修改某个页面不会影响其他页面
  • 适合教程项目,读者容易理解

缺点:

  • 代码重复
  • 修改通用逻辑需要改多个文件

方式二:通用页面 + 参数

一个通用的排行榜页面,通过路由参数区分:

typescript 复制代码
// 路由配置
<Stack.Screen name="TopAnime" component={TopAnimeScreen} />

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

// 页面内获取参数
const { filter = '', title = '排行榜' } = route.params || {};
const res = await getTopAnime(pageNum, filter);

优点:

  • 代码不重复
  • 修改一处,所有排行榜都生效

缺点:

  • 逻辑稍复杂
  • 如果某个排行榜需要特殊处理,会变得麻烦

实际项目中,方式二更常用。但在教程中,方式一更容易讲解。

人气榜 vs 评分榜的数据差异

拿真实数据举例,人气榜前几名可能是:

  1. Death Note(死亡笔记)
  2. Shingeki no Kyojin(进击的巨人)
  3. Sword Art Online(刀剑神域)

而评分榜前几名可能是:

  1. Fullmetal Alchemist: Brotherhood(钢之炼金术师)
  2. Steins;Gate(命运石之门)
  3. Gintama(银魂)

两个榜单的重合度并不高。人气榜更偏向"大众向"、"话题性强"的作品,评分榜更偏向"口碑好"、"经得起推敲"的作品。

这就是为什么要提供两个榜单:满足不同用户的需求。有人想看"大家都在看什么",有人想看"什么最值得看"。

排名显示的细节

AnimeListItem 组件接收 rank 参数来显示排名:

typescript 复制代码
<AnimeListItem
  anime={item}
  rank={index + 1}
  onPress={...}
/>

组件内部会根据排名显示不同的样式:

  • 前三名可能用金银铜色
  • 其他名次用普通样式

这个逻辑封装在组件内部,页面不需要关心。

但有个潜在问题:当加载第二页时,index 从 0 重新开始。如果第一页有 25 条数据,第二页第一条的 index 是 0,显示的排名是 1,但实际应该是 26。

解决方案是用累计索引:

typescript 复制代码
const renderItem = ({ item, index }: { item: Anime; index: number }) => (
  <AnimeListItem
    anime={item}
    rank={index + 1}  // 这里 FlatList 会自动处理累计索引
    onPress={...}
  />
);

好消息是,FlatList 的 index 是相对于整个 data 数组的,不是相对于当前页的。所以当我们用 [...prev, ...newData] 追加数据时,index 会自动累加。第二页第一条的 index 确实是 25(假设每页 25 条),排名显示 26,是正确的。

小结

人气排行页面展示按人气(用户关注数)排序的动漫,和按评分排序的热门排行形成互补。实现上只是 API 参数不同,其他代码几乎一样。

这种高度相似的页面在实际项目中可以抽象成通用组件,通过参数区分。但在教程项目中,保持独立文件更便于理解和学习。

人气和评分是两个不同的维度。人气高的作品不一定评分高,评分高的作品不一定人气高。提供两个榜单可以满足不同用户的需求。

下一篇是最后一篇,讲最受喜爱页面,展示被收藏最多的动漫。


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

相关推荐
IT大白鼠4 天前
Redis 系列 · 第 01 篇——认知入门:Redis 是什么
redis·nosql
彧azz4 天前
Linux 环境下 Redis 学习总结:数据类型、持久化、锁、事务、主从与缓存问题
linux·redis·笔记·学习·面试
lbb 小魔仙4 天前
OpenClaw + cpolar 实战:远程 NAS、分享小游戏、RDP,再配置公网 AI 入口
数据库·人工智能·redis·oracle·prometheus
夕除4 天前
redis--018
redis
两锭子4 天前
Redis基础
redis
烟沙九洲4 天前
Redis 缓存穿透、缓存击穿、缓存雪崩
数据库·redis
字节探索4 天前
Redis 只会当缓存用?这 10 大实战场景,让你的系统快到飞起
redis·后端
于樱花森上飞舞4 天前
【Redis】哨兵详解
java·开发语言·数据库·redis
Rain的Java大神之路4 天前
如何快速上传10G文件
java·spring boot·redis·后端·mysql·spring cloud·面试
Wang's Blog5 天前
Java 接入Redis: 通用命令与键管理
java·服务器·redis