Flutter for OpenHarmony衣橱管家App实战:预算管理实现

买衣服是件开心的事,但如果不控制预算,月底看账单就不那么开心了。衣橱管家App里的预算管理功能,就是帮用户管好买衣服的钱袋子。

今天这篇文章,我来详细讲讲预算管理功能的实现。这个功能包括设置月预算、记录支出、查看预算使用情况等,涉及到数据展示、用户输入、状态管理等多个方面。

功能需求分析

预算管理功能需要实现以下几点:

第一,展示预算概览,包括月预算、已花费、剩余金额、使用百分比。

第二,设置月预算,用户可以输入具体金额或选择预设金额。

第三,记录支出,每次买衣服后记录花了多少钱。

第四,智能提示,根据预算使用情况给出不同的建议。

页面基础结构

先看BudgetScreen的定义:

dart 复制代码
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:percent_indicator/circular_percent_indicator.dart';
import '../../providers/wardrobe_provider.dart';

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

  @override
  State<BudgetScreen> createState() => _BudgetScreenState();
}

class _BudgetScreenState extends State<BudgetScreen> {
  final _budgetController = TextEditingController();
  final _expenseController = TextEditingController();

  @override
  void dispose() {
    _budgetController.dispose();
    _expenseController.dispose();
    super.dispose();
  }
}

用StatefulWidget是因为需要维护两个输入框的状态。

_budgetController控制预算输入框,_expenseController控制支出输入框。

dispose里释放Controller,避免内存泄漏。

percent_indicator包提供圆环进度条组件,用来可视化展示预算使用情况。

页面布局

build方法构建整个页面:

dart 复制代码
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(title: const Text('预算管理')),
    body: Consumer<WardrobeProvider>(
      builder: (context, provider, child) {
        final budget = provider.budget;
        final monthly = budget['monthly'] ?? 2000;
        final spent = budget['spent'] ?? 0;
        final remaining = monthly - spent;
        final percent = monthly > 0 ? (spent / monthly).clamp(0.0, 1.0) : 0.0;

        return SingleChildScrollView(
          padding: EdgeInsets.all(16.w),
          child: Column(
            children: [
              _buildBudgetOverview(monthly, spent, remaining, percent),
              SizedBox(height: 16.h),
              _buildSetBudgetCard(provider, monthly),
              SizedBox(height: 16.h),
              _buildAddExpenseCard(provider),
              SizedBox(height: 16.h),
              _buildTipsCard(percent),
            ],
          ),
        );
      },
    ),
  );
}

Consumer监听WardrobeProvider,预算数据变化时自动更新UI。

从provider.budget里取出月预算和已花费金额,计算剩余金额和使用百分比。

clamp(0.0, 1.0)确保百分比在0到1之间,避免进度条显示异常。

页面分四个部分:预算概览、设置预算、记录支出、智能提示。

预算概览区域

用圆环进度条展示预算使用情况:

dart 复制代码
Widget _buildBudgetOverview(int monthly, int spent, int remaining, double percent) {
  return Card(
    child: Padding(
      padding: EdgeInsets.all(24.w),
      child: Column(
        children: [
          CircularPercentIndicator(
            radius: 80.r,
            lineWidth: 12.w,
            percent: percent,
            center: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('${(percent * 100).toInt()}%', style: TextStyle(fontSize: 24.sp, fontWeight: FontWeight.bold)),
                Text('已使用', style: TextStyle(fontSize: 12.sp, color: Colors.grey)),
              ],
            ),
            progressColor: percent > 0.8 ? Colors.red : (percent > 0.5 ? Colors.orange : const Color(0xFFE91E63)),
            backgroundColor: Colors.grey.shade200,
            circularStrokeCap: CircularStrokeCap.round,
          ),
          SizedBox(height: 24.h),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceAround,
            children: [
              _buildBudgetItem('月预算', '¥$monthly', Colors.blue),
              _buildBudgetItem('已花费', '¥$spent', Colors.orange),
              _buildBudgetItem('剩余', '¥$remaining', remaining >= 0 ? Colors.green : Colors.red),
            ],
          ),
        ],
      ),
    ),
  );
}

CircularPercentIndicator是圆环进度条,radius是半径,lineWidth是线宽。

center属性可以在圆环中间放置Widget,这里放百分比数字和"已使用"文字。

progressColor根据使用百分比变化:超过80%是红色警告,超过50%是橙色提醒,否则是主题色。

circularStrokeCap.round让进度条两端是圆角,看起来更柔和。

下方三个数据项横向排列,展示月预算、已花费、剩余金额。

预算数据项的构建方法:

dart 复制代码
Widget _buildBudgetItem(String label, String value, Color color) {
  return Column(
    children: [
      Text(value, style: TextStyle(fontSize: 18.sp, fontWeight: FontWeight.bold, color: color)),
      SizedBox(height: 4.h),
      Text(label, style: TextStyle(fontSize: 12.sp, color: Colors.grey)),
    ],
  );
}

