在这里插入图片描述

从分类指南页面点击某个分类,就会进入这个分类详情页面。这里展示该分类的完整信息,包括分类说明和所有常见垃圾物品的列表。页面的颜色会根据分类类型动态变化,可回收物是蓝色调,有害垃圾是红色调,一眼就能认出来。

接收分类类型参数

页面通过路由参数接收垃圾类型:

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

  
  Widget build(BuildContext context) {
    // 从路由参数获取分类类型
    final GarbageType type = Get.arguments;
    // 根据类型找到对应的分类数据
    final category = GarbageData.categories.firstWhere((c) => c.type == type);
    // 获取分类对应的颜色
    final color = _getTypeColor(type);

Get.arguments拿到的是GarbageType枚举值,然后从GarbageData.categories里找到对应的分类数据。同时根据类型获取对应的主题颜色。

firstWhere的用法firstWhere会返回第一个满足条件的元素。这里用它来根据type找到对应的category对象。

动态颜色的AppBar

AppBar的背景色根据分类类型变化:

    return Scaffold(
      appBar: AppBar(
        title: Text(category.name),
        backgroundColor: color,
        actions: [
          // 搜索按钮,可以在当前分类内搜索
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () => Get.toNamed(Routes.search),
          ),
        ],
      ),

进入可回收物详情,AppBar是蓝色;进入有害垃圾详情,AppBar是红色。这种设计让用户一眼就知道自己在看哪个分类。

页面主体分两部分:

      body: Column(
        children: [
          // 头部信息区域
          _buildHeader(category, color),
          // 物品列表
          Expanded(
            child: ListView.builder(
              padding: EdgeInsets.all(16.w),
              itemCount: category.items.length,
              itemBuilder: (context, index) {
                return _buildItemCard(category.items[index], color);
              },
            ),
          ),
        ],
      ),
    );
  }

上面是头部信息区域,下面是物品列表。用Expanded包着ListView,让列表占据剩余空间。

头部信息区域

头部展示分类的图标、名称、描述和物品数量:

Widget _buildHeader(GarbageCategory category, Color color) {
  return Container(
    padding: EdgeInsets.all(20.w),
    decoration: BoxDecoration(
      // 使用分类颜色的浅色版本作为背景
      color: color.withOpacity(0.1),
    ),
    child: Row(
      children: [
        // 分类图标
        Container(
          width: 72.w,
          height: 72.w,
          decoration: BoxDecoration(
            color: color.withOpacity(0.2),
            borderRadius: BorderRadius.circular(16.r),
          ),
          child: Center(
            child: Text(category.icon, style: TextStyle(fontSize: 40.sp)),
          ),
        ),
        SizedBox(width: 16.w),
        // 分类信息
        Expanded(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 分类名称
              Text(
                category.name,
                style: TextStyle(
                  fontSize: 24.sp,
                  fontWeight: FontWeight.bold,
                  color: color,
                ),
              ),
              SizedBox(height: 4.h),
              // 分类描述
              Text(
                category.description,
                style: TextStyle(fontSize: 14.sp, color: Colors.grey.shade600),
                maxLines: 2,
                overflow: TextOverflow.ellipsis,
              ),
              SizedBox(height: 8.h),
              // 物品数量标签
              Container(
                padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 4.h),
                decoration: BoxDecoration(
                  color: color.withOpacity(0.15),
                  borderRadius: BorderRadius.circular(4.r),
                ),
                child: Text(
                  '${category.items.length} 个常见物品',
                  style: TextStyle(fontSize: 12.sp, color: color),
                ),
              ),
            ],
          ),
        ),
      ],
    ),
  );
}

背景用分类颜色的10%透明度版本,跟AppBar形成呼应但不会太重。大号的emoji图标是视觉焦点,旁边是分类名称和描述。

颜色的层次:AppBar用纯色,头部背景用浅色,这样从上到下有个颜色的过渡,不会显得突兀。

物品列表卡片

每个垃圾物品显示为一个可点击的卡片:

Widget _buildItemCard(GarbageItem item, Color color) {
  return Card(
    margin: EdgeInsets.only(bottom: 8.h),
    elevation: 1,
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(12.r),
    ),
    child: InkWell(
      onTap: () => Get.toNamed(Routes.itemDetail, arguments: item),
      borderRadius: BorderRadius.circular(12.r),
      child: Padding(
        padding: EdgeInsets.all(12.w),
        child: Row(
          children: [
            // 物品图标
            Container(
              width: 48.w,
              height: 48.w,
              decoration: BoxDecoration(
                color: color.withOpacity(0.1),
                borderRadius: BorderRadius.circular(8.r),
              ),
              child: Center(
                child: Text(item.icon, style: TextStyle(fontSize: 28.sp)),
              ),
            ),
            SizedBox(width: 12.w),
            // 物品信息
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    item.name,
                    style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.w500),
                  ),
                  SizedBox(height: 4.h),
                  Text(
                    item.description,
                    style: TextStyle(fontSize: 13.sp, color: Colors.grey),
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  ),
                ],
              ),
            ),
            // 箭头图标
            Icon(Icons.arrow_forward_ios, size: 16.sp, color: Colors.grey),
          ],
        ),
      ),
    ),
  );
}

