rn_for_openharmony狗狗之家app实战-品种分组实现
案例开源地址:https://atomgit.com/nutpi/rn_openharmony_dogimg
换个角度看品种
品种列表页是按字母顺序展示所有品种,但用户可能想按类型来浏览。比如想看工作犬有哪些、运动犬有哪些。品种分组页就是为这个需求设计的。
这个页面的核心是数据分组。API 返回的是扁平的品种数组,我们需要按 breed_group 字段把它们分成若干组。这是个很常见的数据处理场景,值得好好聊聊。
API 返回的数据结构
先回顾一下品种数据的结构:
export 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 };
}
关键字段是 breed_group,表示品种所属的分组,比如:
- Working:工作犬,如哈士奇、萨摩耶
- Sporting:运动犬,如金毛、拉布拉多
- Herding:牧羊犬,如边牧、德牧
- Toy:玩具犬,如吉娃娃、博美
注意这个字段是可选的(带 ?),有些品种没有分组信息。
组件引入
import React, {useState, useEffect} from 'react';
import {View, Text, ScrollView, TouchableOpacity, StyleSheet} from 'react-native';
import {Header, Loading} from '../../components';
import {api} from '../../api';
import type {Breed} from '../../types';
这个页面比较简单,没有用到 useNavigation,因为当前只展示分组列表,点击暂时没有跳转。
状态定义
export function BreedGroupsPage() {
const [loading, setLoading] = useState(true);
const [groups, setGroups] = useState<Record<string, Breed[]>>({});
两个状态:
- loading:加载状态
- groups:分组后的数据
groups 的类型是 Record<string, Breed[]>,这是 TypeScript 的工具类型,表示一个对象,key 是字符串,value 是品种数组。
比如:
{
"Working": [哈士奇, 萨摩耶, ...],
"Sporting": [金毛, 拉布拉多, ...],
"其他": [没有分组的品种, ...]
}
数据加载和分组
useEffect(() => {
api.getBreeds().then(res => {
const g: Record<string, Breed[]> = {};
res.forEach(b => {
const key = b.breed_group || '其他';
if (!g[key]) g[key] = [];
g[key].push(b);
});
setGroups(g);
setLoading(false);
});
}, []);
这段代码是核心,逐步分析:
调用 API:api.getBreeds() 获取所有品种,返回 Promise。
初始化空对象:const g: Record<string, Breed[]> = {} 用来存储分组结果。
遍历品种数组:res.forEach(b => {...}) 对每个品种进行处理。
确定分组 key:const key = b.breed_group || '其他',没有分组的归入"其他"。
初始化数组:if (!g[key]) g[key] = [],如果这个分组还不存在,先创建空数组。
添加品种:g[key].push(b) 把品种加入对应分组。
更新状态:遍历完成后,setGroups(g) 更新状态,setLoading(false) 结束加载。
分组算法的时间复杂度
上面的分组算法是 O(n),只需要遍历一次数组。
每个品种只处理一次,对象的属性访问和数组 push 都是 O(1) 操作。
如果用 filter 来分组,比如:
// 不推荐的写法
const working = breeds.filter(b => b.breed_group === 'Working');
const sporting = breeds.filter(b => b.breed_group === 'Sporting');
// ...
每个分组都要遍历一次数组,时间复杂度是 O(n * m),m 是分组数量。数据量大时差别明显。
加载状态的处理
if (loading) return (
<View style={s.container}>
<Header title="品种分组" />
<Loading full />
</View>
);
这里用了提前返回的写法。如果还在加载,直接返回 Loading 界面,不执行后面的渲染逻辑。
这种写法比在 JSX 里写三元表达式更清晰,特别是当加载状态的 UI 比较复杂时。
分组列表的渲染
return (
<View style={s.container}>
<Header title="品种分组" />
<ScrollView style={s.content}>
{Object.entries(groups).map(([name, list]) => (
Object.entries(groups) 把对象转成数组,每个元素是 [key, value] 的形式。
比如 {Working: [...], Sporting: [...]} 会变成:
[
['Working', [...]],
['Sporting', [...]]
]
然后用 map 遍历,解构出 name(分组名)和 list(品种数组)。
分组项的渲染
<TouchableOpacity key={name} style={s.item}>
<View style={s.info}>
<Text style={s.name}>{name}</Text>
<Text style={s.count}>{list.length} 个品种</Text>
</View>
<Text style={s.arrow}>›</Text>
</TouchableOpacity>
))}
</ScrollView>
</View>
);
}
每个分组项显示:
- 分组名称:如 “Working”、“Sporting”
- 品种数量:
list.length获取该分组的品种数 - 箭头:提示可以点击(虽然当前没有实现跳转)
key={name} 用分组名作为 key,因为分组名是唯一的。
样式设计
const s = StyleSheet.create({
container: {flex: 1, backgroundColor: '#f5f5f5'},
content: {flex: 1, backgroundColor: '#fff'},
容器背景浅灰,内容区背景白色,形成层次感。
列表项样式:
item: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#f0f0f0'
},
- 横向排列,垂直居中
- 内边距 16
- 底部有分隔线
信息区和箭头:
info: {flex: 1},
name: {fontSize: 16, fontWeight: '500', color: '#333'},
count: {fontSize: 13, color: '#999', marginTop: 2},
arrow: {fontSize: 20, color: '#ccc'},
});
info用flex: 1占满剩余空间- 分组名字号 16,加粗
- 数量字号 13,灰色,作为次要信息
- 箭头浅灰色,不抢眼
Object.entries vs Object.keys
遍历对象有几种方式:
Object.keys:返回 key 数组
Object.keys(groups).map(name => {
const list = groups[name];
// ...
});
Object.entries:返回 [key, value] 数组
Object.entries(groups).map(([name, list]) => {
// ...
});
Object.values:返回 value 数组
Object.values(groups).map(list => {
// 拿不到 key
});
这个场景需要同时用到 key 和 value,所以 Object.entries 最合适。
分组顺序的问题
当前分组的顺序是不确定的。JavaScript 对象的属性顺序在大多数情况下是按添加顺序,但不保证。
如果想要固定顺序,可以这样处理:
const ORDER = ['Working', 'Sporting', 'Herding', 'Hound', 'Toy', 'Terrier', 'Non-Sporting', '其他'];
const sortedEntries = Object.entries(groups).sort(([a], [b]) => {
return ORDER.indexOf(a) - ORDER.indexOf(b);
});
先定义顺序数组,然后按这个顺序排序。
当前没有实现,因为对用户体验影响不大。
可以扩展的功能
点击跳转:点击分组后跳转到该分组的品种列表。
onPress={() => navigate('GroupBreeds', {group: name, breeds: list})}
展开收起:点击分组展开显示品种列表,再点收起。类似手风琴效果。
搜索过滤:在顶部加搜索框,可以搜索分组名或品种名。
分组图标:每个分组加一个代表性的图标或图片。
这些功能后续可以加,当前先实现基础版本。
数据分组的通用方法
分组是个常见需求,可以封装成通用函数:
function groupBy<T>(arr: T[], keyFn: (item: T) => string): Record<string, T[]> {
const result: Record<string, T[]> = {};
arr.forEach(item => {
const key = keyFn(item);
if (!result[key]) result[key] = [];
result[key].push(item);
});
return result;
}
// 使用
const groups = groupBy(breeds, b => b.breed_group || '其他');
keyFn 是一个函数,接收数组元素,返回分组的 key。这样就可以按任意字段分组了。
当前页面代码量小,没必要抽取。但如果多处用到分组,封装成工具函数是个好选择。
Promise 的 then 写法
数据加载用的是 .then() 而不是 async/await:
api.getBreeds().then(res => {
// 处理数据
});
两种写法都可以,效果一样。.then() 在简单场景下代码更紧凑。
如果有多个异步操作需要顺序执行,async/await 可读性更好:
const loadData = async () => {
const breeds = await api.getBreeds();
const images = await api.getImages();
// ...
};
类型安全的好处
groups 的类型是 Record<string, Breed[]>,这带来几个好处:
编辑器提示:访问 groups.xxx 时,编辑器知道值是 Breed[] 类型。
类型检查:如果不小心写成 groups[name].push('string'),编译时就会报错。
代码可读:一眼就知道这个变量存的是什么结构的数据。
TypeScript 的类型系统在大型项目中特别有价值,能减少很多运行时错误。
小结
品种分组页涉及的知识点:
- 数据分组算法:O(n) 时间复杂度的分组实现
- Record 类型:TypeScript 的工具类型,表示对象结构
- Object.entries:把对象转成可遍历的数组
- 提前返回:加载状态的简洁处理方式
- 分组顺序:JavaScript 对象属性顺序的特点
数据处理是前端开发的基本功,掌握好分组、排序、过滤这些操作,很多需求都能轻松应对。
下一篇讲品种对比页面,会涉及到多选和数据对比展示。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐




所有评论(0)