数值在上,标签在下,数值用大字体粗体,标签用小字体灰色。

每个数据项有自己的颜色,月预算蓝色,已花费橙色,剩余绿色或红色。

剩余金额为负数时用红色,提醒用户已经超支了。

设置预算卡片

用户可以输入金额或选择预设金额:

dart 复制代码
Widget _buildSetBudgetCard(WardrobeProvider provider, int currentBudget) {
  return Card(
    child: Padding(
      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('设置月预算', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
          SizedBox(height: 12.h),
          Row(
            children: [
              Expanded(
                child: TextField(
                  controller: _budgetController,
                  keyboardType: TextInputType.number,
                  decoration: InputDecoration(
                    hintText: '输入预算金额',
                    prefixText: '¥ ',
                    border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
                  ),
                ),
              ),
              SizedBox(width: 12.w),
              ElevatedButton(
                onPressed: () {
                  final amount = int.tryParse(_budgetController.text);
                  if (amount != null && amount > 0) {
                    provider.updateBudget(amount);
                    _budgetController.clear();
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('预算已更新')));
                  }
                },
                style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFFE91E63)),
                child: const Text('设置', style: TextStyle(color: Colors.white)),
              ),
            ],
          ),
          SizedBox(height: 8.h),
          Wrap(
            spacing: 8.w,
            children: [1000, 2000, 3000, 5000].map((amount) {
              return ActionChip(
                label: Text('¥$amount'),
                onPressed: () {
                  provider.updateBudget(amount);
                  ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('预算已设置为¥$amount')));
                },
              );
            }).toList(),
          ),
        ],
      ),
    ),
  );
}

输入框和设置按钮横向排列,用Row和Expanded实现。

keyboardType: TextInputType.number弹出数字键盘,方便输入金额。

prefixText: '¥ '在输入框前面显示人民币符号,用户知道输入的是金额。

int.tryParse安全地把字符串转成整数,转换失败返回null而不是抛异常。

下方的ActionChip是预设金额快捷按钮,点击直接设置对应金额,省去输入的麻烦。

记录支出卡片

每次买衣服后记录花费:

dart 复制代码
Widget _buildAddExpenseCard(WardrobeProvider provider) {
  return Card(
    child: Padding(
      padding: EdgeInsets.all(16.w),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('记录支出', style: TextStyle(fontSize: 16.sp, fontWeight: FontWeight.bold)),
          SizedBox(height: 12.h),
          Row(
            children: [
              Expanded(
                child: TextField(
                  controller: _expenseController,
                  keyboardType: TextInputType.number,
                  decoration: InputDecoration(
                    hintText: '输入支出金额',
                    prefixText: '¥ ',
                    border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.r)),
                  ),
                ),
              ),
              SizedBox(width: 12.w),
              ElevatedButton(
                onPressed: () {
                  final amount = int.tryParse(_expenseController.text);
                  if (amount != null && amount > 0) {
                    provider.addExpense(amount);
                    _expenseController.clear();
                    ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('支出已记录')));
                  }
                },
                style: ElevatedButton.styleFrom(backgroundColor: Colors.orange),
                child: const Text('记录', style: TextStyle(color: Colors.white)),
              ),
            ],
          ),
        ],
      ),
    ),
  );
}

结构和设置预算卡片类似,但按钮颜色用橙色,和设置按钮区分开。

记录支出后清空输入框,显示SnackBar提示。

provider.addExpense方法会把支出金额加到已花费里。

智能提示卡片

根据预算使用情况给出不同的建议:

dart 复制代码
Widget _buildTipsCard(double percent) {
  String tip;
  IconData icon;
  Color color;

  if (percent < 0.5) {
    tip = '预算使用良好,继续保持理性消费!';
    icon = Icons.thumb_up;
    color = Colors.green;
  } else if (percent < 0.8) {
    tip = '预算已过半,建议控制后续支出。';
    icon = Icons.info;
    color = Colors.orange;
  } else {
    tip = '预算即将用完,请谨慎消费!';
    icon = Icons.warning;
    color = Colors.red;
  }

  return Card(
    color: color.withOpacity(0.1),
    child: Padding(
      padding: EdgeInsets.all(16.w),
      child: Row(
        children: [
          Icon(icon, color: color, size: 32.sp),
          SizedBox(width: 12.w),
          Expanded(child: Text(tip, style: TextStyle(fontSize: 14.sp, color: color))),
        ],
      ),
    ),
  );
}

根据使用百分比分三档:50%以下是绿色鼓励,50%-80%是橙色提醒,80%以上是红色警告。

每档有对应的图标、颜色、提示文字,视觉上很直观。

卡片背景色是提示颜色的浅色版本,和文字颜色呼应。

Expanded包裹Text,防止文字太长时溢出。

Provider里的预算方法

WardrobeProvider里需要实现预算相关的方法:

dart 复制代码
// WardrobeProvider里的预算相关代码
Map<String, int> _budget = {'monthly': 2000, 'spent': 0};

