在这里插入图片描述

养猫的开销需要有个地方记录和查看。今天我们来实现支出列表页面,展示所有支出记录,支持滑动删除,还有一个醒目的总支出统计。


功能规划

支出列表页面需要实现:

  • 顶部显示总支出金额
  • 列表展示所有支出记录
  • 支持滑动删除记录
  • 空状态友好提示
  • 快速添加新支出

把这些功能组合起来,就是一个实用的支出管理页面。


依赖引入

首先导入需要的包:

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:intl/intl.dart';
import '../../providers/cat_provider.dart';
import '../../models/expense_record.dart';
import 'add_expense_screen.dart';

Provider管理支出数据,数据变化时自动刷新UI。
intl用于日期格式化显示。


无状态组件

支出列表不需要维护额外状态:

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

数据由Provider管理,页面本身不需要状态。
StatelessWidget更简洁高效。


页面结构

build方法构建整体布局:

  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('支出记录')),
      body: Consumer<CatProvider>(
        builder: (context, provider, child) {
          final records = provider.expenseRecords;
          final totalExpense = provider.getTotalExpenses();

Consumer监听CatProvider的变化。
获取支出记录列表和总支出金额。


页面内容布局

Column组织顶部统计和列表:

          return Column(
            children: [
              _buildTotalCard(totalExpense),
              Expanded(
                child: records.isEmpty
                    ? Center(
                        child: Column(
                          mainAxisAlignment: MainAxisAlignment.center,
                          children: [
                            Icon(Icons.receipt_long, size: 80.sp, color: Colors.grey[300]),
                            SizedBox(height: 16.h),
                            Text('暂无支出记录', style: TextStyle(color: Colors.grey[600])),
                          ],
                        ),
                      )

Column让统计卡片和列表垂直排列。
Expanded让列表占据剩余空间。

有数据时显示列表:

                    : ListView.builder(
                        padding: EdgeInsets.all(16.w),
                        itemCount: records.length,
                        itemBuilder: (context, index) => _buildExpenseCard(context, records[index], provider),
                      ),
              ),
            ],
          );
        },
      ),

ListView.builder按需构建列表项。
传入provider用于删除操作。


悬浮添加按钮

页面底部的FAB:

      floatingActionButton: FloatingActionButton(
        onPressed: () => Navigator.push(context, MaterialPageRoute(
          builder: (_) => const AddExpenseScreen(),
        )),
        backgroundColor: Colors.orange,
        child: const Icon(Icons.add),
      ),
    );
  }

点击跳转到添加支出页面。
橙色与App主题一致。


总支出卡片

顶部的统计卡片:

  Widget _buildTotalCard(double total) {
    return Container(
      padding: EdgeInsets.all(20.w),
      decoration: BoxDecoration(
        gradient: LinearGradient(
          colors: [Colors.orange, Colors.orange.shade300],
        ),
      ),
      child: Row(
        children: [
          Icon(Icons.account_balance_wallet, color: Colors.white, size: 40.sp),
          SizedBox(width: 16.w),

渐变背景让卡片更有层次感。
钱包图标直观表达支出含义。

金额显示:

          Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text('总支出', style: TextStyle(color: Colors.white70, fontSize: 14.sp)),
              Text(
                ${total.toStringAsFixed(2)}',
                style: TextStyle(color: Colors.white, fontSize: 28.sp, fontWeight: FontWeight.bold),
              ),
            ],
          ),
        ],
      ),
    );
  }

大字号显示金额,一目了然。
保留两位小数更精确。


支出卡片组件

构建单条支出记录:

  Widget _buildExpenseCard(BuildContext context, ExpenseRecord record, CatProvider provider) {
    return Dismissible(
      key: Key(record.id),
      direction: DismissDirection.endToStart,
      background: Container(
        color: Colors.red,
        alignment: Alignment.centerRight,
        padding: EdgeInsets.only(right: 16.w),
        child: const Icon(Icons.delete, color: Colors.white),
      ),
      onDismissed: (_) => provider.deleteExpenseRecord(record.id),

Dismissible实现滑动删除功能。
从右向左滑动显示红色删除背景。

卡片内容:

      child: Card(
        margin: EdgeInsets.only(bottom: 8.h),
        child: ListTile(
          leading: CircleAvatar(
            backgroundColor: _getCategoryColor(record.category).withOpacity(0.1),
            child: Icon(_getCategoryIcon(record.category), color: _getCategoryColor(record.category)),
          ),
          title: Text(record.title),
          subtitle: Text('${record.categoryString} · ${DateFormat('MM-dd').format(record.date)}'),

CircleAvatar显示类别对应的图标。
副标题显示类别和日期。

金额显示:

          trailing: Text(
            ${record.amount.toStringAsFixed(2)}',
            style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold, color: Colors.orange),
          ),
        ),
      ),
    );
  }

