React Native鸿蒙跨平台完成动漫应用实现本地数据持久化、收藏管理、观看历史记录、标签页切换
本文探讨了一个基于React Native的动漫应用实现方案,重点分析了其架构设计、状态管理和跨平台兼容性策略。应用采用组件化架构,分离全局状态与UI渲染,使用TypeScript确保类型安全。通过AsyncStorage实现数据持久化,并采用同步机制保证内存与本地存储的一致性。针对跨平台开发,提出了组件兼容性、样式统一、本地存储抽象等适配策略。同时给出了性能优化建议,包括FlatList渲染优化
在移动应用开发中,媒体类应用是一种常见的应用类型,需要考虑内容管理、用户交互、数据持久化等多个方面。本文将深入分析一个功能完备的 React Native 动漫应用实现,探讨其架构设计、状态管理、数据持久化以及跨端兼容性策略。
组件化
该实现采用了清晰的组件化架构,主要包含以下部分:
- 主应用组件 (
AnimeApp) - 负责整体布局和状态管理 - 内容列表渲染 - 负责渲染动漫卡片列表
- 标签页导航 - 实现历史记录和收藏的切换
- 功能按钮 - 提供播放、收藏等功能
这种架构设计使得代码结构清晰,易于维护和扩展。主应用组件负责管理全局状态和业务逻辑,而各个功能部分负责具体的 UI 渲染,实现了关注点分离。
状态管理
AnimeApp 组件使用 useState 钩子管理多个关键状态:
const [animes, setAnimes] = useState<Anime[]>(initialAnimes);
const [history, setHistory] = useState<Anime[]>([]);
const [favorites, setFavorites] = useState<Anime[]>([]);
const [activeTab, setActiveTab] = useState<'history' | 'favorites'>('history');
const [loading, setLoading] = useState(true);
这种状态管理方式简洁高效,通过状态更新触发组件重新渲染,实现了动漫的展示、收藏和历史记录管理等功能。使用 TypeScript 类型定义确保了数据结构的类型安全,提高了代码的可靠性。
本地存储
应用集成了 AsyncStorage 实现数据持久化:
// 加载本地数据
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
const storedHistory = await AsyncStorage.getItem(STORAGE_KEYS.WATCH_HISTORY);
const storedFavorites = await AsyncStorage.getItem(STORAGE_KEYS.FAVORITES);
if (storedHistory) {
setHistory(JSON.parse(storedHistory));
} else {
setHistory(initialAnimes);
}
if (storedFavorites) {
setFavorites(JSON.parse(storedFavorites));
}
} catch (error) {
console.error('加载数据失败:', error);
setHistory(initialAnimes);
} finally {
setLoading(false);
}
};
// 保存数据到本地存储
const saveData = async (key: string, data: any) => {
try {
await AsyncStorage.setItem(key, JSON.stringify(data));
} catch (error) {
console.error('保存数据失败:', error);
}
};
这种实现方式确保了应用数据在应用重启后不会丢失,提高了用户体验。通过 useEffect 钩子在组件挂载时加载数据,确保了应用启动时能够显示之前的状态。
数据同步
应用实现了数据同步机制,当用户进行收藏、播放等操作时,会同时更新内存中的状态和本地存储:
// 切换收藏状态
const toggleFavorite = (anime: Anime) => {
const updatedFavorites = favorites.some(fav => fav.id === anime.id)
? favorites.filter(fav => fav.id !== anime.id)
: [...favorites, anime];
setFavorites(updatedFavorites);
saveData(STORAGE_KEYS.FAVORITES, updatedFavorites);
// 更新历史记录中的收藏状态
const updatedHistory = history.map(item =>
item.id === anime.id ? { ...item, isFavorite: !item.isFavorite } : item
);
setHistory(updatedHistory);
saveData(STORAGE_KEYS.WATCH_HISTORY, updatedHistory);
};
这种实现方式确保了内存状态和本地存储的一致性,避免了数据不同步的问题。同时,当更新收藏状态时,也会同步更新历史记录中的对应项,确保了数据的一致性。
动漫管理功能
应用实现了完整的动漫管理功能:
- 观看历史 - 记录用户观看过的动漫
- 收藏管理 - 管理用户收藏的动漫
- 标签页切换 - 在历史记录和收藏之间切换
- 数据清空 - 提供清空历史记录和收藏的功能
这些功能覆盖了动漫应用的基本需求,为用户提供了便捷的动漫管理体验。
类型定义
该实现使用 TypeScript 定义了核心数据类型:
// 动漫类型
type Anime = {
id: string;
title: string;
episode: string;
cover: string;
duration: string;
watchedTime: string;
date: string;
isFavorite: boolean;
};
这个类型定义包含了动漫的完整信息,包括:
- 基本信息 - 标题、集数、封面、时长
- 播放状态 - 已观看时间、观看日期
- 收藏状态 - 是否收藏
这种类型定义使得数据结构更加清晰,提高了代码的可读性和可维护性,同时也提供了类型安全保障。
数据组织
应用数据按照功能模块进行组织:
- animes - 动漫列表
- history - 观看历史记录
- favorites - 收藏的动漫列表
- activeTab - 当前激活的标签页
- loading - 数据加载状态
这种数据组织方式使得数据管理更加清晰,易于扩展和维护。
在设计跨端动漫应用时,需要特别关注以下几个方面:
- 组件 API 兼容性 - 确保使用的 React Native 组件在鸿蒙系统上有对应实现
- 样式系统差异 - 不同平台对样式的支持程度不同,需要确保样式在两端都能正常显示
- 本地存储差异 - 不同平台的本地存储机制可能存在差异
- 图标系统 - 确保图标在不同平台上都能正常显示
- 媒体播放 - 不同平台的媒体播放 API 可能存在差异
针对上述问题,该实现采用了以下适配策略:
- 使用 React Native 核心组件 - 优先使用 React Native 内置的组件,如 View、Text、TouchableOpacity、ScrollView、FlatList 等
- 统一的样式定义 - 使用 StyleSheet.create 定义样式,确保样式在不同平台上的一致性
- Base64 图标 - 使用 Base64 编码的图标,确保图标在不同平台上的一致性
- 本地存储抽象 - 使用 AsyncStorage 进行本地存储,它在不同平台上都有对应实现
- 平台无关的媒体处理 - 暂时使用简化的媒体播放逻辑,避免依赖平台特定的媒体 API
当前实现使用 FlatList 渲染动漫列表,这是一个好的做法,但可以进一步优化:
// 优化前
<FlatList
data={activeTab === 'history' ? history : favorites}
renderItem={({ item }) => (
<AnimeCard anime={item} />
)}
keyExtractor={item => item.id}
/>
// 优化后
<FlatList
data={activeTab === 'history' ? history : favorites}
renderItem={({ item }) => (
<AnimeCard anime={item} />
)}
keyExtractor={item => item.id}
initialNumToRender={5} // 初始渲染的项目数
maxToRenderPerBatch={10} // 每批渲染的最大项目数
windowSize={10} // 可见区域外渲染的项目数
removeClippedSubviews={true} // 移除不可见的子视图
updateCellsBatchingPeriod={100} // 单元格更新的批处理周期
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT, // 预计算的项目高度
offset: ITEM_HEIGHT * index,
index
})}
/>
2. 状态管理优化
当前实现使用多个 useState 钩子管理状态,可以考虑使用 useReducer 或状态管理库来管理复杂状态:
// 优化前
const [animes, setAnimes] = useState<Anime[]>(initialAnimes);
const [history, setHistory] = useState<Anime[]>([]);
const [favorites, setFavorites] = useState<Anime[]>([]);
const [activeTab, setActiveTab] = useState<'history' | 'favorites'>('history');
const [loading, setLoading] = useState(true);
// 优化后
type AppState = {
animes: Anime[];
history: Anime[];
favorites: Anime[];
activeTab: 'history' | 'favorites';
loading: boolean;
};
type AppAction =
| { type: 'SET_ANIMES'; payload: Anime[] }
| { type: 'SET_HISTORY'; payload: Anime[] }
| { type: 'SET_FAVORITES'; payload: Anime[] }
| { type: 'SET_ACTIVE_TAB'; payload: 'history' | 'favorites' }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'ADD_TO_HISTORY'; payload: Anime }
| { type: 'TOGGLE_FAVORITE'; payload: Anime }
| { type: 'CLEAR_HISTORY' }
| { type: 'CLEAR_FAVORITES' };
const initialState: AppState = {
animes: initialAnimes,
history: [],
favorites: [],
activeTab: 'history',
loading: true
};
const appReducer = (state: AppState, action: AppAction): AppState => {
switch (action.type) {
case 'SET_ANIMES':
return { ...state, animes: action.payload };
case 'SET_HISTORY':
return { ...state, history: action.payload };
case 'SET_FAVORITES':
return { ...state, favorites: action.payload };
case 'SET_ACTIVE_TAB':
return { ...state, activeTab: action.payload };
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'ADD_TO_HISTORY':
return {
...state,
history: [action.payload, ...state.history.filter(item => item.id !== action.payload.id)]
};
case 'TOGGLE_FAVORITE':
const updatedFavorites = state.favorites.some(fav => fav.id === action.payload.id)
? state.favorites.filter(fav => fav.id !== action.payload.id)
: [...state.favorites, action.payload];
const updatedHistory = state.history.map(item =>
item.id === action.payload.id ? { ...item, isFavorite: !item.isFavorite } : item
);
return {
...state,
favorites: updatedFavorites,
history: updatedHistory
};
case 'CLEAR_HISTORY':
return { ...state, history: [] };
case 'CLEAR_FAVORITES':
return {
...state,
favorites: [],
history: state.history.map(item => ({ ...item, isFavorite: false }))
};
default:
return state;
}
};
const [state, dispatch] = useReducer(appReducer, initialState);
3. 网络请求
当前实现使用静态数据,可以考虑集成网络请求获取实时动漫数据:
import axios from 'axios';
const fetchAnimes = async () => {
try {
setLoading(true);
const response = await axios.get('https://api.example.com/animes');
const animesData = response.data;
setAnimes(animesData);
} catch (error) {
console.error('获取动漫数据失败:', error);
// 使用本地存储的缓存数据
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchAnimes();
}, []);
4. 导航系统
可以集成 React Navigation 实现动漫详情页面的导航:
import { createStackNavigator } from '@react-navigation/stack';
const Stack = createStackNavigator();
const App = () => {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen
name="Home"
component={AnimeApp}
options={{ title: '动漫库' }}
/>
<Stack.Screen
name="AnimeDetail"
component={AnimeDetailScreen}
options={({ route }) => ({ title: route.params?.animeTitle || '动漫详情' })}
/>
</Stack.Navigator>
</NavigationContainer>
);
};
const AnimeDetailScreen = ({ route }: { route: any }) => {
const { animeId } = route.params;
// 获取动漫详情并渲染
return (
<View style={styles.detailContainer}>
{/* 动漫详情内容 */}
</View>
);
};
本文深入分析了一个功能完备的 React Native 动漫应用实现,从架构设计、状态管理、数据持久化到跨端兼容性都进行了详细探讨。该实现不仅功能完整,而且代码结构清晰,具有良好的可扩展性和可维护性。
理解这个功能完整的 React Native 动漫应用的技术实现细节,同时掌握其向鸿蒙(HarmonyOS)平台跨端适配的核心思路与实践方案。该应用具备本地数据持久化、收藏管理、观看历史记录、标签页切换等核心功能,是典型的内容类移动应用架构,涵盖了本地存储、列表渲染、状态管理等移动端开发的核心场景。
1. 数据模型
该动漫应用采用数据驱动+本地持久化的现代化 React Native 架构,核心数据模型和存储设计体现了内容类应用的典型特征:
// 动漫数据模型 - 覆盖内容类应用核心属性
type Anime = {
id: string; // 唯一标识
title: string; // 标题
episode: string; // 剧集信息
cover: string; // 封面图
duration: string; // 总时长
watchedTime: string; // 已观看时长
date: string; // 观看日期
isFavorite: boolean; // 收藏状态
};
// 本地存储键常量 - 统一管理存储键名
const STORAGE_KEYS = {
WATCH_HISTORY: '@watch_history',
FAVORITES: '@favorites',
};
数据模型设计亮点:
- 属性完整性:涵盖动漫播放类应用所需的核心属性,支持扩展;
- 状态标识清晰:
isFavorite布尔值明确标识收藏状态; - 存储键常量化:避免硬编码,便于维护和修改;
- 类型安全:TypeScript 类型定义确保数据结构一致性。
2. 本地存储
应用基于 AsyncStorage 实现完整的本地数据持久化方案,这是 React Native 中轻量级数据存储的标准实践:
(1)数据加载
// 组件挂载时加载本地数据
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
// 异步获取存储数据
const storedHistory = await AsyncStorage.getItem(STORAGE_KEYS.WATCH_HISTORY);
const storedFavorites = await AsyncStorage.getItem(STORAGE_KEYS.FAVORITES);
// 数据存在则解析使用,否则使用初始数据
if (storedHistory) {
setHistory(JSON.parse(storedHistory));
} else {
setHistory(initialAnimes);
}
if (storedFavorites) {
setFavorites(JSON.parse(storedFavorites));
}
} catch (error) {
console.error('加载数据失败:', error);
setHistory(initialAnimes); // 异常处理,保证应用可用性
} finally {
setLoading(false); // 无论成功失败,结束加载状态
}
};
(2)通用保存方法
const saveData = async (key: string, data: any) => {
try {
await AsyncStorage.setItem(key, JSON.stringify(data));
} catch (error) {
console.error('保存数据失败:', error);
}
};
(3)数据操作
// 添加到历史记录(去重 + 置顶)
const addToHistory = (anime: Anime) => {
// 过滤掉已存在的同ID项,新项置顶
const updatedHistory = [anime, ...history.filter(item => item.id !== anime.id)];
setHistory(updatedHistory);
saveData(STORAGE_KEYS.WATCH_HISTORY, updatedHistory);
};
// 切换收藏状态(双向同步)
const toggleFavorite = (anime: Anime) => {
// 更新收藏列表
const updatedFavorites = favorites.some(fav => fav.id === anime.id)
? favorites.filter(fav => fav.id !== anime.id) // 已收藏则移除
: [...favorites, anime]; // 未收藏则添加
setFavorites(updatedFavorites);
saveData(STORAGE_KEYS.FAVORITES, updatedFavorites);
// 同步更新历史记录中的收藏状态
const updatedHistory = history.map(item =>
item.id === anime.id ? { ...item, isFavorite: !item.isFavorite } : item
);
setHistory(updatedHistory);
saveData(STORAGE_KEYS.WATCH_HISTORY, updatedHistory);
};
本地存储设计要点:
- 异步处理:所有存储操作使用
async/await处理异步流程; - 异常处理:完善的 try/catch 保证应用稳定性;
- 数据一致性:收藏状态在历史和收藏列表间双向同步;
- 历史记录优化:自动去重并将最新观看内容置顶;
- 安全清空:清空操作前增加确认弹窗,防止误操作。
(1)标签页切换系统
实现了典型的移动端标签页切换交互,状态驱动视图更新:
<View style={styles.tabContainer}>
<TouchableOpacity
style={[styles.tab, activeTab === 'history' && styles.activeTab]}
onPress={() => setActiveTab('history')}
>
<Text style={[styles.tabText, activeTab === 'history' && styles.activeTabText]}>观看历史</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, activeTab === 'favorites' && styles.activeTab]}
onPress={() => setActiveTab('favorites')}
>
<Text style={[styles.tabText, activeTab === 'favorites' && styles.activeTabText]}>我的收藏</Text>
</TouchableOpacity>
</View>
(2)列表渲染
{activeTab === 'history' ? (
history.length > 0 ? (
<FlatList
data={history}
renderItem={renderAnimeItem}
keyExtractor={item => item.id}
showsVerticalScrollIndicator={false}
/>
) : (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}>暂无观看历史</Text>
<Text style={styles.emptyStateSubtext}>观看动漫后历史记录会显示在这里</Text>
</View>
)
) : (
// 收藏列表类似实现
)}
(3)动漫项渲染
const renderAnimeItem = ({ item }: { item: Anime }) => (
<View style={styles.animeItem}>
{/* 封面区域 */}
<TouchableOpacity style={styles.coverContainer} onPress={() => playAnime(item)}>
<View style={styles.coverPlaceholder}>
<Text style={styles.coverText}>📺</Text>
</View>
<Text style={styles.duration}>{item.duration}</Text>
</TouchableOpacity>
{/* 信息区域 */}
<View style={styles.infoContainer}>
{/* 标题和剧集 */}
<View style={styles.titleContainer}>
<Text style={styles.title} numberOfLines={1}>{item.title}</Text>
<Text style={styles.episode}>{item.episode}</Text>
</View>
{/* 元数据 */}
<View style={styles.metaContainer}>
<Text style={styles.date}>{item.date}</Text>
<Text style={styles.watchedTime}>已观看: {item.watchedTime}</Text>
</View>
{/* 操作按钮 */}
<View style={styles.actionsContainer}>
<TouchableOpacity
style={styles.actionButton}
onPress={() => playAnime(item)}
>
<Text style={styles.actionText}>▶️ 继续播放</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.favoriteButton}
onPress={() => toggleFavorite(item)}
>
<Text style={styles.favoriteText}>{item.isFavorite ? '❤️' : '🤍'}</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
4. 样式系统设计
应用采用 StyleSheet.create 构建完整的样式体系,遵循 React Native 最佳实践:
- 组件化样式:每个UI组件有独立的样式定义;
- 状态关联样式:通过样式数组实现激活状态的样式切换;
- 响应式尺寸:基于屏幕尺寸设计适配性布局;
- 阴影效果:使用
elevation和shadow属性实现跨平台阴影; - 圆角设计:统一的圆角半径,符合现代UI设计规范。
将该 React Native 动漫应用适配到鸿蒙平台,核心是将 React 的状态管理、本地存储、列表渲染等核心能力映射到鸿蒙 ArkTS + ArkUI 生态,以下是完整的适配方案。
1. 核心技术
| React Native 核心能力 | 鸿蒙 ArkTS 对应实现 | 适配关键说明 |
|---|---|---|
useState 状态管理 |
@State/@Link/@StorageProp |
状态声明语法替换 |
useEffect 生命周期 |
aboutToAppear |
组件挂载时加载数据 |
AsyncStorage 本地存储 |
Preferences 偏好设置 |
本地持久化方案替换 |
FlatList 列表渲染 |
List + LazyForEach |
高性能列表渲染 |
TouchableOpacity |
Button + stateEffect(false) |
可点击组件替换 |
StyleSheet.create |
@Styles/@Extend + 内联样式 |
样式体系重构 |
Alert.alert |
AlertDialog 组件 |
弹窗交互替换 |
| 条件渲染(三元运算符) | if/else 条件渲染 |
视图控制语法适配 |
SafeAreaView |
.safeArea(true) 修饰符 |
安全区域适配 |
2. 鸿蒙端
// index.ets - 鸿蒙端动漫应用完整实现
import preferences from '@ohos.data.preferences';
import router from '@ohos.router';
// 定义存储键常量(与RN端保持一致)
const STORAGE_KEYS = {
WATCH_HISTORY: '@watch_history',
FAVORITES: '@favorites',
};
// 动漫数据类型定义
type Anime = {
id: string;
title: string;
episode: string;
cover: string;
duration: string;
watchedTime: string;
date: string;
isFavorite: boolean;
};
// 模拟初始数据
const initialAnimes: Anime[] = [
{
id: '1',
title: '进击的巨人 最终季',
episode: '第75话:道路',
cover: '',
duration: '24:05',
watchedTime: '15:32',
date: '2023-05-15',
isFavorite: true
},
{
id: '2',
title: '鬼灭之刃 无限列车篇',
episode: '第19话:炼狱',
cover: '',
duration: '23:48',
watchedTime: '12:15',
date: '2023-05-14',
isFavorite: false
},
{
id: '3',
title: '我的英雄学院 第四季',
episode: '第104话:英雄',
cover: '',
duration: '24:12',
watchedTime: '20:45',
date: '2023-05-13',
isFavorite: true
},
{
id: '4',
title: '咒术回战',
episode: '第45话:宿傩',
cover: '',
duration: '23:55',
watchedTime: '8:20',
date: '2023-05-12',
isFavorite: false
},
{
id: '5',
title: '龙与虎',
episode: '第12话:圣诞',
cover: '',
duration: '24:00',
watchedTime: '18:30',
date: '2023-05-11',
isFavorite: true
},
{
id: '6',
title: '钢之炼金术师FA',
episode: '第51话:真理',
cover: '',
duration: '24:30',
watchedTime: '22:10',
date: '2023-05-10',
isFavorite: false
},
];
@Entry
@Component
struct AnimeApp {
// 核心状态管理(对应RN的useState)
@State animes: Anime[] = initialAnimes;
@State history: Anime[] = [];
@State favorites: Anime[] = [];
@State activeTab: 'history' | 'favorites' = 'history';
@State loading: boolean = true;
// 偏好设置实例
private pref: preferences.Preferences | null = null;
// 通用样式封装
@Styles
cardShadow() {
.shadow({ radius: 2, color: '#000', opacity: 0.1, offsetX: 0, offsetY: 1 });
}
// 组件生命周期(对应RN的useEffect)
async aboutToAppear() {
await this.loadData();
}
// 初始化偏好设置
private async getPreferences() {
if (!this.pref) {
this.pref = await preferences.getPreferences(getContext(), 'anime_app');
}
return this.pref;
}
// 加载本地数据(替换RN的AsyncStorage)
private async loadData() {
try {
const pref = await this.getPreferences();
// 从偏好设置读取数据
const storedHistory = pref.get(STORAGE_KEYS.WATCH_HISTORY, '');
const storedFavorites = pref.get(STORAGE_KEYS.FAVORITES, '');
if (storedHistory) {
this.history = JSON.parse(storedHistory);
} else {
this.history = initialAnimes;
}
if (storedFavorites) {
this.favorites = JSON.parse(storedFavorites);
}
} catch (error) {
console.error('加载数据失败:', error);
this.history = initialAnimes;
} finally {
this.loading = false;
}
}
// 保存数据到本地存储(替换RN的saveData)
private async saveData(key: string, data: any) {
try {
const pref = await this.getPreferences();
await pref.put(key, JSON.stringify(data));
await pref.flush(); // 立即刷盘
} catch (error) {
console.error('保存数据失败:', error);
}
}
// 添加到历史记录(逻辑与RN完全一致)
private addToHistory(anime: Anime) {
const updatedHistory = [anime, ...this.history.filter(item => item.id !== anime.id)];
this.history = updatedHistory;
this.saveData(STORAGE_KEYS.WATCH_HISTORY, updatedHistory);
}
// 切换收藏状态(逻辑与RN完全一致)
private toggleFavorite(anime: Anime) {
const updatedFavorites = this.favorites.some(fav => fav.id === anime.id)
? this.favorites.filter(fav => fav.id !== anime.id)
: [...this.favorites, anime];
this.favorites = updatedFavorites;
this.saveData(STORAGE_KEYS.FAVORITES, updatedFavorites);
// 同步更新历史记录中的收藏状态
this.history = this.history.map(item =>
item.id === anime.id ? { ...item, isFavorite: !item.isFavorite } : item
);
this.saveData(STORAGE_KEYS.WATCH_HISTORY, this.history);
}
// 清空历史记录(替换RN的Alert.alert)
private clearHistory() {
AlertDialog.show({
title: '清空历史记录',
message: '确定要清空所有观看历史吗?此操作无法撤销。',
cancel: {
value: '取消',
action: () => {}
},
confirm: {
value: '确定',
action: () => {
this.history = [];
this.saveData(STORAGE_KEYS.WATCH_HISTORY, []);
}
}
});
}
// 清空收藏(替换RN的Alert.alert)
private clearFavorites() {
AlertDialog.show({
title: '清空收藏',
message: '确定要清空所有收藏吗?此操作无法撤销。',
cancel: {
value: '取消',
action: () => {}
},
confirm: {
value: '确定',
action: () => {
this.favorites = [];
this.saveData(STORAGE_KEYS.FAVORITES, []);
// 更新历史记录中的收藏状态
this.history = this.history.map(item => ({ ...item, isFavorite: false }));
this.saveData(STORAGE_KEYS.WATCH_HISTORY, this.history);
}
}
});
}
// 播放动画(替换RN的Alert.alert)
private playAnime(anime: Anime) {
this.addToHistory(anime);
AlertDialog.show({
title: '播放',
message: `正在播放: ${anime.title} - ${anime.episode}`,
confirm: { value: '确定' }
});
}
// 渲染动漫项(Builder函数替换RN的renderAnimeItem)
@Builder
renderAnimeItem(item: Anime) {
Row({ space: 12 }) {
// 封面区域
Button()
.width(100)
.height(60)
.backgroundColor('#e2e8f0')
.borderRadius(8)
.stateEffect(true)
.onClick(() => this.playAnime(item))
.contentLayout(ContentLayout.Center)
.overlay(
Text(item.duration)
.fontSize(10)
.fontColor('#ffffff')
.backgroundColor('rgba(0,0,0,0.7)')
.padding({ left: 4, right: 4, top: 2, bottom: 2 })
.borderRadius(4)
.position({ bottom: 4, right: 4 })
) {
Text('📺')
.fontSize(24);
}
// 信息区域
Column({ space: 8 }) {
// 标题和剧集
Column({ space: 4 }) {
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b')
.maxLines(1);
Text(item.episode)
.fontSize(14)
.fontColor('#64748b');
}
// 元数据
Row({ space: 0 }) {
Text(item.date)
.fontSize(12)
.fontColor('#94a3b8');
Text(`已观看: ${item.watchedTime}`)
.fontSize(12)
.fontColor('#94a3b8')
.marginLeft('auto');
}
// 操作按钮
Row({ space: 0 }) {
Button('▶️ 继续播放')
.backgroundColor('#3b82f6')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.borderRadius(6)
.fontSize(12)
.fontColor('#ffffff')
.fontWeight(FontWeight.Medium)
.stateEffect(true)
.onClick(() => this.playAnime(item));
Button(item.isFavorite ? '❤️' : '🤍')
.backgroundColor(Color.Transparent)
.padding(6)
.fontSize(18)
.stateEffect(true)
.marginLeft('auto')
.onClick(() => this.toggleFavorite(item));
}
}
.flex(1);
}
.backgroundColor('#ffffff')
.borderRadius(12)
.padding(12)
.marginBottom(12)
.cardShadow();
}
// 空状态渲染
@Builder
renderEmptyState(title: string, subtext: string) {
Column({ space: 8 }) {
Text(title)
.fontSize(16)
.fontColor('#64748b');
Text(subtext)
.fontSize(14)
.fontColor('#94a3b8')
.textAlign(TextAlign.Center);
}
.width('100%')
.paddingVertical(60)
.alignItems(ItemAlign.Center);
}
build() {
Column({ space: 0 }) {
// 加载状态
if (this.loading) {
Column() {
Text('加载中...')
.fontSize(18)
.fontColor('#64748b');
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(ItemAlign.Center);
return;
}
// 头部组件
this.Header();
// 内容区域
Scroll() {
Column({ space: 16 }) {
// 标签页切换
this.TabContainer();
// 功能按钮
this.FunctionButtons();
// 内容列表
if (this.activeTab === 'history') {
if (this.history.length > 0) {
List() {
LazyForEach(new AnimeDataSource(this.history), (item: Anime) => {
ListItem() {
this.renderAnimeItem(item);
}
});
}
.width('100%')
.scrollBar(BarState.Off);
} else {
this.renderEmptyState('暂无观看历史', '观看动漫后历史记录会显示在这里');
}
} else {
if (this.favorites.length > 0) {
List() {
LazyForEach(new AnimeDataSource(this.favorites), (item: Anime) => {
ListItem() {
this.renderAnimeItem(item);
}
});
}
.width('100%')
.scrollBar(BarState.Off);
} else {
this.renderEmptyState('暂无收藏', '点击心形图标可收藏喜欢的动漫');
}
}
// 使用说明
this.InfoCard();
}
.padding(16)
.width('100%');
}
.flex(1)
.width('100%');
// 底部导航
this.BottomNav();
}
.width('100%')
.height('100%')
.backgroundColor('#f8fafc')
.safeArea(true);
}
// 头部组件
@Builder
Header() {
Row({ space: 0 }) {
Text('微动漫')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
Row({ space: 12 }) {
// 搜索按钮
Button()
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor('#f1f5f9')
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '搜索',
confirm: { value: '确定' }
});
}) {
Text('🔍')
.fontSize(18)
.fontColor('#64748b');
}
// 用户按钮
Button()
.width(36)
.height(36)
.borderRadius(18)
.backgroundColor('#f1f5f9')
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '用户中心',
confirm: { value: '确定' }
});
}) {
Text('👤')
.fontSize(18)
.fontColor('#64748b');
}
}
.marginLeft('auto');
}
.padding(20)
.backgroundColor('#ffffff')
.borderBottom({ width: 1, color: '#e2e8f0' })
.width('100%');
}
// 标签页容器
@Builder
TabContainer() {
Row({ space: 0 }) {
// 历史标签
Button('观看历史')
.flex(1)
.height(40)
.backgroundColor(this.activeTab === 'history' ? '#3b82f6' : '#ffffff')
.fontColor(this.activeTab === 'history' ? '#ffffff' : '#64748b')
.fontSize(14)
.fontWeight(this.activeTab === 'history' ? FontWeight.Medium : FontWeight.Normal)
.borderRadius(16)
.stateEffect(true)
.onClick(() => this.activeTab = 'history');
// 收藏标签
Button('我的收藏')
.flex(1)
.height(40)
.backgroundColor(this.activeTab === 'favorites' ? '#3b82f6' : '#ffffff')
.fontColor(this.activeTab === 'favorites' ? '#ffffff' : '#64748b')
.fontSize(14)
.fontWeight(this.activeTab === 'favorites' ? FontWeight.Medium : FontWeight.Normal)
.borderRadius(16)
.stateEffect(true)
.onClick(() => this.activeTab = 'favorites');
}
.backgroundColor('#ffffff')
.borderRadius(20)
.padding(4)
.width('100%');
}
// 功能按钮
@Builder
FunctionButtons() {
Button(this.activeTab === 'history' ? '🗑️ 清空历史' : '🗑️ 清空收藏')
.width('100%')
.backgroundColor('#f1f5f9')
.padding(12)
.borderRadius(8)
.fontSize(14)
.fontColor('#3b82f6')
.fontWeight(FontWeight.Medium)
.stateEffect(true)
.onClick(() => {
if (this.activeTab === 'history') {
this.clearHistory();
} else {
this.clearFavorites();
}
});
}
// 使用说明卡片
@Builder
InfoCard() {
Column({ space: 8 }) {
Text('使用说明')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#1e293b');
Text('• 自动记录观看历史')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
Text('• 点击心形图标收藏动漫')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
Text('• 可随时清除历史记录或收藏')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
Text('• 所有数据本地存储,保护隐私')
.fontSize(14)
.fontColor('#64748b')
.lineHeight(22);
}
.backgroundColor('#ffffff')
.borderRadius(12)
.padding(16)
.cardShadow()
.width('100%');
}
// 底部导航
@Builder
BottomNav() {
Row({ space: 0 }) {
// 首页
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '首页',
confirm: { value: '确定' }
});
}) {
Column({ space: 4 }) {
Text('🏠')
.fontSize(20)
.fontColor('#94a3b8');
Text('首页')
.fontSize(12)
.fontColor('#94a3b8');
}
};
// 分类
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => {
AlertDialog.show({
title: '分类',
confirm: { value: '确定' }
});
}) {
Column({ space: 4 }) {
Text('📚')
.fontSize(20)
.fontColor('#94a3b8');
Text('分类')
.fontSize(12)
.fontColor('#94a3b8');
}
};
// 历史
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => this.activeTab = 'history') {
Column({ space: 4 }) {
Text('🕒')
.fontSize(20)
.fontColor(this.activeTab === 'history' ? '#3b82f6' : '#94a3b8');
Text('历史')
.fontSize(12)
.fontColor(this.activeTab === 'history' ? '#3b82f6' : '#94a3b8');
}
};
// 收藏
Button()
.flex(1)
.backgroundColor(Color.Transparent)
.stateEffect(true)
.onClick(() => this.activeTab = 'favorites') {
Column({ space: 4 }) {
Text('❤️')
.fontSize(20)
.fontColor(this.activeTab === 'favorites' ? '#3b82f6' : '#94a3b8');
Text('收藏')
.fontSize(12)
.fontColor(this.activeTab === 'favorites' ? '#3b82f6' : '#94a3b8');
}
};
}
.backgroundColor('#ffffff')
.borderTop({ width: 1, color: '#e2e8f0' })
.paddingVertical(12)
.width('100%');
}
}
// 鸿蒙List数据源(LazyForEach必需)
class AnimeDataSource implements IDataSource {
private data: Anime[];
private listener: DataChangeListener | null = null;
constructor(data: Anime[]) {
this.data = data;
}
totalCount(): number {
return this.data.length;
}
getData(index: number): Anime {
return this.data[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
this.listener = listener;
}
unregisterDataChangeListener(): void {
this.listener = null;
}
}
(1)本地存储
React Native 的 AsyncStorage 替换为鸿蒙 Preferences 偏好设置:
// React Native
const loadData = async () => {
const storedHistory = await AsyncStorage.getItem(STORAGE_KEYS.WATCH_HISTORY);
if (storedHistory) setHistory(JSON.parse(storedHistory));
};
// 鸿蒙
private async loadData() {
const pref = await this.getPreferences();
const storedHistory = pref.get(STORAGE_KEYS.WATCH_HISTORY, '');
if (storedHistory) this.history = JSON.parse(storedHistory);
}
适配要点:
- 鸿蒙
Preferences需要先获取实例,RNAsyncStorage可直接使用; - 鸿蒙需要显式调用
flush()刷盘,RN 自动持久化; - 数据读写API语法不同,但数据格式和处理逻辑完全一致。
(2)列表渲染
React Native FlatList 替换为鸿蒙 List + LazyForEach:
// React Native
<FlatList
data={history}
renderItem={renderAnimeItem}
keyExtractor={item => item.id}
/>
// 鸿蒙
List() {
LazyForEach(new AnimeDataSource(this.history), (item: Anime) => {
ListItem() {
this.renderAnimeItem(item);
}
});
}
适配优势:
- 鸿蒙
LazyForEach实现真正的懒加载,性能更优; - 内置列表复用机制,内存占用更低;
- 支持更多列表特性(分组、索引、粘性头部等)。
React Native 组件体系到鸿蒙 ArkUI 的映射:
| React Native 组件 | 鸿蒙 ArkUI 组件 | 适配说明 |
|---|---|---|
TouchableOpacity |
Button + stateEffect |
可点击区域替换 |
View |
Column/Row/Stack |
布局容器替换 |
Text |
Text |
API基本一致 |
SafeAreaView |
.safeArea(true) |
修饰符方式实现 |
Alert.alert |
AlertDialog.show() |
弹窗API替换 |
StyleSheet.create |
@Styles/内联样式 |
样式定义方式替换 |
(4)状态管理
React Hooks 到鸿蒙状态装饰器的映射:
// React Native
const [history, setHistory] = useState<Anime[]>([]);
const [activeTab, setActiveTab] = useState<'history' | 'favorites'>('history');
// 鸿蒙
@State history: Anime[] = [];
@State activeTab: 'history' | 'favorites' = 'history';
核心差异:
- React 状态更新需要调用
setter函数,鸿蒙直接赋值即可; - React 状态更新是不可变的,鸿蒙直接修改状态对象;
- 两者的响应式原理不同,但开发体验和效果一致。
真实演示案例代码:
// app.tsx
import React, { useState, useEffect } from 'react';
import { SafeAreaView, View, Text, StyleSheet, TouchableOpacity, ScrollView, Dimensions, Alert, FlatList, AsyncStorage } from 'react-native';
// Base64 图标库
const ICONS_BASE64 = {
home: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
video: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
history: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
favorite: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
download: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
search: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
user: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
play: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==',
};
const { width, height } = Dimensions.get('window');
// 动漫类型
type Anime = {
id: string;
title: string;
episode: string;
cover: string;
duration: string;
watchedTime: string;
date: string;
isFavorite: boolean;
};
// 本地存储键
const STORAGE_KEYS = {
WATCH_HISTORY: '@watch_history',
FAVORITES: '@favorites',
};
// 模拟数据
const initialAnimes: Anime[] = [
{
id: '1',
title: '进击的巨人 最终季',
episode: '第75话:道路',
cover: '',
duration: '24:05',
watchedTime: '15:32',
date: '2023-05-15',
isFavorite: true
},
{
id: '2',
title: '鬼灭之刃 无限列车篇',
episode: '第19话:炼狱',
cover: '',
duration: '23:48',
watchedTime: '12:15',
date: '2023-05-14',
isFavorite: false
},
{
id: '3',
title: '我的英雄学院 第四季',
episode: '第104话:英雄',
cover: '',
duration: '24:12',
watchedTime: '20:45',
date: '2023-05-13',
isFavorite: true
},
{
id: '4',
title: '咒术回战',
episode: '第45话:宿傩',
cover: '',
duration: '23:55',
watchedTime: '8:20',
date: '2023-05-12',
isFavorite: false
},
{
id: '5',
title: '龙与虎',
episode: '第12话:圣诞',
cover: '',
duration: '24:00',
watchedTime: '18:30',
date: '2023-05-11',
isFavorite: true
},
{
id: '6',
title: '钢之炼金术师FA',
episode: '第51话:真理',
cover: '',
duration: '24:30',
watchedTime: '22:10',
date: '2023-05-10',
isFavorite: false
},
];
const AnimeApp: React.FC = () => {
const [animes, setAnimes] = useState<Anime[]>(initialAnimes);
const [history, setHistory] = useState<Anime[]>([]);
const [favorites, setFavorites] = useState<Anime[]>([]);
const [activeTab, setActiveTab] = useState<'history' | 'favorites'>('history');
const [loading, setLoading] = useState(true);
// 加载本地数据
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
const storedHistory = await AsyncStorage.getItem(STORAGE_KEYS.WATCH_HISTORY);
const storedFavorites = await AsyncStorage.getItem(STORAGE_KEYS.FAVORITES);
if (storedHistory) {
setHistory(JSON.parse(storedHistory));
} else {
setHistory(initialAnimes);
}
if (storedFavorites) {
setFavorites(JSON.parse(storedFavorites));
}
} catch (error) {
console.error('加载数据失败:', error);
setHistory(initialAnimes);
} finally {
setLoading(false);
}
};
// 保存数据到本地存储
const saveData = async (key: string, data: any) => {
try {
await AsyncStorage.setItem(key, JSON.stringify(data));
} catch (error) {
console.error('保存数据失败:', error);
}
};
// 添加到历史记录
const addToHistory = (anime: Anime) => {
const updatedHistory = [anime, ...history.filter(item => item.id !== anime.id)];
setHistory(updatedHistory);
saveData(STORAGE_KEYS.WATCH_HISTORY, updatedHistory);
};
// 切换收藏状态
const toggleFavorite = (anime: Anime) => {
const updatedFavorites = favorites.some(fav => fav.id === anime.id)
? favorites.filter(fav => fav.id !== anime.id)
: [...favorites, anime];
setFavorites(updatedFavorites);
saveData(STORAGE_KEYS.FAVORITES, updatedFavorites);
// 更新历史记录中的收藏状态
const updatedHistory = history.map(item =>
item.id === anime.id ? { ...item, isFavorite: !item.isFavorite } : item
);
setHistory(updatedHistory);
saveData(STORAGE_KEYS.WATCH_HISTORY, updatedHistory);
};
// 清空历史记录
const clearHistory = () => {
Alert.alert(
'清空历史记录',
'确定要清空所有观看历史吗?此操作无法撤销。',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
onPress: () => {
setHistory([]);
saveData(STORAGE_KEYS.WATCH_HISTORY, []);
}
}
]
);
};
// 清空收藏
const clearFavorites = () => {
Alert.alert(
'清空收藏',
'确定要清空所有收藏吗?此操作无法撤销。',
[
{ text: '取消', style: 'cancel' },
{
text: '确定',
onPress: () => {
setFavorites([]);
saveData(STORAGE_KEYS.FAVORITES, []);
// 更新历史记录中的收藏状态
const updatedHistory = history.map(item => ({ ...item, isFavorite: false }));
setHistory(updatedHistory);
saveData(STORAGE_KEYS.WATCH_HISTORY, updatedHistory);
}
}
]
);
};
// 播放动画
const playAnime = (anime: Anime) => {
addToHistory(anime);
Alert.alert('播放', `正在播放: ${anime.title} - ${anime.episode}`);
};
// 渲染动漫项
const renderAnimeItem = ({ item }: { item: Anime }) => (
<View style={styles.animeItem}>
<TouchableOpacity style={styles.coverContainer} onPress={() => playAnime(item)}>
<View style={styles.coverPlaceholder}>
<Text style={styles.coverText}>📺</Text>
</View>
<Text style={styles.duration}>{item.duration}</Text>
</TouchableOpacity>
<View style={styles.infoContainer}>
<View style={styles.titleContainer}>
<Text style={styles.title} numberOfLines={1}>{item.title}</Text>
<Text style={styles.episode}>{item.episode}</Text>
</div>
<View style={styles.metaContainer}>
<Text style={styles.date}>{item.date}</Text>
<Text style={styles.watchedTime}>已观看: {item.watchedTime}</Text>
</div>
<View style={styles.actionsContainer}>
<TouchableOpacity
style={styles.actionButton}
onPress={() => playAnime(item)}
>
<Text style={styles.actionText}>▶️ 继续播放</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.favoriteButton}
onPress={() => toggleFavorite(item)}
>
<Text style={styles.favoriteText}>{item.isFavorite ? '❤️' : '🤍'}</Text>
</TouchableOpacity>
</div>
</View>
</View>
);
if (loading) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.loadingContainer}>
<Text style={styles.loadingText}>加载中...</Text>
</View>
</SafeAreaView>
);
}
return (
<SafeAreaView style={styles.container}>
{/* 头部 */}
<View style={styles.header}>
<Text style={styles.title}>微动漫</Text>
<View style={styles.headerActions}>
<TouchableOpacity style={styles.searchButton} onPress={() => Alert.alert('搜索')}>
<Text style={styles.searchIcon}>🔍</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.userButton} onPress={() => Alert.alert('用户中心')}>
<Text style={styles.userIcon}>👤</Text>
</TouchableOpacity>
</div>
</View>
<ScrollView style={styles.content}>
{/* 标签页切换 */}
<View style={styles.tabContainer}>
<TouchableOpacity
style={[styles.tab, activeTab === 'history' && styles.activeTab]}
onPress={() => setActiveTab('history')}
>
<Text style={[styles.tabText, activeTab === 'history' && styles.activeTabText]}>观看历史</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, activeTab === 'favorites' && styles.activeTab]}
onPress={() => setActiveTab('favorites')}
>
<Text style={[styles.tabText, activeTab === 'favorites' && styles.activeTabText]}>我的收藏</Text>
</TouchableOpacity>
</View>
{/* 功能按钮 */}
<View style={styles.functionButtons}>
<TouchableOpacity
style={styles.functionButton}
onPress={activeTab === 'history' ? clearHistory : clearFavorites}
>
<Text style={styles.functionButtonText}>
{activeTab === 'history' ? '🗑️ 清空历史' : '🗑️ 清空收藏'}
</Text>
</TouchableOpacity>
</View>
{/* 内容列表 */}
{activeTab === 'history' ? (
history.length > 0 ? (
<FlatList
data={history}
renderItem={renderAnimeItem}
keyExtractor={item => item.id}
showsVerticalScrollIndicator={false}
/>
) : (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}>暂无观看历史</Text>
<Text style={styles.emptyStateSubtext}>观看动漫后历史记录会显示在这里</Text>
</View>
)
) : (
favorites.length > 0 ? (
<FlatList
data={favorites}
renderItem={renderAnimeItem}
keyExtractor={item => item.id}
showsVerticalScrollIndicator={false}
/>
) : (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}>暂无收藏</Text>
<Text style={styles.emptyStateSubtext}>点击心形图标可收藏喜欢的动漫</Text>
</View>
)
)}
{/* 使用说明 */}
<View style={styles.infoCard}>
<Text style={styles.infoTitle}>使用说明</Text>
<Text style={styles.infoText}>• 自动记录观看历史</Text>
<Text style={styles.infoText}>• 点击心形图标收藏动漫</Text>
<Text style={styles.infoText}>• 可随时清除历史记录或收藏</Text>
<Text style={styles.infoText}>• 所有数据本地存储,保护隐私</Text>
</View>
</ScrollView>
{/* 底部导航 */}
<View style={styles.bottomNav}>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('首页')}
>
<Text style={styles.navIcon}>🏠</Text>
<Text style={styles.navText}>首页</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => Alert.alert('分类')}
>
<Text style={styles.navIcon}>📚</Text>
<Text style={styles.navText}>分类</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => setActiveTab('history')}
>
<Text style={styles.navIcon}>🕒</Text>
<Text style={styles.navText}>历史</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.navItem}
onPress={() => setActiveTab('favorites')}
>
<Text style={styles.navIcon}>❤️</Text>
<Text style={styles.navText}>收藏</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f8fafc',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingText: {
fontSize: 18,
color: '#64748b',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
padding: 20,
backgroundColor: '#ffffff',
borderBottomWidth: 1,
borderBottomColor: '#e2e8f0',
},
title: {
fontSize: 20,
fontWeight: 'bold',
color: '#1e293b',
},
headerActions: {
flexDirection: 'row',
},
searchButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#f1f5f9',
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
},
searchIcon: {
fontSize: 18,
color: '#64748b',
},
userButton: {
width: 36,
height: 36,
borderRadius: 18,
backgroundColor: '#f1f5f9',
alignItems: 'center',
justifyContent: 'center',
},
userIcon: {
fontSize: 18,
color: '#64748b',
},
content: {
flex: 1,
padding: 16,
},
tabContainer: {
flexDirection: 'row',
backgroundColor: '#ffffff',
borderRadius: 20,
padding: 4,
marginBottom: 16,
},
tab: {
flex: 1,
paddingVertical: 10,
alignItems: 'center',
borderRadius: 16,
},
activeTab: {
backgroundColor: '#3b82f6',
},
tabText: {
fontSize: 14,
color: '#64748b',
},
activeTabText: {
color: '#ffffff',
fontWeight: '500',
},
functionButtons: {
marginBottom: 16,
},
functionButton: {
backgroundColor: '#f1f5f9',
padding: 12,
borderRadius: 8,
alignItems: 'center',
},
functionButtonText: {
fontSize: 14,
color: '#3b82f6',
fontWeight: '500',
},
animeItem: {
backgroundColor: '#ffffff',
borderRadius: 12,
flexDirection: 'row',
padding: 12,
marginBottom: 12,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
coverContainer: {
position: 'relative',
marginRight: 12,
},
coverPlaceholder: {
width: 100,
height: 60,
backgroundColor: '#e2e8f0',
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
},
coverText: {
fontSize: 24,
},
duration: {
position: 'absolute',
bottom: 4,
right: 4,
backgroundColor: 'rgba(0,0,0,0.7)',
color: '#ffffff',
fontSize: 10,
paddingHorizontal: 4,
paddingVertical: 2,
borderRadius: 4,
},
infoContainer: {
flex: 1,
justifyContent: 'space-between',
},
titleContainer: {
marginBottom: 8,
},
title: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 4,
},
episode: {
fontSize: 14,
color: '#64748b',
},
metaContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 8,
},
date: {
fontSize: 12,
color: '#94a3b8',
},
watchedTime: {
fontSize: 12,
color: '#94a3b8',
},
actionsContainer: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
actionButton: {
backgroundColor: '#3b82f6',
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 6,
},
actionText: {
color: '#ffffff',
fontSize: 12,
fontWeight: '500',
},
favoriteButton: {
padding: 6,
},
favoriteText: {
fontSize: 18,
},
emptyState: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 60,
},
emptyStateText: {
fontSize: 16,
color: '#64748b',
marginBottom: 8,
},
emptyStateSubtext: {
fontSize: 14,
color: '#94a3b8',
textAlign: 'center',
},
infoCard: {
backgroundColor: '#ffffff',
borderRadius: 12,
padding: 16,
marginTop: 16,
elevation: 1,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
infoTitle: {
fontSize: 16,
fontWeight: 'bold',
color: '#1e293b',
marginBottom: 12,
},
infoText: {
fontSize: 14,
color: '#64748b',
lineHeight: 22,
marginBottom: 8,
},
bottomNav: {
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#ffffff',
borderTopWidth: 1,
borderTopColor: '#e2e8f0',
paddingVertical: 12,
},
navItem: {
alignItems: 'center',
flex: 1,
},
navIcon: {
fontSize: 20,
color: '#94a3b8',
marginBottom: 4,
},
activeNavIcon: {
color: '#3b82f6',
},
navText: {
fontSize: 12,
color: '#94a3b8',
},
activeNavText: {
color: '#3b82f6',
fontWeight: '500',
},
});
export default AnimeApp;

打包
接下来通过打包命令npn run harmony将reactNative的代码打包成为bundle,这样可以进行在开源鸿蒙OpenHarmony中进行使用。

打包之后再将打包后的鸿蒙OpenHarmony文件拷贝到鸿蒙的DevEco-Studio工程目录去:

最后运行效果图如下显示:

本文探讨了一个基于React Native的动漫应用实现方案,重点分析了其架构设计、状态管理和跨平台兼容性策略。应用采用组件化架构,分离全局状态与UI渲染,使用TypeScript确保类型安全。通过AsyncStorage实现数据持久化,并采用同步机制保证内存与本地存储的一致性。针对跨平台开发,提出了组件兼容性、样式统一、本地存储抽象等适配策略。同时给出了性能优化建议,包括FlatList渲染优化和状态管理改进方案。该实现覆盖了动漫应用的核心功能需求,为移动端媒体类应用开发提供了参考架构。
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐




所有评论(0)