在这里插入图片描述

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

追番党最关心的问题之一就是"现在有什么好看的在播"。正在热播页面就是为了解决这个需求,它展示当前正在播出且评分较高的动漫作品。

从用户需求说起

想象一下这个场景:小明刚入坑动漫,想找一些正在播出的热门作品来追。他不想看已经完结的老番(怕追不上进度),也不想看冷门作品(怕踩雷)。正在热播页面就是为小明这样的用户准备的。

这个页面要回答一个简单的问题:现在有什么值得追的番?

页面长什么样

打开正在热播页面,你会看到一个纵向滚动的列表。每个列表项显示一部动漫的基本信息:

  • 左侧是排名数字(1、2、3…)
  • 中间是封面缩略图
  • 右侧是标题、评分、集数等信息

列表按热度排序,最火的在最上面。滚动到底部会自动加载更多。

核心实现思路

这个页面的实现可以分解为三个问题:

问题一:数据从哪来?

Jikan API 提供了 Top Anime 接口,通过 filter 参数可以筛选正在播出的动漫:

const res = await getTopAnime(pageNum, 'airing');

'airing' 就是"正在播出"的意思。API 会返回按热度排序的结果。

问题二:如何显示排名?

FlatList 的 renderItem 回调会传入 index 参数,表示当前项在数组中的位置。用 index + 1 就是排名:

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

问题三:如何实现无限滚动?

FlatList 提供了 onEndReached 回调,当滚动到底部时触发。我们在这个回调里加载下一页数据:

<FlatList
  onEndReached={handleLoadMore}
  onEndReachedThreshold={0.5}
/>

完整代码拆解

先看导入部分,没什么特别的:

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';

这里用到了三个 React Hook:

  • useState 管理状态
  • useEffect 处理副作用(数据加载)
  • useCallback 缓存函数引用

接下来是状态定义。分页列表需要的状态比较固定,几乎是个模板:

export const AiringAnimeScreen = ({ 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);

五个状态各司其职:

  • animeList 存数据
  • loading 控制首屏加载
  • loadingMore 控制加载更多
  • page 记录当前页码
  • hasMore 标记是否还有数据

数据加载函数是页面的核心。注意它接收两个参数:页码和是否追加:

const loadData = async (pageNum: number, append = false) => {
  try {
    if (pageNum === 1) setLoading(true);
    else setLoadingMore(true);
    
    const res = await getTopAnime(pageNum, 'airing');
    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);
  }
};

这段代码有几个细节值得注意:

第一,根据页码决定显示哪种 Loading。首页用全屏 Loading,后续页用底部小 Loading。用户体验完全不同。

第二,append 参数决定数据是替换还是追加。首次加载替换,加载更多追加。

第三,setAnimeList(prev => [...prev, ...newData]) 用函数式更新。这样可以确保拿到最新的 prev 值,避免闭包陷阱。

第四,finally 块确保无论成功失败都重置 Loading 状态。不然出错时页面会一直转圈。

加载更多的处理函数用 useCallback 包裹:

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

为什么要用 useCallback?因为这个函数会传给 FlatList 的 onEndReached。如果每次渲染都创建新函数,可能导致不必要的重渲染。useCallback 会缓存函数引用,只有依赖项变化时才创建新函数。

条件判断 !loadingMore && hasMore 很重要:

  • !loadingMore 防止重复请求(用户快速滚动时可能多次触发)
  • hasMore 防止无效请求(已经没有更多数据了)

渲染部分的两种状态

首屏加载时,显示全屏 Loading:

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

注意即使在加载中也显示 Header。这样用户知道自己在哪个页面,也可以点返回取消。

加载完成后,显示列表:

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="movie" title="暂无数据" />}
    />
  </View>
);

FlatList 的配置项挺多,逐个解释:

keyExtractor 告诉 FlatList 如何识别每个项。用动漫的 mal_id(MyAnimeList ID)作为 key,因为它是唯一的。

onEndReachedThreshold={0.5} 设置触发加载的时机。0.5 表示距离底部还有 50% 时就开始加载。这样等用户滚到底部时,新数据可能已经加载好了。

ListFooterComponent 在列表底部显示内容。加载更多时显示 Loading,否则不显示。

ListEmptyComponent 在列表为空时显示。虽然正在热播不太可能为空,但加上这个是好习惯。

样式部分

样式很简单,只有两个:

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

大部分样式都在 AnimeListItem 组件里定义了。页面只需要设置容器和列表的基本样式。

这体现了组件化的好处:样式封装在组件内部,使用组件的页面不需要关心细节。

和其他排行页的关系

AnimeHub 有好几个排行页面:

页面 filter 参数 含义
正在热播 airing 正在播出的动漫
即将上映 upcoming 即将播出的动漫
人气排行 bypopularity 按人气排序
最受喜爱 favorite 按收藏数排序

这些页面的代码结构几乎一样,只是 filter 参数不同。在实际项目中,可以考虑合并成一个通用页面,通过路由参数区分。

但在教程项目中,保持独立的文件更清晰。读者可以单独阅读每个页面的代码,不需要理解复杂的参数传递。

为什么用列表而不是网格

正在热播页面用列表布局,而不是网格布局。这是有意为之的设计选择。

列表布局的优势:

  • 可以显示排名数字
  • 可以显示更多文字信息(评分、集数、状态)
  • 阅读顺序明确(从上到下)

网格布局的优势:

  • 一屏显示更多内容
  • 封面图更大更醒目
  • 视觉上更丰富

对于排行榜类页面,列表布局更合适。用户关心的是"第几名"和"评分多少",这些信息在列表布局中更容易获取。

性能优化点

FlatList 本身就是性能优化的产物。它只渲染可见区域的内容,滚动时复用已有的组件实例。

但还有一些可以进一步优化的地方:

图片优化:AnimeListItem 中的封面图可以用较小的尺寸。API 返回多种尺寸的图片 URL,选择合适的尺寸可以减少流量和内存占用。

数据缓存:正在热播的数据不会频繁变化,可以缓存一段时间。用户短时间内多次访问时,直接用缓存数据,不需要重新请求。

预加载:当用户滚动到列表中部时,可以预加载下一页数据。这样用户滚到底部时,数据已经准备好了。

小结

正在热播页面是一个标准的分页列表页面,展示当前正在播出的热门动漫。页面使用 FlatList 实现无限滚动,通过 onEndReached 触发加载更多。

排名显示利用了 FlatList renderItem 的 index 参数,简单直接。列表布局比网格布局更适合排行榜场景,因为可以清晰地显示排名和评分信息。

这个页面的代码结构可以作为分页列表的模板,其他类似页面(人气排行、最受喜爱等)都可以参考这个实现。

下一篇讲即将上映页面,展示还没开播但即将播出的动漫。


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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