金额用橙色粗体突出显示。
保留两位小数。


类别颜色映射

不同类别用不同颜色:

  Color _getCategoryColor(ExpenseCategory category) {
    switch (category) {
      case ExpenseCategory.food: return Colors.green;
      case ExpenseCategory.medical: return Colors.red;
      case ExpenseCategory.grooming: return Colors.pink;
      case ExpenseCategory.toys: return Colors.purple;
      case ExpenseCategory.supplies: return Colors.blue;
      case ExpenseCategory.other: return Colors.grey;
    }
  }

食品用绿色,医疗用红色。
颜色区分让用户快速识别类别。


类别图标映射

不同类别用不同图标:

  IconData _getCategoryIcon(ExpenseCategory category) {
    switch (category) {
      case ExpenseCategory.food: return Icons.restaurant;
      case ExpenseCategory.medical: return Icons.medical_services;
      case ExpenseCategory.grooming: return Icons.content_cut;
      case ExpenseCategory.toys: return Icons.toys;
      case ExpenseCategory.supplies: return Icons.shopping_bag;
      case ExpenseCategory.other: return Icons.more_horiz;
    }
  }
}

图标让类别更直观。
Material Icons提供了丰富的选择。


Dismissible详解

滑动删除组件的使用:

Dismissible(
  key: Key(record.id),
  direction: DismissDirection.endToStart,
  background: Container(...),
  onDismissed: (_) => provider.deleteExpenseRecord(record.id),
  child: Card(...),
)

key必须唯一,用于标识每个项。
direction设置滑动方向。

背景设置:

background: Container(
  color: Colors.red,
  alignment: Alignment.centerRight,
  padding: EdgeInsets.only(right: 16.w),
  child: const Icon(Icons.delete, color: Colors.white),
)

红色背景表示删除操作。
图标靠右对齐,与滑动方向一致。


LinearGradient渐变

创建渐变背景:

decoration: BoxDecoration(
  gradient: LinearGradient(
    colors: [Colors.orange, Colors.orange.shade300],
  ),
)

LinearGradient创建线性渐变。
从深橙色到浅橙色的过渡。


金额格式化

保留两位小数:

total.toStringAsFixed(2)

输出类似"123.45"的格式。
金额显示更规范。


Consumer使用

监听Provider数据变化:

Consumer<CatProvider>(
  builder: (context, provider, child) {
    final records = provider.expenseRecords;
    final totalExpense = provider.getTotalExpenses();
    // 使用数据构建UI
  },
)

builder在数据变化时重新执行。
删除记录后列表自动刷新。


空状态设计

没有记录时的显示:

Center(
  child: Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Icon(Icons.receipt_long, size: 80.sp, color: Colors.grey[300]),
      SizedBox(height: 16.h),
      Text('暂无支出记录', style: TextStyle(color: Colors.grey[600])),
    ],
  ),
)

账单图标暗示支出主题。
灰色调表示空状态。


Column和Expanded

布局组合使用:

Column(
  children: [
    _buildTotalCard(totalExpense),  // 固定高度
    Expanded(
      child: ListView(...),  // 占据剩余空间
    ),
  ],
)

统计卡片高度固定。
Expanded让列表填充剩余空间。


ListTile布局

列表项的标准结构:

ListTile(
  leading: CircleAvatar(...),
  title: Text(record.title),
  subtitle: Text(...),
  trailing: Text(...),
)

四个位置放置不同内容。
自动处理间距和对齐。


日期格式化

简短的日期格式:

DateFormat('MM-dd').format(record.date)

只显示月日,节省空间。
年份通常是当年,不需要显示。


小结

支出列表页面涉及的知识点:

  • Dismissible滑动删除
  • LinearGradient渐变背景
  • Consumer监听数据变化
  • 颜色和图标映射

这些技巧在其他列表页面也能复用。


欢迎加入OpenHarmony跨平台开发社区,一起交流Flutter开发经验:

https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