在这里插入图片描述

收藏功能是很多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

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