rn_for_openharmony狗狗之家app实战-品种详情实现
本文介绍了基于React Native开发的OpenHarmony狗狗品种详情页实现。该页面主要展示品种详细信息,包括基本信息、体型数据和性格特点等,并支持收藏功能。通过自定义Hook获取路由参数和全局状态,使用Header组件展示收藏按钮,图片区域采用自适应布局。关键点包括:数据兜底处理、二维数组配置信息项、标签式性格展示以及全局状态管理实现收藏功能。页面结构清晰,采用卡片式布局提升用户体验,为
案例开源地址:https://atomgit.com/nutpi/rn_openharmony_dogimg
详情页的重要性
列表页负责展示,详情页负责深入。用户在列表里看到感兴趣的品种,点进来想看更多信息。详情页要做的就是把品种的各种属性清晰地呈现出来。
这个页面还有一个重要功能——收藏。用户可以把喜欢的品种收藏起来,方便以后查看。这就涉及到全局状态管理,是这篇文章的重点之一。
页面需要哪些数据
先看看从列表页传过来的品种数据长什么样:
interface Breed {
id: number;
name: string;
bred_for?: string;
breed_group?: string;
life_span: string;
temperament?: string;
origin?: string;
weight: { metric: string };
height: { metric: string };
reference_image_id?: string;
image?: { url: string };
}
信息挺丰富的:名称、分组、用途、产地、寿命、性格、体型、图片。详情页要把这些都展示出来。
组件引入
import React from 'react';
import {View, Text, ScrollView, Image, TouchableOpacity, StyleSheet} from 'react-native';
import {Header, Card} from '../../components';
import {useNavigation, useRoute, useStore} from '../../hooks';
import type {Breed} from '../../types';
引入了三个自定义 Hook:
- useNavigation:页面跳转
- useRoute:获取路由参数
- useStore:获取全局状态和操作方法
数据准备
export function BreedDetailPage() {
const {navigate} = useNavigation();
const {params} = useRoute<{breed: Breed}>();
const {favoriteBreeds, toggleBreed} = useStore();
从三个 Hook 里分别取出需要的东西:
navigate用于跳转到图片页params.breed是列表页传过来的品种数据favoriteBreeds是收藏的品种 ID 数组toggleBreed是切换收藏状态的方法
继续处理数据:
const breed = params.breed;
const isFav = favoriteBreeds.includes(breed.id);
const img = breed.image?.url || `https://cdn2.thedogapi.com/images/${breed.reference_image_id}.jpg`;
breed简化后续使用isFav判断当前品种是否已收藏img处理图片 URL,有image.url就用,没有就用 ID 拼接
Header 的收藏按钮
<Header title={breed.name} right={
<TouchableOpacity onPress={() => toggleBreed(breed.id)}>
<Text style={s.fav}>{isFav ? '❤️' : '🤍'}</Text>
</TouchableOpacity>
} />
Header 组件支持 right 属性,可以传入右侧的自定义内容。
这里放了一个收藏按钮:
- 已收藏显示红心 ❤️
- 未收藏显示白心 🤍
- 点击调用
toggleBreed切换状态
用 emoji 做图标,简单直接。
顶部大图
<Image source={{uri: img}} style={s.img} />
品种图片放在最顶部,宽度撑满,高度固定 260:
img: {width: '100%', height: 260, backgroundColor: '#eee'},
backgroundColor 是图片加载前的占位色,避免白屏闪烁。
操作按钮区
<View style={s.actions}>
<TouchableOpacity style={s.action} onPress={() => navigate('BreedImages', {breed})}>
<Text style={s.actionIcon}>📷</Text>
<Text style={s.actionText}>更多图片</Text>
</TouchableOpacity>
</View>
图片下方是操作区,目前只有一个"更多图片"按钮。点击跳转到品种图集页面。
样式上做成横向排列,方便以后加更多按钮:
actions: {flexDirection: 'row', backgroundColor: '#fff', paddingVertical: 12, marginBottom: 8},
action: {flex: 1, alignItems: 'center'},
flex: 1 让按钮平分宽度,即使以后加多个按钮也能自动均分。
基本信息卡片
<Card>
<Text style={s.sectionTitle}>基本信息</Text>
{[
['品种名称', breed.name],
['品种分组', breed.breed_group || '未分类'],
['培育用途', breed.bred_for || '未知'],
['原产地', breed.origin || '未知'],
['寿命', breed.life_span],
].map(([label, value]) => (
<View key={label} style={s.row}>
<Text style={s.label}>{label}</Text>
<Text style={s.value}>{value}</Text>
</View>
))}
</Card>
用二维数组配置信息项,然后 map 渲染。这种写法比写五个重复的 View 简洁多了。
每项都做了兜底处理,比如 breed.breed_group || '未分类',没有数据时显示默认文案。
行样式:
row: {flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#f0f0f0'},
label: {fontSize: 14, color: '#666'},
value: {fontSize: 14, color: '#333', flex: 1, textAlign: 'right'},
- 左右两端对齐
- 底部有分隔线
- 值文字右对齐,
flex: 1让它占满剩余空间
体型信息卡片
<Card>
<Text style={s.sectionTitle}>体型信息</Text>
<View style={s.sizeRow}>
<View style={s.sizeItem}>
<Text style={s.sizeLabel}>身高</Text>
<Text style={s.sizeValue}>{breed.height.metric} cm</Text>
</View>
<View style={s.sizeItem}>
<Text style={s.sizeLabel}>体重</Text>
<Text style={s.sizeValue}>{breed.weight.metric} kg</Text>
</View>
</View>
</Card>
身高体重并排显示,用大字号突出数值:
sizeRow: {flexDirection: 'row'},
sizeItem: {flex: 1, alignItems: 'center', paddingVertical: 10},
sizeLabel: {fontSize: 13, color: '#999'},
sizeValue: {fontSize: 20, fontWeight: '600', color: '#D2691E', marginTop: 4},
数值用主题色 #D2691E,字号 20,加粗,是视觉焦点。
性格特点卡片
{breed.temperament && (
<Card>
<Text style={s.sectionTitle}>性格特点</Text>
<View style={s.tags}>
{breed.temperament.split(', ').map((t, i) => (
<View key={i} style={s.tag}>
<Text style={s.tagText}>{t}</Text>
</View>
))}
</View>
</Card>
)}
temperament 是逗号分隔的字符串,比如 “Friendly, Active, Outgoing”。
用 split(', ') 拆成数组,每个特点渲染成一个标签。
条件渲染 breed.temperament &&,没有性格数据的品种不显示这个卡片。
标签样式:
tags: {flexDirection: 'row', flexWrap: 'wrap'},
tag: {backgroundColor: '#FFF3E0', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, marginRight: 8, marginBottom: 8},
tagText: {fontSize: 13, color: '#E65100'},
flexWrap: 'wrap'让标签自动换行- 浅橙色背景 + 深橙色文字,和主题色呼应
borderRadius: 16做成胶囊形状
useStore 的实现
收藏功能依赖 useStore Hook,看看它是怎么实现的:
export function useStore(): AppState & {
toggleBreed: (id: number) => void;
toggleImage: (id: string) => void;
} {
const [state, setState] = useState(appStore.getState());
返回类型是 AppState 加上两个方法。用 useState 存储状态,初始值从 appStore 获取。
订阅状态变化:
useEffect(() => {
const unsubscribe = appStore.subscribe(setState);
return unsubscribe;
}, []);
组件挂载时订阅 store,状态变化时自动更新组件。
toggleBreed 方法
const toggleBreed = (id: number) => {
const list = state.favoriteBreeds;
appStore.setState({
favoriteBreeds: list.includes(id) ? list.filter(x => x !== id) : [...list, id],
});
};
这个方法实现收藏的切换:
- 如果
id已经在列表里,用filter移除 - 如果不在,用展开运算符添加
list.includes(id) 检查是否已收藏,返回布尔值。
三元表达式根据结果决定是移除还是添加。
toggleImage 方法
const toggleImage = (id: string) => {
const list = state.favoriteImages;
appStore.setState({
favoriteImages: list.includes(id) ? list.filter(x => x !== id) : [...list, id],
});
};
return {...state, toggleBreed, toggleImage};
}
和 toggleBreed 逻辑一样,只是操作的是图片收藏列表。
最后返回状态和两个方法,组件里解构使用。
全局状态的定义
export interface AppState {
favoriteBreeds: number[];
favoriteImages: string[];
darkMode: boolean;
}
export const appStore = createStore<AppState>({
favoriteBreeds: [],
favoriteImages: [],
darkMode: false,
});
AppState 定义了三个字段:
- favoriteBreeds:收藏的品种 ID 数组,number 类型
- favoriteImages:收藏的图片 ID 数组,string 类型
- darkMode:深色模式开关
初始状态都是空数组和 false。
为什么用 ID 而不是整个对象
收藏列表存的是 ID,不是完整的品种对象。为什么?
节省空间。一个品种对象有十几个字段,存 ID 只需要一个数字。
避免数据不一致。如果存对象,品种信息更新后,收藏里的还是旧数据。存 ID 的话,每次展示都从最新数据里取。
序列化方便。如果要把收藏存到本地(AsyncStorage),ID 数组序列化后很小。
收藏状态的持久化
当前的收藏只存在内存里,App 关闭就没了。要持久化可以这样做:
// 伪代码
import AsyncStorage from '@react-native-async-storage/async-storage';
// 读取
const saved = await AsyncStorage.getItem('favorites');
if (saved) {
appStore.setState(JSON.parse(saved));
}
// 保存
appStore.subscribe(state => {
AsyncStorage.setItem('favorites', JSON.stringify(state));
});
订阅状态变化,每次变化都存到本地。App 启动时读取恢复。
这个功能后续可以加,先保证核心流程。
样式的组织
详情页样式比较多,看看怎么组织的:
const s = StyleSheet.create({
container: {flex: 1, backgroundColor: '#f5f5f5'},
content: {flex: 1},
fav: {fontSize: 22},
按功能分组:容器、收藏按钮…
图片和操作区:
img: {width: '100%', height: 260, backgroundColor: '#eee'},
actions: {flexDirection: 'row', backgroundColor: '#fff', paddingVertical: 12, marginBottom: 8},
action: {flex: 1, alignItems: 'center'},
actionIcon: {fontSize: 24, marginBottom: 2},
actionText: {fontSize: 12, color: '#666'},
基本信息:
sectionTitle: {fontSize: 16, fontWeight: '600', color: '#333', marginBottom: 10},
row: {flexDirection: 'row', justifyContent: 'space-between', paddingVertical: 10, borderBottomWidth: 1, borderBottomColor: '#f0f0f0'},
label: {fontSize: 14, color: '#666'},
value: {fontSize: 14, color: '#333', flex: 1, textAlign: 'right'},
体型信息:
sizeRow: {flexDirection: 'row'},
sizeItem: {flex: 1, alignItems: 'center', paddingVertical: 10},
sizeLabel: {fontSize: 13, color: '#999'},
sizeValue: {fontSize: 20, fontWeight: '600', color: '#D2691E', marginTop: 4},
性格标签:
tags: {flexDirection: 'row', flexWrap: 'wrap'},
tag: {backgroundColor: '#FFF3E0', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, marginRight: 8, marginBottom: 8},
tagText: {fontSize: 13, color: '#E65100'},
});
样式命名清晰,一眼就知道是干什么的。
数据展示的兜底处理
详情页多处用到了兜底:
breed.breed_group || '未分类'
breed.bred_for || '未知'
breed.origin || '未知'
API 返回的数据不一定完整,有些品种缺少某些字段。不做兜底的话,页面会显示空白或 undefined,用户体验不好。
图片 URL 也有兜底:
breed.image?.url || `https://cdn2.thedogapi.com/images/${breed.reference_image_id}.jpg`
优先用 image.url,没有就用 ID 拼接 CDN 地址。
小结
品种详情页涉及的知识点:
- 路由参数接收:useRoute 获取列表页传来的数据
- 收藏功能:useStore 管理全局状态,toggleBreed 切换收藏
- 数据展示:二维数组配置 + map 渲染,减少重复代码
- 条件渲染:没有数据的卡片不显示
- 兜底处理:缺失字段显示默认文案
下一篇讲品种图集页面,会涉及到按品种筛选图片、图片列表的网格布局。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐




所有评论(0)