RN for OpenHarmony AnimeHub项目实战:正在热播页面开发
正在热播页面为动漫爱好者提供当前热门在播作品推荐,主要功能包括: 核心功能:展示正在播出且评分较高的动漫列表,按热度排序 技术实现: 使用Jikan API获取数据,通过airing参数筛选正在播出的动漫 采用分页加载方式,支持无限滚动 通过FlatList组件实现高效列表渲染 用户体验优化: 显示排名序号(1/2/3...) 区分首次加载和加载更多状态 添加空状态和加载提示 代码特点: 使用Re

案例开源地址: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
更多推荐

所有评论(0)