rn_for_openharmony狗狗之家app实战-领养列表实现
本文介绍了狗狗领养应用中的领养列表页面实现。首先定义了领养数据的结构AdoptDog,包含id、name、breed等字段。然后设计了5只不同品种、年龄和地区的模拟数据。页面组件采用Header+ScrollView结构,通过map渲染AdoptCard列表卡片。卡片组件采用左图右文布局,包含狗狗图片和基本信息,点击可跳转详情页。整体设计注重数据多样性和用户体验,体现了从云养狗到真实领养的过渡理念
案例开源地址:https://atomgit.com/nutpi/rn_openharmony_dogimg
从云养狗到真养狗
前面讲的功能都是"看"狗狗,浏览图片、了解品种、投票互动。但狗狗之家还有一个更有温度的功能:领养。
领养模块连接的是真实的需求。有些狗狗因为各种原因需要新家庭,有些人想养狗但不知道去哪找。领养中心就是这个桥梁。
这篇文章讲领养列表页面的实现,涉及到数据结构设计、卡片组件封装、列表渲染等内容。
领养数据的结构
先看 AdoptDog 类型的定义:
export interface AdoptDog {
id: string;
name: string;
breed: string;
age: string;
gender: '公' | '母';
size: string;
desc: string;
image: string;
location: string;
}
每个字段都有明确的用途:
id 是唯一标识,用于列表渲染的 key 和跳转详情时的参数。
name 是狗狗的名字,领养和买卖不同,每只狗都有自己的名字,这让它们更像"家人"而不是"商品"。
breed 是品种,字符串类型而不是关联 Breed 对象,因为领养的狗可能是混血或者品种不明确。
age 也是字符串,因为年龄的表达方式多样:“2岁”、“8个月”、“约3岁”。
gender 用联合类型 '公' | '母',只有这两个可能的值。TypeScript 会在编译时检查,传其他值会报错。
size 是体型,“小型”、“中型”、“大型”,帮助用户判断是否适合自己的居住环境。
desc 是描述,介绍狗狗的性格、健康状况等。
image 是照片 URL。
location 是所在地,领养通常需要线下见面,地理位置很重要。
模拟数据的设计
AdoptPage 里有一组模拟数据:
const DOGS: AdoptDog[] = [
{
id: '1',
name: '小黄',
breed: '中华田园犬',
age: '2岁',
gender: '公',
size: '中型',
desc: '性格温顺,亲人,已完成疫苗接种。',
image: 'https://cdn2.thedogapi.com/images/rkZRggqVX.jpg',
location: '北京市朝阳区'
},
第一只是中华田园犬,也就是俗称的"土狗"。在领养场景里,田园犬占很大比例,它们同样值得被爱。
描述里提到"已完成疫苗接种",这是领养者很关心的信息。
{
id: '2',
name: '豆豆',
breed: '泰迪',
age: '1岁',
gender: '母',
size: '小型',
desc: '活泼可爱,喜欢玩耍。',
image: 'https://cdn2.thedogapi.com/images/A09F4c1qP.jpg',
location: '上海市浦东新区'
},
泰迪是小型犬,适合公寓养。"活泼可爱"是性格描述,帮助用户判断是否和自己的生活方式匹配。
{
id: '3',
name: '大毛',
breed: '金毛',
age: '3岁',
gender: '公',
size: '大型',
desc: '温柔的大暖男,喜欢和小朋友玩。',
image: 'https://cdn2.thedogapi.com/images/SyRe4eAhm.jpg',
location: '广州市天河区'
},
金毛是大型犬,需要更大的活动空间。"喜欢和小朋友玩"对有孩子的家庭很有吸引力。
{
id: '4',
name: '妞妞',
breed: '柯基',
age: '8个月',
gender: '母',
size: '小型',
desc: '小短腿,超级萌。',
image: 'https://cdn2.thedogapi.com/images/Bymw-fYVX.jpg',
location: '深圳市南山区'
},
柯基虽然腿短,但其实是中型犬的体重。这里标成小型是为了数据多样性。
"8个月"说明还是幼犬,很多人偏好领养幼犬,觉得更容易培养感情。
{
id: '5',
name: '黑子',
breed: '拉布拉多',
age: '2岁',
gender: '公',
size: '大型',
desc: '聪明听话,已学会基本指令。',
image: 'https://cdn2.thedogapi.com/images/B1uW7l5VX.jpg',
location: '成都市武侯区'
},
];
拉布拉多是导盲犬的常用品种,智商高。"已学会基本指令"说明经过训练,对新手友好。
五只狗覆盖了不同的品种、体型、年龄、性别、城市,数据多样性有助于测试和展示。
页面组件的结构
import React from 'react';
import {View, ScrollView, StyleSheet} from 'react-native';
import {Header, AdoptCard} from '../../components';
import {useNavigation} from '../../hooks';
import type {AdoptDog} from '../../types';
导入分成四组:
React:基础依赖。
RN 组件:View 是容器,ScrollView 是滚动列表。
业务组件:Header 是顶部导航栏,AdoptCard 是领养卡片。
Hooks 和类型:useNavigation 处理页面跳转,AdoptDog 是类型定义。
export function AdoptPage() {
const {navigate} = useNavigation();
return (
<View style={s.container}>
<Header title="❤️ 领养中心" showBack={false} />
<ScrollView style={s.content}>
{DOGS.map(dog => (
<AdoptCard
key={dog.id}
dog={dog}
onPress={() => navigate('AdoptDetail', {dog})}
/>
))}
</ScrollView>
</View>
);
}
页面结构很简单:Header + 滚动列表。
showBack={false} 因为这是底部 Tab 页面,不需要返回按钮。
标题用了 ❤️ emoji,传达"爱心领养"的理念。
列表渲染的方式
{DOGS.map(dog => (
<AdoptCard
key={dog.id}
dog={dog}
onPress={() => navigate('AdoptDetail', {dog})}
/>
))}
用 map 遍历数组,为每只狗生成一个 AdoptCard。
key={dog.id} 是 React 列表渲染的必要属性,帮助 React 识别哪些元素变化了。
dog={dog} 把整个狗狗对象传给卡片组件。
onPress 点击时跳转到详情页,把 dog 对象作为参数传递。
为什么传整个对象而不是只传 id?因为详情页需要显示狗狗的所有信息,传对象可以直接用,不需要再查询。
页面样式
const s = StyleSheet.create({
container: {flex: 1, backgroundColor: '#f5f5f5'},
content: {flex: 1, paddingTop: 8},
});
container 是页面容器,灰色背景。
content 是滚动区域,paddingTop: 8 让第一张卡片和 Header 之间有点间距。
为什么不用 padding: 16?因为卡片自己有 marginHorizontal: 16,如果 content 也有水平 padding,间距就太大了。
AdoptCard 组件的设计
import React from 'react';
import {View, Text, Image, TouchableOpacity, StyleSheet} from 'react-native';
import type {AdoptDog} from '../types';
卡片组件用到了更多的 RN 组件:View、Text、Image、TouchableOpacity。
interface Props {
dog: AdoptDog;
onPress?: () => void;
}
Props 很简单,一个必传的 dog 对象,一个可选的点击回调。
onPress 设为可选是为了灵活性,有些场景可能只需要展示,不需要点击。
export function AdoptCard({dog, onPress}: Props) {
return (
<TouchableOpacity style={s.card} onPress={onPress} activeOpacity={0.85}>
<Image source={{uri: dog.image}} style={s.img} />
<View style={s.info}>
...
</View>
</TouchableOpacity>
);
}
卡片整体是个 TouchableOpacity,点击任何位置都能触发 onPress。
activeOpacity={0.85} 让点击时的透明度变化柔和一些。
布局是左图右文,Image 在左边,信息在右边。
卡片的图片部分
<Image source={{uri: dog.image}} style={s.img} />
图片样式:
img: {width: 110, height: 130, backgroundColor: '#f0f0f0'},
固定宽高,110 x 130,略微竖向的比例。
backgroundColor 在图片加载前显示,避免空白。
卡片的信息部分
<View style={s.info}>
<View style={s.row}>
<Text style={s.name}>{dog.name}</Text>
<Text style={s.gender}>{dog.gender === '公' ? '♂' : '♀'}</Text>
</View>
<Text style={s.breed}>{dog.breed}</Text>
<View style={s.tags}>
<View style={s.tag}><Text style={s.tagText}>{dog.age}</Text></View>
<View style={s.tag}><Text style={s.tagText}>{dog.size}</Text></View>
</View>
<Text style={s.loc}>📍 {dog.location}</Text>
</View>
信息区域分四行:
第一行:名字 + 性别符号。性别用 ♂ 和 ♀ 符号,比文字更直观。
第二行:品种。
第三行:标签,年龄和体型。用标签形式展示,视觉上更清晰。
第四行:位置,前面加个 📍 图标。
名字和性别的样式
row: {flexDirection: 'row', alignItems: 'center'},
name: {fontSize: 17, fontWeight: '600', color: '#333'},
gender: {fontSize: 16, marginLeft: 6, color: '#D2691E'},
名字和性别横向排列,用 flexDirection: 'row'。
名字是 17 号半粗体,是卡片里最醒目的文字。
性别符号用主题色 #D2691E,和名字区分开。
品种的样式
breed: {fontSize: 13, color: '#666', marginTop: 2},
品种用小一号的灰色字体,作为名字的补充信息。
marginTop: 2 和名字行拉开一点距离。
标签的实现
tags: {flexDirection: 'row', marginTop: 8},
tag: {
backgroundColor: '#f5f5f5',
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
marginRight: 6
},
tagText: {fontSize: 12, color: '#666'},
标签容器用 flexDirection: 'row' 让标签横向排列。
每个标签是灰色背景的小方块,圆角 4,内边距让文字不贴边。
marginRight: 6 让标签之间有间距。
位置的样式
loc: {fontSize: 12, color: '#999', marginTop: 8},
位置用最小的字号和最浅的颜色,是次要信息。
marginTop: 8 和标签行拉开距离。
卡片整体样式
card: {
flexDirection: 'row',
backgroundColor: '#fff',
borderRadius: 12,
marginHorizontal: 16,
marginVertical: 8,
overflow: 'hidden',
elevation: 2
},
flexDirection: ‘row’ 让图片和信息横向排列。
backgroundColor: ‘#fff’ 白色背景,和页面的灰色背景形成对比。
borderRadius: 12 圆角,看起来更柔和。
marginHorizontal: 16 左右边距,卡片不贴边。
marginVertical: 8 上下边距,卡片之间有间隔。
overflow: ‘hidden’ 配合圆角,裁剪超出的内容。
elevation: 2 是 Android 的阴影效果。iOS 需要用 shadowXxx 属性。
信息区域样式
info: {flex: 1, padding: 12},
flex: 1 让信息区域占据图片之外的所有空间。
padding: 12 内边距,文字不贴边。
跨平台阴影的处理
当前代码只用了 elevation,在 iOS 上没有阴影效果。完整的跨平台阴影:
card: {
...
// Android
elevation: 2,
// iOS
shadowColor: '#000',
shadowOffset: {width: 0, height: 1},
shadowOpacity: 0.1,
shadowRadius: 2,
},
iOS 的阴影需要四个属性配合:颜色、偏移、透明度、模糊半径。
这是 React Native 跨平台开发的常见问题,同一个效果在不同平台需要不同的实现。
数据来源的思考
当前用的是硬编码的模拟数据。真实场景下,数据应该从服务器获取:
const [dogs, setDogs] = useState<AdoptDog[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
api.getAdoptDogs().then(res => {
setDogs(res);
setLoading(false);
});
}, []);
加上 loading 状态,在数据加载完成前显示 Loading 组件。
下拉刷新的添加
const [refreshing, setRefreshing] = useState(false);
const onRefresh = async () => {
setRefreshing(true);
const res = await api.getAdoptDogs();
setDogs(res);
setRefreshing(false);
};
<ScrollView
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
>
领养信息会更新,有的狗被领养了,有的新狗加入。下拉刷新让用户能获取最新数据。
空状态处理
{dogs.length > 0 ? (
dogs.map(dog => <AdoptCard key={dog.id} dog={dog} ... />)
) : (
<Empty
icon="🐕"
title="暂无待领养狗狗"
desc="所有狗狗都找到家啦!"
/>
)}
如果列表为空,显示一个温馨的提示。"所有狗狗都找到家啦"比"暂无数据"更有人情味。
筛选功能的扩展
用户可能想按条件筛选:
const [filters, setFilters] = useState({
size: null, // 体型:小型/中型/大型
gender: null, // 性别:公/母
city: null, // 城市
});
const filteredDogs = dogs.filter(dog => {
if (filters.size && dog.size !== filters.size) return false;
if (filters.gender && dog.gender !== filters.gender) return false;
if (filters.city && !dog.location.includes(filters.city)) return false;
return true;
});
用 filter 方法筛选符合条件的狗狗。
每个条件都是可选的,为 null 时不筛选。
搜索功能
const [keyword, setKeyword] = useState('');
const searchedDogs = filteredDogs.filter(dog => {
if (!keyword) return true;
const k = keyword.toLowerCase();
return (
dog.name.toLowerCase().includes(k) ||
dog.breed.toLowerCase().includes(k) ||
dog.location.toLowerCase().includes(k)
);
});
支持按名字、品种、位置搜索。
toLowerCase() 让搜索不区分大小写。
排序功能
const [sortBy, setSortBy] = useState<'default' | 'age'>('default');
const sortedDogs = [...searchedDogs].sort((a, b) => {
if (sortBy === 'age') {
// 简单的年龄排序,实际需要更复杂的解析
return a.age.localeCompare(b.age);
}
return 0;
});
按年龄排序,让用户更容易找到幼犬或成年犬。
[...searchedDogs] 创建副本再排序,避免修改原数组。
收藏功能
用户可能想收藏感兴趣的狗狗,以后再看:
const {favoriteAdopts, toggleAdopt} = useStore();
<AdoptCard
dog={dog}
liked={favoriteAdopts.includes(dog.id)}
onLike={() => toggleAdopt(dog.id)}
onPress={() => navigate('AdoptDetail', {dog})}
/>
需要在 AdoptCard 里加上收藏按钮,类似 ImageCard 的实现。
小结
领养列表页面的实现要点:
- 数据结构设计:AdoptDog 类型包含领养场景需要的所有字段
- 卡片组件封装:AdoptCard 展示狗狗的关键信息
- 左图右文布局:flexDirection: ‘row’ 实现横向排列
- 标签展示:用小方块展示年龄、体型等信息
- 跨平台阴影:Android 用 elevation,iOS 用 shadow 系列属性
领养功能有社会价值,希望能帮助更多狗狗找到温暖的家。
下一篇讲领养详情页面,展示狗狗的完整信息和领养流程。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)