InkWell包裹实现点击水波纹效果。ListTile虽然方便,但自定义布局时用Row更灵活。

  • leading:物品的emoji图标,背景用分类颜色的浅色版本
  • title:物品名称
  • subtitle:物品描述,限制一行,超出显示省略号
  • trailing:箭头图标,提示可以点击

点击后跳转到物品详情页,把完整的GarbageItem对象传过去。

颜色映射方法

根据垃圾类型返回对应颜色:

Color _getTypeColor(GarbageType type) {
  switch (type) {
    case GarbageType.recyclable:
      return AppTheme.recyclableColor;
    case GarbageType.hazardous:
      return AppTheme.hazardousColor;
    case GarbageType.kitchen:
      return AppTheme.kitchenColor;
    case GarbageType.other:
      return AppTheme.otherColor;
  }
}

枚举匹配:这里直接用枚举值匹配,比用字符串匹配更安全。如果写错了枚举值,编译器会报错;写错字符串的话,编译器不会提醒。

数据模型的结构

分类数据的模型是这样定义的:

class GarbageCategory {
  final String id;
  final String name;
  final GarbageType type;
  final String description;
  final String icon;
  final List<GarbageItem> items;

  GarbageCategory({
    required this.id,
    required this.name,
    required this.type,
    required this.description,
    required this.icon,
    this.items = const [],
  });
}

每个分类包含:

  • id:唯一标识
  • name:分类名称,如"可回收物"
  • type:分类类型枚举
  • description:分类描述
  • icon:emoji图标
  • items:该分类下的所有垃圾物品列表

页面的信息层次

分类详情页面的设计体现了信息架构的层次性:

  1. 第一层:AppBar颜色 → 快速识别是哪个分类
  2. 第二层:头部信息 → 了解分类的基本情况
  3. 第三层:物品列表 → 查看具体有哪些垃圾
  4. 第四层:物品详情 → 深入了解某个垃圾的分类信息

用户可以根据自己的需求,在不同层次停留或深入。这种渐进式的信息展示,比一股脑把所有信息堆在一起要好得多。

列表性能优化

当分类下的物品很多时,需要考虑列表性能:

// 使用ListView.builder而不是ListView
ListView.builder(
  itemCount: category.items.length,
  itemBuilder: (context, index) {
    return _buildItemCard(category.items[index], color);
  },
);

// 如果需要更好的性能,可以使用ListView.separated
ListView.separated(
  itemCount: category.items.length,
  separatorBuilder: (context, index) => SizedBox(height: 8.h),
  itemBuilder: (context, index) {
    return _buildItemCard(category.items[index], color);
  },
);

ListView.builder只会构建可见区域的item,滚动时动态创建和销毁,内存占用更低。

搜索功能的扩展

AppBar上的搜索按钮可以扩展为分类内搜索:

IconButton(
  icon: const Icon(Icons.search),
  onPressed: () {
    // 方式1:跳转到搜索页,预设分类筛选
    Get.toNamed(Routes.search, arguments: {'filterType': type});
    
    // 方式2:在当前页面显示搜索框
    showSearch(
      context: context,
      delegate: CategorySearchDelegate(category.items),
    );
  },
),

分类内搜索可以帮助用户在大量物品中快速找到目标。

空状态处理

如果某个分类下没有物品(虽然实际不太可能),需要显示空状态:

body: Column(
  children: [
    _buildHeader(category, color),
    Expanded(
      child: category.items.isEmpty
          ? Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.inbox, size: 64.sp, color: Colors.grey),
                  SizedBox(height: 16.h),
                  Text('暂无物品数据', style: TextStyle(color: Colors.grey)),
                ],
              ),
            )
          : ListView.builder(...),
    ),
  ],
),

下拉刷新的实现

如果数据来自网络,可以添加下拉刷新:

Expanded(
  child: RefreshIndicator(
    onRefresh: () async {
      await guideController.refreshCategoryItems(type);
    },
    child: ListView.builder(...),
  ),
),

RefreshIndicator会在用户下拉时显示刷新指示器,刷新完成后自动隐藏。

与其他页面的导航关系

分类详情页在导航结构中的位置:

首页
  └── 分类指南页
        └── 分类详情页(当前)
              └── 物品详情页

用户可以通过以下方式到达分类详情页:

  1. 从分类指南页点击某个分类
  2. 从首页的分类快捷入口
  3. 从搜索结果的分类标签

页面状态保持

如果用户从分类详情进入物品详情,再返回,列表的滚动位置应该保持:

// 方式1:使用PageStorageKey
ListView.builder(
  key: PageStorageKey('category_${category.id}'),
  // ...
);

// 方式2:使用ScrollController手动管理
final ScrollController _scrollController = ScrollController();


void dispose() {
  _scrollController.dispose();
  super.dispose();
}

PageStorageKey会自动保存和恢复滚动位置,是最简单的方案。

动画效果

可以为列表添加入场动画:

itemBuilder: (context, index) {
  return AnimatedOpacity(
    opacity: 1.0,
    duration: Duration(milliseconds: 300 + index * 50),
    child: _buildItemCard(category.items[index], color),
  );
}

或者使用flutter_staggered_animations包实现更复杂的交错动画效果。


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

Logo

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

更多推荐