在这里插入图片描述

在浏览菜谱时,我们经常会遇到这样的情况:看到一道菜觉得不错,但没有立即收藏,过几天想起来却找不到了。今天我们要实现浏览历史记录功能,让用户能够回顾之前看过的所有菜谱。

浏览历史的设计思路

浏览历史要解决的核心问题是:如何让用户快速找到之前看过的菜谱?我选择了列表布局,因为历史记录通常按时间排序,列表最适合展示时间序列数据。

每个历史记录显示菜谱图片、名称和浏览时间。浏览时间很重要,它能帮助用户回忆是什么时候看的。比如"2小时前"、"昨天"这样的相对时间,比具体的日期时间更直观。

列表项可以点击进入详情页,也可以滑动删除。滑动删除是移动应用的通用交互,用户很容易理解。这种设计在邮件、消息等应用中很常见。

创建无状态组件

浏览历史页面的内容相对固定,使用 StatelessWidget 就够了。

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';

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

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('浏览历史'),
        actions: [
          TextButton(
            onPressed: () {},
            child: const Text('清空', style: TextStyle(color: Colors.white)),
          ),
        ],
      ),

AppBar 右侧放了一个"清空"按钮,用于清空所有历史记录。按钮使用白色文字,和 AppBar 的背景色对比明显。

清空是一个比较重要的操作,所以放在 AppBar 上,让用户容易找到。点击后应该弹出确认对话框,避免误操作。

      body: ListView.builder(
        padding: EdgeInsets.all(16.w),
        itemCount: 30,
        itemBuilder: (context, index) => _buildItem(index),
      ),
    );
  }

body 使用 ListView.builder 来展示历史记录。padding 设置为 16.w,让列表内容不要紧贴屏幕边缘。

itemCount 现在是固定的 30,实际开发中应该根据历史记录数量动态设置。历史记录可能很多,所以使用 ListView.builder 而不是 ListView,这样只会渲染可见区域的内容。

实现列表项

每个列表项包含图片、名称和浏览时间。

  Widget _buildItem(int index) {
    return Container(
      margin: EdgeInsets.only(bottom: 12.h),
      padding: EdgeInsets.all(12.w),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(12.r),
      ),

列表项使用卡片样式,白色背景和圆角。margin 设置为 bottom: 12.h,让卡片之间有间距。padding 设置为 12.w,让内容不要紧贴边缘。

这里没有设置阴影,因为背景色已经是浅灰色了,白色卡片本身就有足够的对比度。

      child: Row(
        children: [
          Container(
            width: 60.w,
            height: 60.h,
            decoration: BoxDecoration(
              color: Colors.orange.shade100,
              borderRadius: BorderRadius.circular(8.r),
            ),
            child: Icon(Icons.restaurant, size: 30.sp, color: Colors.orange),
          ),

左侧是菜谱图片的占位符。实际开发中应该显示真实的图片,这里用图标代替。容器大小 60x60,比其他页面的图片稍小一些,因为历史记录不需要太大的图片。

背景色是橙色的浅色版本,圆角 8.r。图标大小 30.sp,颜色是橙色,和背景色搭配。

          SizedBox(width: 12.w),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text('历史菜谱 ${index + 1}', style: TextStyle(fontSize: 14.sp, fontWeight: FontWeight.bold)),
                SizedBox(height: 4.h),
                Text('浏览于 2小时前', style: TextStyle(fontSize: 11.sp, color: Colors.grey)),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

右侧是菜谱信息。使用 Expanded 让这部分占据剩余空间。菜谱名称使用粗体,字号 14.sp。

浏览时间使用灰色,字号 11.sp,表示这是次要信息。时间使用相对时间格式,比如"2小时前"、"昨天"等,这比具体的日期时间更直观。

实现相对时间

相对时间的计算需要一个辅助函数:

String getRelativeTime(DateTime dateTime) {
  final now = DateTime.now();
  final difference = now.difference(dateTime);
  
  if (difference.inMinutes < 60) {
    return '${difference.inMinutes}分钟前';
  } else if (difference.inHours < 24) {
    return '${difference.inHours}小时前';
  } else if (difference.inDays == 1) {
    return '昨天';
  } else if (difference.inDays < 7) {
    return '${difference.inDays}天前';
  } else {
    return '${dateTime.month}${dateTime.day}日';
  }
}

这个函数根据时间差返回不同的格式。1小时内显示分钟,24小时内显示小时,1天显示"昨天",7天内显示天数,超过7天显示具体日期。

这种相对时间格式在社交应用中很常见,用户很容易理解。它比具体的日期时间更有亲和力。

添加点击交互

点击列表项应该跳转到菜谱详情页:

Widget _buildItem(int index) {
  return GestureDetector(
    onTap: () {
      Get.to(() => RecipeDetailPage(recipeId: 'recipe_$index'));
    },
    child: Container(
      // ...
    ),
  );
}

使用 GestureDetector 包裹整个卡片,点击时跳转到详情页。传递菜谱 ID 作为参数,详情页会根据这个 ID 加载对应的菜谱数据。

跳转到详情页后,应该再次记录浏览历史。这样历史记录会更新,最近浏览的菜谱会排在前面。

实现滑动删除

用户可能想删除某些历史记录。可以使用 Dismissible 组件实现滑动删除:

Widget _buildItem(int index) {
  return Dismissible(
    key: Key('history_$index'),
    direction: DismissDirection.endToStart,
    background: Container(
      alignment: Alignment.centerRight,
      padding: EdgeInsets.only(right: 20.w),
      color: Colors.red,
      child: Icon(Icons.delete, color: Colors.white),
    ),
    onDismissed: (direction) {
      // 删除历史记录
      Get.snackbar('成功', '已删除历史记录');
    },
    child: Container(
      // ...
    ),
  );
}

Dismissible 组件可以让子组件支持滑动删除。key 是必需的,用于标识每个列表项。

direction 设置为 endToStart,表示只能从右向左滑动。这是 iOS 的标准交互,Android 用户也很熟悉。

background 是滑动时显示的背景。我们使用红色背景和删除图标,表示这是删除操作。图标靠右对齐,跟随手指滑动。

onDismissed 回调在滑动完成后触发。我们在这里执行删除操作,并显示提示。

实现清空功能

AppBar 上的"清空"按钮用于清空所有历史记录:

TextButton(
  onPressed: () {
    _confirmClearAll(context);
  },
  child: const Text('清空', style: TextStyle(color: Colors.white)),
)

点击后弹出确认对话框,避免误操作:

void _confirmClearAll(BuildContext context) {
  showDialog(
    context: context,
    builder: (context) {
      return AlertDialog(
        title: Text('清空历史'),
        content: Text('确定要清空所有浏览历史吗?'),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('取消'),
          ),
          TextButton(
            onPressed: () {
              Navigator.pop(context);
              // 执行清空操作
              Get.snackbar('成功', '已清空浏览历史');
            },
            child: Text('清空', style: TextStyle(color: Colors.red)),
          ),
        ],
      );
    },
  );
}

确认对话框和删除确认类似。"清空"按钮使用红色,表示这是危险操作。

添加空状态

如果用户还没有浏览任何菜谱,需要显示空状态:

if (history.isEmpty) {
  return Center(
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(Icons.history, size: 64.sp, color: Colors.grey),
        SizedBox(height: 16.h),
        Text('还没有浏览记录', style: TextStyle(color: Colors.grey)),
        SizedBox(height: 8.h),
        ElevatedButton(
          onPressed: () => Get.back(),
          child: Text('去浏览菜谱'),
        ),
      ],
    ),
  );
}

