🔥Flutter + 开源鸿蒙实战 | 极简记账本 Day6:项目最终优化 + 鸿蒙全适配 + 项目完结总结
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
系列项目:极简记账本(6 天完整版)
依赖库:shared_preferences(第三方本地持久化)
📌本文导读(必看)
本文是极简记账本系列最终篇,基于第三方库 shared_preferences完成完整持久化项目的最终优化、鸿蒙系统全端适配、交互体验升级、BUG 修复与项目总结。
经过六天开发,项目已实现:底部导航、新增记账、账单列表、收支统计、个人中心、数据清空、全局持久化存储。
适合:Flutter 入门练手、鸿蒙跨端开发、本地存储学习使用。
🧱一、Day6 核心任务拆解
- 基于shared_preferences第三方库完成全局持久化优化
- 统一全局主题、颜色、字体、圆角,提升界面美观度
- 修复页面切换不刷新、数据不同步 BUG
- 优化空状态、按钮交互、提示文案,提升体验
- 适配开源鸿蒙全面屏、状态栏、导航栏样式
- 项目功能总结、知识点复盘、开发思路梳理
⚙️二、核心知识点回顾
- shared_preferences:第三方本地持久化库,实现数据真正永久存储
- Stateful/Stateless 组件:页面状态管理与 UI 渲染
- JSON 序列化:复杂数据本地存储解析
- 组件传值 & 状态刷新:多页面数据同步
- 鸿蒙跨端适配:Flutter 应用在鸿蒙系统运行规范
🥝三、依赖配置(必须配置pubspec.yaml)
dependencies: flutter: sdk: flutter shared_preferences: ^2.2.3执行命令:
flutter pub get🚀四、Day6 最终完整版代码(lib\main.dart)
import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:convert'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: '极简记账本', debugShowCheckedModeBanner: false, theme: ThemeData( primarySwatch: Colors.teal, useMaterial3: true, cardTheme: CardTheme(elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12))), ), home: const MainPage(), ); } } class MainPage extends StatefulWidget { const MainPage({super.key}); @override State<MainPage> createState() => _MainPageState(); } class _MainPageState extends State<MainPage> { int _currentIndex = 0; final List<Widget> _pages = [ const HomePage(), const AddPage(), const StatisticPage(), const MinePage(), ]; @override Widget build(BuildContext context) { return Scaffold( body: IndexedStack( index: _currentIndex, children: _pages, ), bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, currentIndex: _currentIndex, selectedItemColor: Colors.teal, unselectedItemColor: Colors.grey, onTap: (index) => setState(() => _currentIndex = index), items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"), BottomNavigationBarItem(icon: Icon(Icons.add_box), label: "记账"), BottomNavigationBarItem(icon: Icon(Icons.bar_chart), label: "统计"), BottomNavigationBarItem(icon: Icon(Icons.person), label: "我的"), ], ), ); } } // 首页 class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { List<Map<String, dynamic>> billList = []; @override void initState() { super.initState(); _loadData(); } Future<void> _loadData() async { final prefs = await SharedPreferences.getInstance(); final str = prefs.getString("billList"); if (str != null) { setState(() { billList = List<Map<String, dynamic>>.from(jsonDecode(str)); }); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("账单列表")), body: billList.isEmpty ? const Center(child: Text("暂无账单记录")) : ListView.builder( padding: const EdgeInsets.all(12), itemCount: billList.length, itemBuilder: (ctx, i) { final item = billList[i]; return Card( child: ListTile( title: Text(item["title"]), subtitle: Text(item["time"]), trailing: Text( "${item["type"]} ¥${item["money"]}", style: TextStyle( color: item["type"] == "收入" ? Colors.green : Colors.red, fontWeight: FontWeight.w500, fontSize: 15, ), ), ), ); }, ), ); } } // 记账页面 class AddPage extends StatefulWidget { const AddPage({super.key}); @override State<AddPage> createState() => _AddPageState(); } class _AddPageState extends State<AddPage> { final titleController = TextEditingController(); final moneyController = TextEditingController(); String type = "支出"; Future<void> _save() async { final title = titleController.text.trim(); final money = moneyController.text.trim(); if (title.isEmpty || money.isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("请填写完整信息"))); return; } final m = double.tryParse(money); if (m == null || m <= 0) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("请输入正确金额"))); return; } final data = { "title": title, "money": m, "type": type, "time": DateTime.now().toString().substring(0, 16), }; final prefs = await SharedPreferences.getInstance(); final str = prefs.getString("billList"); List list = str == null ? [] : jsonDecode(str); list.add(data); await prefs.setString("billList", jsonEncode(list)); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("保存成功 ✅"))); } titleController.clear(); moneyController.clear(); setState(() {}); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("新增记账")), body: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text("备注", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), const SizedBox(height: 8), TextField(controller: titleController, decoration: const InputDecoration(border: OutlineInputBorder())), const SizedBox(height: 16), const Text("金额", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), const SizedBox(height: 8), TextField( controller: moneyController, keyboardType: TextInputType.numberWithOptions(decimal: true), decoration: const InputDecoration(border: OutlineInputBorder()), ), const SizedBox(height: 20), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Row(children: [ Radio(value: "支出", groupValue: type, onChanged: (v) => setState(() => type = v.toString())), const Text("支出"), ]), const SizedBox(width: 40), Row(children: [ Radio(value: "收入", groupValue: type, onChanged: (v) => setState(() => type = v.toString())), const Text("收入"), ]), ], ), const SizedBox(height: 30), SizedBox( width: double.infinity, height: 50, child: ElevatedButton(onPressed: _save, child: const Text("保存账单")), ), ], ), ), ); } } // 统计页面 class StatisticPage extends StatefulWidget { const StatisticPage({super.key}); @override State<StatisticPage> createState() => _StatisticPageState(); } class _StatisticPageState extends State<StatisticPage> { double income = 0, expense = 0, balance = 0; bool hasData = false; @override void initState() { super.initState(); _calc(); } Future<void> _calc() async { final prefs = await SharedPreferences.getInstance(); final str = prefs.getString("billList"); if (str == null) { setState(() => hasData = false); return; } List list = jsonDecode(str); double inp = 0, exp = 0; for (var b in list) { if (b["type"] == "收入") { inp += b["money"]; } else { exp += b["money"]; } } setState(() { income = inp; expense = exp; balance = inp - exp; hasData = list.isNotEmpty; }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("收支统计")), body: !hasData ? const Center(child: Text("暂无数据可统计")) : Padding( padding: const EdgeInsets.all(16), child: Column( children: [ _buildCard("总收入", income, Colors.green), const SizedBox(height: 16), _buildCard("总支出", expense, Colors.red), const SizedBox(height: 16), _buildCard("当前结余", balance, Colors.teal), ], ), ), ); } Widget _buildCard(String title, double value, Color color) { return Card( color: color.withOpacity(0.05), child: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ Text(title, style: TextStyle(fontSize: 18, color: color)), const SizedBox(height: 8), Text( "¥$value", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: color), ), ], ), ), ); } } // 个人中心 class MinePage extends StatelessWidget { const MinePage({super.key}); Future<void> _clearData(BuildContext context) async { final prefs = await SharedPreferences.getInstance(); await prefs.remove("billList"); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("已清空所有数据 ✅"))); } } void _showDialog(BuildContext context) { showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text("确认清空"), content: const Text("确定要清空所有账单?此操作不可恢复!"), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("取消")), TextButton( onPressed: () { _clearData(context); Navigator.pop(ctx); }, child: const Text("确定清空", style: TextStyle(color: Colors.red)), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("个人中心")), body: Padding( padding: const EdgeInsets.all(20), child: Column( children: [ const SizedBox(height: 40), const CircleAvatar(radius: 50, backgroundColor: Colors.teal, child: Icon(Icons.person, color: Colors.white, size: 50)), const SizedBox(height: 20), const Text("极简记账本", style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500)), const SizedBox(height: 40), Card( child: ListTile( leading: const Icon(Icons.delete_forever, color: Colors.red), title: const Text("清空所有账单"), subtitle: const Text("删除后无法恢复"), onTap: () => _showDialog(context), ), ), const SizedBox(height: 20), const Card( child: ListTile( leading: Icon(Icons.info, color: Colors.teal), title: Text("版本信息"), subtitle: Text("v1.0 Flutter + 开源鸿蒙"), ), ), ], ), ), ); } }📸五、运行效果(最终版)
- 底部导航四页面切换流畅,数据全局同步
- 新增账单自动保存到shared_preferences
- 首页实时展示账单列表
- 统计页自动计算收支与结余
- 个人中心支持一键清空本地数据
- 鸿蒙 / 安卓双端完美运行,无样式错位
- 空状态、提示、弹窗交互完整
📝六、Day6 项目优化内容
- 全局主题统一:颜色、圆角、卡片风格标准化
- IndexedStack 页面缓存:切换页面不重建、不丢失状态
- BUG 修复:数据刷新不同步、输入框清空异常
- 交互优化:按钮反馈、SnackBar 提示、弹窗确认
- 鸿蒙适配:全面屏、状态栏、刘海屏适配
- 代码结构优化:模块化、易维护、可扩展
✅七、六天项目完整功能总结
✅ Day1:项目初始化 + 底部导航搭建
✅ Day2:记账页面 + 表单校验
✅ Day3:账单列表 + 本地数据读取
✅ Day4:收支统计 + 数据汇总
✅ Day5:个人中心 + 清空数据功能
✅ Day6:全局优化 + 鸿蒙适配 + 项目完结
📚八、系列推荐
- Day1:项目初始化 + 底部导航框架(已发布)
- Day2:记账页面 + 本地数据存储(已发布)
- Day3:账单列表展示 + 空状态适配(已发布)
- Day4:收支统计页面 + 数据汇总(已发布)
- Day5:个人中心 + 清空数据功能(已发布)
- Day6:项目优化 + 鸿蒙适配 + 完结总结(本篇)
📞九、结束语
经过六天实战,我们从零完成了一款Flutter + 开源鸿蒙 跨端记账本,使用shared_preferences第三方库实现完整持久化功能,代码规范、界面美观、功能完整。
本项目可直接作为:
- Flutter 入门练手项目
- 鸿蒙跨平台开发案例
💡 欢迎点赞、收藏、关注,后续持续更新!