RN for OpenHarmony 实战 TodoList 项目:按状态筛选
本文介绍了TodoList应用中任务状态筛选功能的实现细节。通过定义FilterType联合类型管理三种筛选状态(全部、待办、已完成),使用useState跟踪当前筛选条件。筛选按钮组通过动态样式切换实现视觉反馈,选中按钮显示高亮背景色和白色文字。核心筛选逻辑采用条件判断组合,结合搜索、优先级等其他筛选条件,实现即时响应的任务列表更新。文章详细解析了类型安全、样式处理、条件渲染等关键实现技术点,展

案例开源地址:https://atomgit.com/lqjmac/rn_openharmony_todolist
任务的两种状态
任务只有两种状态:做完了,或者还没做完。
这听起来很简单,但当任务列表变长时,你会发现自己经常想"只看看还有哪些没做的"或者"看看今天完成了多少"。状态筛选就是为这个需求而生的。
在我们的 TodoList 应用中,状态筛选是三个按钮:全部、待办、已完成。点击不同的按钮,列表就会显示对应状态的任务。
筛选类型的定义
首先我们需要定义筛选的类型:
type FilterType = 'all' | 'active' | 'completed';
为什么用联合类型
TypeScript 的联合类型限制了变量只能是这三个值之一。如果你不小心写成 setFilter('done'),编译器会立刻报错。这种类型安全在开发时能帮你避免很多低级错误。
三种筛选状态
all表示显示所有任务,不做任何筛选active表示显示待办任务,也就是completed为false的任务completed表示显示已完成任务,也就是completed为true的任务
筛选状态的管理
筛选需要一个状态来记录当前选中的筛选条件:
const [filter, setFilter] = useState<FilterType>('all');
默认值的选择
默认值是 'all',也就是显示所有任务。这是最自然的选择,用户打开应用时应该能看到全部任务,而不是被筛选后的部分任务。
状态更新
当用户点击筛选按钮时,调用 setFilter 更新状态。状态变化会触发组件重新渲染,列表会根据新的筛选条件显示对应的任务。
筛选按钮组的实现
看看我们的筛选按钮是怎么实现的:
<View style={styles.filterContainer}>
<View style={styles.filterGroup}>
{(['all', 'active', 'completed'] as FilterType[]).map(f => (
<TouchableOpacity
key={f}
style={[styles.filterBtn, filter === f && {backgroundColor: theme.accent}]}
onPress={() => setFilter(f)}>
<Text style={[styles.filterBtnText, {color: filter === f ? '#fff' : theme.subText}]}>
{f === 'all' ? '全部' : f === 'active' ? '待办' : '已完成'}
</Text>
</TouchableOpacity>
))}
</View>
</View>
遍历生成按钮
我们用数组的 map 方法遍历三个筛选值,动态生成三个按钮。这比手写三个按钮更简洁,也更容易维护。如果以后要加一个新的筛选条件,只需要在数组里加一项就行。
as FilterType[] 的作用
as FilterType[] 是类型断言,告诉 TypeScript 这个数组的元素类型是 FilterType。没有这个断言,TypeScript 会把数组类型推断为 string[],后面使用 f 时就会报类型错误。
key 属性
key={f} 给每个按钮一个唯一的标识。React 用这个 key 来追踪列表项的变化,优化渲染性能。这里用筛选值本身作为 key,因为它们是唯一的。
按钮样式的动态切换
按钮有两种状态:选中和未选中。我们通过条件样式来区分:
style={[styles.filterBtn, filter === f && {backgroundColor: theme.accent}]}
样式数组
React Native 支持数组形式的样式,后面的样式会覆盖前面的。styles.filterBtn 是基础样式,filter === f && {backgroundColor: theme.accent} 是条件样式。
短路求值
filter === f && {backgroundColor: theme.accent} 使用了短路求值。如果 filter === f 为 false,整个表达式返回 false,不应用任何额外样式。如果为 true,返回后面的样式对象,应用背景色。
选中状态的视觉反馈
选中的按钮背景色变成主题强调色 theme.accent,未选中的按钮保持默认的半透明背景。这种视觉差异让用户一眼就能看出当前选中的是哪个筛选条件。
文字颜色的切换
按钮文字的颜色也要根据选中状态变化:
<Text style={[styles.filterBtnText, {color: filter === f ? '#fff' : theme.subText}]}>
三元运算符
filter === f ? '#fff' : theme.subText 是三元运算符。如果当前按钮被选中,文字颜色是白色,与紫色背景形成对比。如果未选中,文字颜色是次要文字颜色,比较低调。
对比度的重要性
选中状态用白色文字配紫色背景,对比度很高,容易阅读。未选中状态用灰色文字配半透明背景,视觉上比较弱化,不会抢夺注意力。这种设计让用户的注意力自然集中在选中的按钮上。
文字标签的转换
筛选值是英文的,但显示给用户的应该是中文:
{f === 'all' ? '全部' : f === 'active' ? '待办' : '已完成'}
嵌套三元运算符
这是一个嵌套的三元运算符。先判断是不是 'all',如果是就显示"全部"。如果不是,再判断是不是 'active',如果是就显示"待办"。如果都不是,那就是 'completed',显示"已完成"。
另一种写法
嵌套三元运算符不太好读,可以用对象映射来代替:
const filterLabels: Record<FilterType, string> = {
all: '全部',
active: '待办',
completed: '已完成',
};
// 使用
{filterLabels[f]}
这种写法更清晰,也更容易扩展。不过对于只有三个选项的情况,三元运算符也能接受。
筛选按钮的样式
filterContainer: {marginBottom: 8},
filterGroup: {flexDirection: 'row', gap: 8},
filterBtn: {paddingHorizontal: 12, paddingVertical: 6, borderRadius: 16, backgroundColor: 'rgba(255,255,255,0.1)'},
filterBtnText: {fontSize: 12},
filterContainer 容器样式
marginBottom: 8 是与下方内容的间距,让筛选按钮和任务列表之间有一点空隙。
filterGroup 按钮组样式
flexDirection: 'row'让三个按钮水平排列gap: 8是按钮之间的间距,这是React Native较新版本支持的属性,比用marginRight更方便
filterBtn 按钮样式
paddingHorizontal: 12和paddingVertical: 6设置内边距,让按钮有合适的大小borderRadius: 16是大圆角,让按钮看起来像药丸形状backgroundColor: 'rgba(255,255,255,0.1)'是半透明白色背景,在深色主题下有微弱的可见度
filterBtnText 文字样式
fontSize: 12 是小字体,因为筛选按钮不需要太大,够看清就行。
筛选逻辑的实现
筛选的核心是 filter 方法:
const filteredTasks = tasks.filter(task => {
const matchesSearch = task.title.toLowerCase().includes(searchText.toLowerCase());
const matchesFilter = filter === 'all' || (filter === 'active' && !task.completed) || (filter === 'completed' && task.completed);
const matchesPriority = priorityFilter === 'all' || task.priority === priorityFilter;
const matchesCategory = categoryFilter === 'all' || task.category === categoryFilter;
return matchesSearch && matchesFilter && matchesPriority && matchesCategory;
});
matchesFilter 的逻辑
状态筛选的逻辑在 matchesFilter 这一行:
const matchesFilter = filter === 'all' || (filter === 'active' && !task.completed) || (filter === 'completed' && task.completed);
这是一个或运算,满足任意一个条件就算匹配:
filter === 'all'如果筛选条件是"全部",所有任务都匹配filter === 'active' && !task.completed如果筛选条件是"待办",只有未完成的任务匹配filter === 'completed' && task.completed如果筛选条件是"已完成",只有已完成的任务匹配
与其他筛选条件的组合
状态筛选不是孤立的,它和搜索、优先级筛选、分类筛选一起工作。四个条件用 && 连接,只有全部满足的任务才会显示。
筛选的即时响应
当用户点击筛选按钮时,列表会立即更新。这是怎么实现的?
数据流
- 用户点击"待办"按钮
onPress回调执行setFilter('active')filter状态从'all'变成'active'- 状态变化触发组件重新渲染
filteredTasks重新计算,只包含未完成的任务FlatList接收到新的数据,显示筛选后的列表
整个过程是自动的,我们不需要手动触发列表更新。这就是 React 响应式编程的魅力。
性能考虑
每次状态变化都会重新计算 filteredTasks。对于几十条甚至几百条任务,这完全不是问题。JavaScript 的 filter 方法很快,用户感觉不到任何延迟。
筛选结果为空的处理
如果筛选后没有匹配的任务怎么办?
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text style={styles.emptyIcon}>📝</Text>
<Text style={[styles.emptyText, {color: theme.subText}]}>暂无任务</Text>
<Text style={[styles.emptySubText, {color: theme.subText}]}>点击下方按钮添加新任务</Text>
</View>
}
通用的空状态
FlatList 的 ListEmptyComponent 会在列表为空时显示。不管是因为没有任务,还是因为筛选后没有匹配的任务,都会显示这个组件。
更精确的提示
如果想根据筛选条件显示不同的提示,可以这样做:
ListEmptyComponent={
<View style={styles.emptyContainer}>
<Text style={styles.emptyIcon}>{filter === 'completed' ? '🎉' : '📝'}</Text>
<Text style={[styles.emptyText, {color: theme.subText}]}>
{filter === 'all' ? '暂无任务' : filter === 'active' ? '没有待办任务' : '没有已完成任务'}
</Text>
</View>
}
筛选"已完成"但没有结果时,显示庆祝图标和"没有已完成任务"的提示。筛选"待办"但没有结果时,显示"没有待办任务"。这种细节能让用户体验更好。
筛选按钮的交互细节
单选行为
三个按钮是互斥的,同一时间只能选中一个。这是通过状态管理实现的,filter 状态只能有一个值。
即时反馈
点击按钮后,按钮样式立即变化,列表立即更新。没有延迟,没有加载动画。这种即时反馈让用户感觉应用很"灵敏"。
触摸反馈
TouchableOpacity 在按下时会降低透明度,给用户视觉反馈。这个反馈很微妙,但能让用户确认"我点到了"。
筛选状态的持久化
目前筛选状态是临时的,刷新页面或重启应用后会重置为"全部"。如果想保持用户的筛选偏好,可以用 AsyncStorage 持久化:
import AsyncStorage from '@react-native-async-storage/async-storage';
// 读取保存的筛选状态
useEffect(() => {
AsyncStorage.getItem('filter').then(value => {
if (value) setFilter(value as FilterType);
});
}, []);
// 保存筛选状态
useEffect(() => {
AsyncStorage.setItem('filter', filter);
}, [filter]);
是否需要持久化
这取决于产品需求。有些用户可能希望每次打开应用都看到全部任务,有些用户可能希望保持上次的筛选。可以在设置里提供一个选项让用户自己选择。
筛选按钮的可访问性
为了让筛选按钮对所有用户友好,可以添加可访问性属性:
<TouchableOpacity
key={f}
style={[styles.filterBtn, filter === f && {backgroundColor: theme.accent}]}
onPress={() => setFilter(f)}
accessibilityRole="button"
accessibilityState={{selected: filter === f}}
accessibilityLabel={f === 'all' ? '显示全部任务' : f === 'active' ? '显示待办任务' : '显示已完成任务'}
>
accessibilityRole
accessibilityRole="button" 告诉屏幕阅读器这是一个按钮。
accessibilityState
accessibilityState={{selected: filter === f}} 告诉屏幕阅读器当前按钮是否被选中。
accessibilityLabel
accessibilityLabel 是屏幕阅读器会朗读的标签,比显示的文字更详细,帮助视障用户理解按钮的作用。
筛选与统计的联动
筛选会影响显示的任务数量,但不会影响统计数据:
const totalTasks = tasks.length;
const completedTasks = tasks.filter(t => t.completed).length;
统计基于原始数据
统计数据是基于原始的 tasks 数组计算的,不受筛选影响。这样用户在筛选"待办"任务时,仍然能看到总任务数和已完成数。
为什么这样设计
如果统计也跟着筛选变化,用户筛选"待办"后会看到"已完成: 0",这会让人困惑。保持统计数据不变,用户能更好地了解整体情况。
小结
状态筛选是一个简单但实用的功能。通过三个按钮让用户快速切换查看全部任务、待办任务或已完成任务。
实现要点:
- 用
TypeScript联合类型定义筛选值,保证类型安全 - 用
useState管理筛选状态,默认显示全部 - 用
map遍历生成按钮,代码更简洁 - 用条件样式区分选中和未选中状态
- 用
filter方法筛选任务列表,与其他筛选条件组合使用
好的筛选应该是"一目了然、一点即达"。用户看一眼就知道当前选中的是什么,点一下就能切换到想要的筛选条件。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)