空状态使用历史图标,表示这是历史记录页面。文字说明当前状态,按钮引导用户去浏览菜谱。

添加分组功能

历史记录可以按日期分组,比如"今天"、“昨天”、"本周"等:

ListView.builder(
  itemCount: groups.length,
  itemBuilder: (context, index) {
    final group = groups[index];
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: EdgeInsets.all(16.w),
          child: Text(
            group.title,
            style: TextStyle(
              fontSize: 16.sp,
              fontWeight: FontWeight.bold,
              color: Colors.grey,
            ),
          ),
        ),
        ...group.items.map((item) => _buildItem(item)),
      ],
    );
  },
)

每个分组包含一个标题和多个列表项。标题使用灰色粗体,和列表项区分开。

这种分组方式能让用户更容易找到想要的历史记录。比如要找今天看过的菜谱,直接看"今天"这一组就行了。

添加搜索功能

如果历史记录很多,可以添加搜索功能:

AppBar(
  title: TextField(
    decoration: InputDecoration(
      hintText: '搜索历史记录...',
      border: InputBorder.none,
      prefixIcon: Icon(Icons.search),
    ),
    onChanged: (value) {
      // 过滤历史记录
    },
  ),
)

搜索框放在 AppBar 的 title 位置,用户输入关键词时实时过滤列表。这需要把 StatelessWidget 改成 StatefulWidget,用一个变量来存储过滤后的列表。

优化性能

历史记录可能很多,需要考虑性能优化。可以使用分页加载,每次只加载一部分数据:

ListView.builder(
  controller: _scrollController,
  itemCount: displayedItems.length + 1,
  itemBuilder: (context, index) {
    if (index == displayedItems.length) {
      // 加载更多
      return Center(child: CircularProgressIndicator());
    }
    return _buildItem(displayedItems[index]);
  },
)

监听滚动位置,当滚动到底部时加载更多数据。这样可以避免一次性加载所有数据,提升性能。

总结

浏览历史记录功能使用列表布局,每个记录显示图片、名称和浏览时间。用户可以点击查看详情,也可以滑动删除或清空所有记录。

通过合理的交互设计,我们让历史记录功能既实用又方便。用户可以轻松回顾之前看过的菜谱,不用担心找不到喜欢的菜。

下一篇文章我们将实现菜谱搜索功能,让用户能够快速找到想要的菜谱。


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

Logo

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

更多推荐