在这里插入图片描述

收藏功能让用户保存感兴趣的商品,方便以后查看和购买。今天我们来实现"闲置换"的我的收藏页面,包括空状态展示和收藏列表。

收藏页面的设计思路

收藏页面比较简单,主要是展示用户收藏的商品列表。如果没有收藏任何商品,显示空状态引导用户去首页浏览。有收藏的话用网格布局展示,每个卡片右上角有取消收藏的按钮,长按可以弹出更多操作。

完整代码实现

import 'package:flutter/material.dart';

class FavoritesPage extends StatelessWidget {
  const FavoritesPage({super.key});

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('我的收藏')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.favorite_border, size: 80, color: Colors.grey[300]),
            const SizedBox(height: 16),
            Text('暂无收藏', style: TextStyle(color: Colors.grey[500])),
            const SizedBox(height: 8),
            Text('去首页逛逛吧', style: TextStyle(color: Colors.grey[400], fontSize: 14)),
          ],
        ),
      ),
    );
  }
}

这是收藏页面的基础版本,展示空状态。用空心爱心图标Icons.favorite_border表示收藏,比实心爱心更能传达"空"的感觉。图标设置80像素大小,灰色调不会太突兀。主提示"暂无收藏"用深一点的灰色,副提示"去首页逛逛吧"用浅灰色,形成主次分明的视觉层次。Center组件让整个内容块在页面正中央显示。

收藏列表的完整实现

实际项目中收藏页面应该这样实现:

class FavoritesPage extends StatefulWidget {
  const FavoritesPage({super.key});

  
  State<FavoritesPage> createState() => _FavoritesPageState();
}

class _FavoritesPageState extends State<FavoritesPage> {
  List<Map<String, dynamic>> _favorites = [];
  bool _isLoading = true;

  
  void initState() {
    super.initState();
    _loadFavorites();
  }

  Future<void> _loadFavorites() async {
    setState(() => _isLoading = true);
    try {
      final data = await api.getFavorites();
      setState(() {
        _favorites = data;
        _isLoading = false;
      });
    } catch (e) {
      setState(() => _isLoading = false);
      Get.snackbar('错误', '加载失败,请重试');
    }
  }

把页面改成StatefulWidget来管理状态。_favorites存储收藏列表数据,_isLoading控制加载状态的显示。initState里调用_loadFavorites加载数据,加载前先把_isLoading设为true显示loading,加载完成后设为false。用try-catch捕获异常,加载失败时弹出错误提示,避免页面卡在loading状态。

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('我的收藏'),
        actions: [
          if (_favorites.isNotEmpty)
            TextButton(
              onPressed: _toggleEditMode,
              child: Text(_isEditMode ? '完成' : '编辑'),
            ),
        ],
      ),
      body: _buildBody(),
    );
  }

  Widget _buildBody() {
    if (_isLoading) {
      return const Center(child: CircularProgressIndicator());
    }
    
    if (_favorites.isEmpty) {
      return _buildEmptyState();
    }
    
    return RefreshIndicator(
      onRefresh: _loadFavorites,
      child: GridView.builder(
        padding: const EdgeInsets.all(12),
        gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
          crossAxisCount: 2,
          childAspectRatio: 0.65,
          crossAxisSpacing: 10,
          mainAxisSpacing: 10,
        ),
        itemCount: _favorites.length,
        itemBuilder: (context, index) => _buildFavoriteCard(_favorites[index]),
      ),
    );
  }

build方法根据状态显示不同内容。AppBar右边在有收藏时显示编辑按钮,点击可以进入批量选择模式。_buildBody方法处理三种情况:加载中显示圆形进度条,空数据显示空状态页,有数据显示网格列表。GridView.builder用两列网格布局,childAspectRatio: 0.65控制卡片宽高比,让卡片稍微高一些以容纳图片和文字。RefreshIndicator支持下拉刷新。

  Widget _buildEmptyState() {
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Icon(Icons.favorite_border, size: 80, color: Colors.grey[300]),
          const SizedBox(height: 16),
          Text('暂无收藏', style: TextStyle(color: Colors.grey[500])),
          const SizedBox(height: 8),
          Text('去首页逛逛吧', style: TextStyle(color: Colors.grey[400], fontSize: 14)),
          const SizedBox(height: 24),
          ElevatedButton(
            onPressed: () => Get.offAll(() => const MainPage()),
            style: ElevatedButton.styleFrom(
              backgroundColor: const Color(0xFF07C160),
            ),
            child: const Text('去首页', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),
    );
  }