Map<String, int> get budget => _budget;

void updateBudget(int amount) {
  _budget['monthly'] = amount;
  notifyListeners();
}

void addExpense(int amount) {
  _budget['spent'] = (_budget['spent'] ?? 0) + amount;
  notifyListeners();
}

void resetMonthlySpent() {
  _budget['spent'] = 0;
  notifyListeners();
}

_budget用Map存储,包含monthly(月预算)和spent(已花费)两个字段。

updateBudget更新月预算金额。

addExpense把新支出加到已花费里。

resetMonthlySpent重置已花费为0,可以在每月初调用。

每个方法最后都调用notifyListeners(),通知UI更新。

数据持久化

实际项目中,预算数据应该保存到本地存储:

dart 复制代码
// 使用shared_preferences保存预算数据
import 'package:shared_preferences/shared_preferences.dart';

Future<void> _saveBudget() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.setInt('budget_monthly', _budget['monthly'] ?? 2000);
  await prefs.setInt('budget_spent', _budget['spent'] ?? 0);
}

Future<void> _loadBudget() async {
  final prefs = await SharedPreferences.getInstance();
  _budget = {
    'monthly': prefs.getInt('budget_monthly') ?? 2000,
    'spent': prefs.getInt('budget_spent') ?? 0,
  };
  notifyListeners();
}

shared_preferences是Flutter常用的本地存储插件。

每次更新预算后调用_saveBudget保存到本地。

App启动时调用_loadBudget加载之前保存的数据。

默认月预算2000元,已花费0元。

输入验证

用户输入需要做验证:

dart 复制代码
final amount = int.tryParse(_budgetController.text);
if (amount != null && amount > 0) {
  provider.updateBudget(amount);
  _budgetController.clear();
  ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('预算已更新')));
}

int.tryParse把字符串转成整数,如果输入的不是数字,返回null。

amount > 0确保金额是正数,不能设置0或负数的预算。

验证通过后才执行更新操作,否则什么都不做。

可以考虑加个else分支,提示用户输入无效。

更完善的验证:

dart 复制代码
void _setBudget(WardrobeProvider provider) {
  final text = _budgetController.text.trim();
  if (text.isEmpty) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('请输入预算金额')),
    );
    return;
  }
  
  final amount = int.tryParse(text);
  if (amount == null) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('请输入有效的数字')),
    );
    return;
  }
  
  if (amount <= 0) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('预算金额必须大于0')),
    );
    return;
  }
  
  provider.updateBudget(amount);
  _budgetController.clear();
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(content: Text('预算已更新')),
  );
}

分步验证,每种错误情况给出不同的提示。

trim()去掉首尾空格,避免用户不小心输入空格。

这样用户知道为什么操作没有成功,体验更好。

进度条颜色的设计

进度条颜色根据使用百分比变化:

dart 复制代码
progressColor: percent > 0.8 ? Colors.red : (percent > 0.5 ? Colors.orange : const Color(0xFFE91E63))

80%以上用红色,表示预算快用完了,需要警惕。

50%-80%用橙色,表示预算已过半,需要注意。

50%以下用主题色,表示预算使用正常。

这种颜色变化让用户一眼就能看出预算使用情况。

总结

预算管理功能的实现涉及到数据展示、用户输入、状态管理、数据持久化等多个方面。关键点在于:

用圆环进度条直观展示预算使用情况。

提供输入框和快捷按钮两种设置方式,满足不同用户的习惯。

根据使用百分比给出智能提示,帮助用户控制消费。

在OpenHarmony平台上,这套实现方式完全适用。预算管理是一个很实用的功能,能帮助用户养成理性消费的习惯。

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

相关推荐
Remember_9932 小时前
Spring 核心原理深度解析:Bean 作用域、生命周期与 Spring Boot 自动配置
java·前端·spring boot·后端·spring·面试
2501_944448002 小时前
Flutter for OpenHarmony衣橱管家App实战:意见反馈功能实现
android·javascript·flutter
笨蛋不要掉眼泪2 小时前
Redis持久化解析:RDB和AOF的对比
前端·javascript·redis
心.c2 小时前
Vue3+Node.js实现文件上传分片上传和断点续传【详细教程】
前端·javascript·vue.js·算法·node.js·哈希算法
We་ct2 小时前
LeetCode 48. 旋转图像:原地旋转最优解法
前端·算法·leetcode·typescript
妙团团2 小时前
React学习之自定义tab组合组件
javascript·学习·react.js
kirk_wang2 小时前
Flutter艺术探索-Flutter推送通知:local_notifications与firebase_messaging
flutter·移动开发·flutter教程·移动开发教程
2601_949809592 小时前
flutter_for_openharmony家庭相册app实战+隐私设置实现
android·javascript·flutter
2601_949593652 小时前
React Native 鸿蒙跨平台开发:LinearGradient 渐变动画效果
javascript·react native·react.js