Flutter for OpenHarmony衣橱管家App实战:收藏功能实现
本文介绍了衣橱管家App收藏功能的实现方案。采用Provider状态管理,通过GridView展示收藏衣物卡片,包含颜色展示区、衣物名称和分类信息。设计考虑了空状态提示、两列网格布局和交互细节,右上角爱心图标可取消收藏,点击卡片可查看详情。功能实现简洁高效,为用户提供良好的收藏管理体验。

收藏功能是很多App的标配,它让用户能够快速找到自己喜欢的内容。在衣橱管家App中,用户可以收藏自己最爱的衣物,方便日后查看和搭配。今天我们来实现这个实用的功能。
收藏功能的设计思路
收藏功能看起来简单,但要做好需要考虑几个方面:收藏状态的存储、收藏列表的展示、收藏操作的交互反馈。我们采用Provider进行状态管理,确保收藏状态在整个App中保持同步。
用户可以在衣物详情页点击爱心图标收藏,也可以在收藏列表页取消收藏。这种双向操作的设计符合用户的使用习惯。
页面整体结构
收藏页面使用GridView展示收藏的衣物,每个衣物显示为一个卡片,包含图片、名称和分类信息。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../providers/wardrobe_provider.dart';
import '../../models/clothing_item.dart';
import 'clothing_detail_screen.dart';
class FavoritesScreen extends StatelessWidget {
const FavoritesScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('我的收藏')),
body: Consumer<WardrobeProvider>(
builder: (context, provider, child) {
final favorites = provider.getFavorites();
// 后续处理
},
),
);
}
}
导入了Provider和Model,以及衣物详情页用于点击跳转。Consumer监听WardrobeProvider的变化,当收藏状态改变时自动刷新界面。
getFavorites()方法从Provider中获取所有收藏的衣物列表,这个方法在Provider中已经实现好了。
空状态处理
当用户还没有收藏任何衣物时,需要显示一个友好的空状态提示。
if (favorites.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite_border, size: 64.sp, color: Colors.grey),
SizedBox(height: 16.h),
Text('暂无收藏衣物', style: TextStyle(fontSize: 16.sp, color: Colors.grey)),
SizedBox(height: 8.h),
Text('点击衣物详情页的❤️添加收藏', style: TextStyle(fontSize: 14.sp, color: Colors.grey)),
],
),
);
}
空状态使用空心爱心图标,与收藏主题呼应。主文案告诉用户当前状态,副文案引导用户如何添加收藏。
两行文字的字号有区分,主文案16sp,副文案14sp,形成视觉层次。灰色调让空状态看起来柔和不刺眼。
网格布局展示
收藏的衣物使用两列网格布局展示,每个卡片显示衣物的颜色、名称和分类。
return GridView.builder(
padding: EdgeInsets.all(16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
childAspectRatio: 0.75,
),
itemCount: favorites.length,
itemBuilder: (context, index) {
final item = favorites[index];
return _buildFavoriteCard(context, item, provider);
},
);
crossAxisCount设为2,一行显示两个卡片。crossAxisSpacing和mainAxisSpacing控制卡片之间的间距。
childAspectRatio设为0.75,意味着卡片高度是宽度的1.33倍,这个比例适合展示衣物卡片。GridView.builder按需构建,性能更好。
收藏卡片设计
每个收藏卡片包含衣物颜色展示区、名称和分类信息,右上角有取消收藏的按钮。
Widget _buildFavoriteCard(BuildContext context, ClothingItem item, WardrobeProvider provider) {
return GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => ClothingDetailScreen(item: item)),
),
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: ClothingItem.getColorFromName(item.color).withOpacity(0.3),
borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
),
child: Stack(
children: [
Center(
child: Icon(Icons.checkroom, size: 48.sp, color: ClothingItem.getColorFromName(item.color)),
),
Positioned(
top: 8.h,
right: 8.w,
child: GestureDetector(
onTap: () => provider.toggleFavorite(item.id),
child: const Icon(Icons.favorite, color: Colors.red),
),
),
],
),
),
),
Padding(
padding: EdgeInsets.all(8.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.name, style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis),
SizedBox(height: 4.h),
Text('${item.category} · ${item.color}', style: TextStyle(fontSize: 12.sp, color: Colors.grey)),
],
),
),
],
),
),
);
}
GestureDetector包裹整个卡片,点击跳转到衣物详情页。卡片分为上下两部分:上面是颜色展示区,下面是文字信息区。
Stack用于在颜色展示区叠加爱心图标,Positioned定位到右上角。点击爱心调用toggleFavorite方法切换收藏状态。
颜色展示区设计
颜色展示区使用衣物的颜色作为背景,中间放置衣架图标,形成统一的视觉风格。
Expanded(
child: Container(
decoration: BoxDecoration(
color: ClothingItem.getColorFromName(item.color).withOpacity(0.3),
borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
),
child: Stack(
children: [
Center(
child: Icon(Icons.checkroom, size: 48.sp, color: ClothingItem.getColorFromName(item.color)),
),
// 爱心图标
],
),
),
),
getColorFromName是ClothingItem模型中的静态方法,根据颜色名称返回对应的Color对象。背景使用30%透明度,图标使用原色,形成层次感。
Expanded让颜色展示区占据卡片的剩余空间,与下面固定高度的文字区域配合,实现自适应布局。
取消收藏交互
点击爱心图标可以取消收藏,为了防止误操作,可以添加确认弹窗。
void _confirmRemoveFavorite(BuildContext context, ClothingItem item, WardrobeProvider provider) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('取消收藏'),
content: Text('确定要取消收藏"${item.name}"吗?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('取消'),
),
TextButton(
onPressed: () {
provider.toggleFavorite(item.id);
Navigator.pop(context);
},
child: const Text('确定'),
),
],
),
);
}
弹窗内容包含衣物名称,让用户确认要取消收藏的是哪件衣物。两个按钮分别是取消和确定,符合用户习惯。
点击确定后先调用toggleFavorite取消收藏,再关闭弹窗。由于使用了Consumer,列表会自动刷新移除这件衣物。
批量管理功能
当收藏的衣物很多时,可以提供批量取消收藏的功能。
class _FavoritesScreenState extends State<FavoritesScreen> {
bool _isSelecting = false;
Set<String> _selectedIds = {};
Widget _buildAppBar(WardrobeProvider provider) {
if (_isSelecting) {
return AppBar(
leading: IconButton(
icon: const Icon(Icons.close),
onPressed: () => setState(() {
_isSelecting = false;
_selectedIds.clear();
}),
),
title: Text('已选择${_selectedIds.length}件'),
actions: [
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _batchRemove(provider),
),
],
);
}
return AppBar(
title: const Text('我的收藏'),
actions: [
IconButton(
icon: const Icon(Icons.checklist),
onPressed: () => setState(() => _isSelecting = true),
),
],
);
}
void _batchRemove(WardrobeProvider provider) {
for (var id in _selectedIds) {
provider.toggleFavorite(id);
}
setState(() {
_isSelecting = false;
_selectedIds.clear();
});
}
}
长按或点击右上角按钮进入选择模式,AppBar显示已选择数量和删除按钮。选择模式下点击卡片切换选中状态。
_selectedIds使用Set存储选中的衣物ID,Set自动去重且查找效率高。批量删除时遍历Set调用toggleFavorite。
选择模式下的卡片
选择模式下,卡片需要显示选中状态的指示器。
Widget _buildSelectableCard(ClothingItem item) {
final isSelected = _selectedIds.contains(item.id);
return GestureDetector(
onTap: () {
setState(() {
if (isSelected) {
_selectedIds.remove(item.id);
} else {
_selectedIds.add(item.id);
}
});
},
child: Stack(
children: [
_buildFavoriteCard(context, item, provider),
if (isSelected)
Positioned(
top: 8.h,
left: 8.w,
child: Container(
width: 24.w,
height: 24.w,
decoration: const BoxDecoration(
color: Color(0xFFE91E63),
shape: BoxShape.circle,
),
child: Icon(Icons.check, color: Colors.white, size: 16.sp),
),
),
],
),
);
}
选中状态通过左上角的粉色圆形对勾指示,与右上角的红色爱心形成区分。
点击卡片切换选中状态,使用Set的add和remove方法操作。setState触发界面刷新,显示或隐藏选中指示器。
排序功能
用户可能想按不同方式排序收藏的衣物,比如按收藏时间、按名称、按分类等。
enum SortType { time, name, category }
class _FavoritesScreenState extends State<FavoritesScreen> {
SortType _sortType = SortType.time;
List<ClothingItem> _sortFavorites(List<ClothingItem> favorites) {
switch (_sortType) {
case SortType.time:
// 假设有收藏时间字段
return favorites;
case SortType.name:
return List.from(favorites)..sort((a, b) => a.name.compareTo(b.name));
case SortType.category:
return List.from(favorites)..sort((a, b) => a.category.compareTo(b.category));
}
}
Widget _buildSortButton() {
return PopupMenuButton<SortType>(
icon: const Icon(Icons.sort),
onSelected: (type) => setState(() => _sortType = type),
itemBuilder: (context) => [
PopupMenuItem(value: SortType.time, child: Text('按收藏时间')),
PopupMenuItem(value: SortType.name, child: Text('按名称')),
PopupMenuItem(value: SortType.category, child: Text('按分类')),
],
);
}
}
使用枚举定义排序类型,代码更清晰。PopupMenuButton提供下拉菜单选择排序方式。
排序时使用List.from创建副本再排序,避免修改原列表。compareTo方法用于字符串比较。
搜索功能
当收藏的衣物很多时,搜索功能可以帮助用户快速找到目标。
class _FavoritesScreenState extends State<FavoritesScreen> {
final _searchController = TextEditingController();
String _searchQuery = '';
List<ClothingItem> _filterFavorites(List<ClothingItem> favorites) {
if (_searchQuery.isEmpty) return favorites;
return favorites.where((item) {
return item.name.contains(_searchQuery) ||
item.category.contains(_searchQuery) ||
item.color.contains(_searchQuery);
}).toList();
}
Widget _buildSearchBar() {
return Padding(
padding: EdgeInsets.all(16.w),
child: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '搜索收藏的衣物...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(24.r)),
),
onChanged: (value) => setState(() => _searchQuery = value),
),
);
}
}
搜索支持按名称、分类、颜色匹配,覆盖用户可能的搜索意图。where方法过滤列表,返回满足条件的元素。
搜索框使用圆角边框,prefixIcon显示搜索图标。onChanged实时更新搜索关键词,实现即时搜索。
分组显示
可以按分类对收藏的衣物进行分组显示,方便用户查看。
Widget _buildGroupedView(List<ClothingItem> favorites) {
Map<String, List<ClothingItem>> grouped = {};
for (var item in favorites) {
grouped.putIfAbsent(item.category, () => []);
grouped[item.category]!.add(item);
}
return ListView.builder(
itemCount: grouped.length,
itemBuilder: (context, index) {
final category = grouped.keys.elementAt(index);
final items = grouped[category]!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: EdgeInsets.all(16.w),
child: Text(
'$category (${items.length})',
style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold),
),
),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: EdgeInsets.symmetric(horizontal: 16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
childAspectRatio: 0.75,
),
itemCount: items.length,
itemBuilder: (context, i) => _buildFavoriteCard(context, items[i], provider),
),
],
);
},
);
}
使用Map按分类分组,key是分类名称,value是该分类下的衣物列表。分组标题显示分类名称和数量。
嵌套的GridView需要设置shrinkWrap为true,physics为NeverScrollableScrollPhysics,让它不可滚动并自适应高度。
空搜索结果处理
当搜索没有结果时,需要显示友好的提示。
Widget _buildSearchEmpty() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 64.sp, color: Colors.grey),
SizedBox(height: 16.h),
Text('没有找到相关衣物', style: TextStyle(fontSize: 16.sp, color: Colors.grey)),
SizedBox(height: 8.h),
Text('试试其他关键词', style: TextStyle(fontSize: 14.sp, color: Colors.grey)),
],
),
);
}
搜索无结果使用search_off图标,直观表达搜索失败。副文案建议用户尝试其他关键词,给出下一步操作指引。
这种空状态设计与收藏为空的状态保持一致的风格,用户体验更统一。
动画效果增强
为收藏和取消收藏添加动画效果,提升交互体验。
class _FavoriteButtonState extends State<FavoriteButton> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scaleAnimation;
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_scaleAnimation = Tween<double>(begin: 1.0, end: 1.3).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
void _onTap() {
_controller.forward().then((_) => _controller.reverse());
widget.onToggle();
}
Widget build(BuildContext context) {
return GestureDetector(
onTap: _onTap,
child: ScaleTransition(
scale: _scaleAnimation,
child: Icon(
widget.isFavorite ? Icons.favorite : Icons.favorite_border,
color: widget.isFavorite ? Colors.red : Colors.grey,
),
),
);
}
}
点击爱心时先放大到1.3倍再恢复原大小,形成弹跳效果。AnimationController控制动画时长,CurvedAnimation添加缓动曲线。
forward()返回Future,可以用then链式调用reverse(),实现先放大后缩小的效果。
完整代码整合
把所有功能整合在一起,形成完整的收藏页面。
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../providers/wardrobe_provider.dart';
import '../../models/clothing_item.dart';
import 'clothing_detail_screen.dart';
class FavoritesScreen extends StatelessWidget {
const FavoritesScreen({super.key});
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('我的收藏')),
body: Consumer<WardrobeProvider>(
builder: (context, provider, child) {
final favorites = provider.getFavorites();
if (favorites.isEmpty) {
return _buildEmptyState();
}
return GridView.builder(
padding: EdgeInsets.all(16.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12.w,
mainAxisSpacing: 12.h,
childAspectRatio: 0.75,
),
itemCount: favorites.length,
itemBuilder: (context, index) {
final item = favorites[index];
return _buildFavoriteCard(context, item, provider);
},
);
},
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite_border, size: 64.sp, color: Colors.grey),
SizedBox(height: 16.h),
Text('暂无收藏衣物', style: TextStyle(fontSize: 16.sp, color: Colors.grey)),
SizedBox(height: 8.h),
Text('点击衣物详情页的❤️添加收藏', style: TextStyle(fontSize: 14.sp, color: Colors.grey)),
],
),
);
}
Widget _buildFavoriteCard(BuildContext context, ClothingItem item, WardrobeProvider provider) {
return GestureDetector(
onTap: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => ClothingDetailScreen(item: item)),
),
child: Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: ClothingItem.getColorFromName(item.color).withOpacity(0.3),
borderRadius: BorderRadius.vertical(top: Radius.circular(12.r)),
),
child: Stack(
children: [
Center(
child: Icon(Icons.checkroom, size: 48.sp, color: ClothingItem.getColorFromName(item.color)),
),
Positioned(
top: 8.h,
right: 8.w,
child: GestureDetector(
onTap: () => provider.toggleFavorite(item.id),
child: const Icon(Icons.favorite, color: Colors.red),
),
),
],
),
),
),
Padding(
padding: EdgeInsets.all(8.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.name, style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold), maxLines: 1, overflow: TextOverflow.ellipsis),
SizedBox(height: 4.h),
Text('${item.category} · ${item.color}', style: TextStyle(fontSize: 12.sp, color: Colors.grey)),
],
),
),
],
),
),
);
}
}
代码结构清晰,主build方法处理整体逻辑,_buildEmptyState和_buildFavoriteCard分别处理空状态和卡片渲染。
Consumer确保收藏状态变化时界面自动刷新,用户在详情页收藏或取消收藏后,返回收藏列表会看到最新状态。
写在最后
收藏功能虽然常见,但要做好需要考虑很多细节。从状态管理到交互反馈,从空状态到批量操作,每一个环节都影响着用户体验。
希望这篇文章能帮助你实现一个完善的收藏功能,让用户能够方便地管理自己喜欢的衣物。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐


所有评论(0)