空状态页面比基础版多了一个跳转按钮。ElevatedButton用主题绿色作为背景,白色文字,点击后跳转到首页。Get.offAll会清空整个路由栈然后跳转,相当于重新打开App直接进入首页,用户按返回键会退出App而不是回到收藏页。这种处理方式在空状态引导场景很常见。

  Widget _buildFavoriteCard(Map<String, dynamic> product) {
    return GestureDetector(
      onTap: () => Get.to(() => ProductDetailPage(productId: product['id'])),
      onLongPress: () => _showActionSheet(product),
      child: Container(
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Stack(
          children: [
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Expanded(
                  flex: 3,
                  child: Container(
                    decoration: BoxDecoration(
                      borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
                      color: Colors.grey[200],
                    ),
                    child: Center(child: Icon(Icons.image, size: 60, color: Colors.grey[400])),
                  ),
                ),
                Expanded(
                  flex: 2,
                  child: Padding(
                    padding: const EdgeInsets.all(10),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(product['title'], style: const TextStyle(fontSize: 14), maxLines: 2, overflow: TextOverflow.ellipsis),
                        const Spacer(),
                        Text('¥${product['price']}', style: const TextStyle(color: Color(0xFFFF4D4F), fontSize: 16, fontWeight: FontWeight.bold)),
                      ],
                    ),
                  ),
                ),
              ],
            ),
        ),
      ),
    );
  }

收藏卡片用Stack实现图片上叠加爱心按钮的效果。卡片分为上下两部分,图片区域占3份高度,信息区域占2份。GestureDetector处理点击和长按事件,点击进入商品详情,长按弹出操作菜单。右上角的红色爱心按钮用Positioned定位,半透明黑色圆形背景让按钮在任何颜色的图片上都清晰可见。标题最多显示两行,超出部分用省略号。

  void _showActionSheet(Map<String, dynamic> product) {
    Get.bottomSheet(
      Container(
        padding: const EdgeInsets.all(16),
        decoration: const BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
        ),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            ListTile(
              leading: const Icon(Icons.delete_outline),
              title: const Text('取消收藏'),
              onTap: () {
                Get.back();
                _removeFavorite(product);
              },
            ),
            ListTile(
              leading: const Icon(Icons.share),
              title: const Text('分享'),
              onTap: () {
                Get.back();
                _shareProduct(product);
              },
            ),
            const SizedBox(height: 8),
            SizedBox(
              width: double.infinity,
              child: TextButton(
                onPressed: () => Get.back(),
                child: const Text('取消'),
              ),
            ),
          ],
        ),
      ),
    );
  }

  Future<void> _removeFavorite(Map<String, dynamic> product) async {
    try {
      await api.removeFavorite(product['id']);
      setState(() {
        _favorites.removeWhere((item) => item['id'] == product['id']);
      });
      Get.snackbar('成功', '已取消收藏');
    } catch (e) {
      Get.snackbar('错误', '操作失败,请重试');
    }
  }

长按弹出底部操作菜单,用Get.bottomSheet实现。菜单顶部有圆角,包含取消收藏和分享两个选项,底部是取消按钮。mainAxisSize: MainAxisSize.min让菜单高度自适应内容。取消收藏时先调用接口,成功后用removeWhere从本地列表中删除对应商品,这样不需要重新请求接口就能更新UI,体验更流畅。

收藏状态的同步

收藏状态需要在多个页面同步,可以用GetX的状态管理实现:

class FavoriteController extends GetxController {
  var favorites = <Map<String, dynamic>>[].obs;
  
  Future<void> addFavorite(Map<String, dynamic> product) async {
    await api.addFavorite(product['id']);
    favorites.add(product);
  }
  
  Future<void> removeFavorite(int productId) async {
    await api.removeFavorite(productId);
    favorites.removeWhere((item) => item['id'] == productId);
  }
  
  bool isFavorite(int productId) {
    return favorites.any((item) => item['id'] == productId);
  }
}

用GetX的Controller统一管理收藏状态。favorites.obs声明为响应式变量,数据变化时会自动通知所有监听者更新UI。在商品详情页和收藏页面都使用这个Controller,用户在详情页点击收藏后,收藏页面会自动显示新收藏的商品,无需手动刷新。isFavorite方法用于判断某个商品是否已收藏,详情页可以据此显示不同的收藏按钮状态。

小结

这篇实现了"闲置换"App的我的收藏页面,包括空状态展示和收藏列表。收藏卡片右上角有取消收藏按钮,长按弹出操作菜单。用GetX的Controller实现收藏状态的跨页面同步,让用户体验更加流畅。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