news 2026/5/14 9:54:07

Flutter + 开源鸿蒙实战 | 极简记账本 Day3:读取本地账单 + 首页列表展示

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flutter + 开源鸿蒙实战 | 极简记账本 Day3:读取本地账单 + 首页列表展示

🔥 Flutter + 开源鸿蒙实战 | 极简记账本 Day3:读取本地账单 + 首页列表展示

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

系列项目:极简记账本全程实战

功能要点:读取 SharedPreferences 本地存储、JSON 解析、ListView 动态渲染账单列表

📌本文导读(必看)

本文是极简记账本系列第三篇,承接 Day2 的记账页面,核心目标:

  • ✅ 实现「首页账单列表」展示(读取本地存储的所有账单)
  • ✅ 完成 JSON 数据解析,适配 shared_preferences 持久化数据
  • ✅ 实现收支金额颜色区分、空数据默认占位
  • ✅ 保证代码可复用、无冗余,适配鸿蒙多端
  • ✅ 衔接 Day2 框架,为 Day4 收支统计做铺垫

适合人群:Flutter 初学者、练手本地存储、ListView 列表渲染、需要快速迭代项目的学生,全程复制代码可直接运行。

🧱一、Day3 核心任务拆解

  1. 完善 HomePage(首页)UI:列表布局、卡片样式、收支颜色区分
  2. 实现本地数据读取(从 shared_preferences 加载账单列表)
  3. 用 ListView.builder 完成高性能账单列表渲染
  4. 完善空状态适配(无数据时的友好占位文案)
  5. 测试数据同步:新增账单后,首页可自动读取并展示
  6. 兼容鸿蒙与安卓双端,保证样式无错位、数据正常读取

⚙️二、核心知识点回顾

  • shared_preferences:继续使用轻量级本地存储插件,读取 Day2 保存的账单列表数据,适配鸿蒙系统,无需额外适配操作。
  • JSON 编解码:使用 dart:convert 的 jsonDecode,将本地存储的字符串还原为 Dart 列表,是持久化存储的必备环节。
  • ListView.builder:Flutter 官方推荐的高性能列表组件,采用懒加载机制,仅渲染屏幕可见条目,数据量大时也能保持流畅。
  • StatefulWidget:首页需要维护账单列表状态,数据读取完成后需通过 setState 刷新 UI,保证列表实时更新。
  • 条件渲染:通过三目运算符判断列表是否为空,动态切换账单列表与空状态页面,提升用户体验。

🚀三、完整 main.dart 代码

3.1 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, ), home: const MainBottomPage(), ); } } class MainBottomPage extends StatefulWidget { const MainBottomPage({super.key}); @override State<MainBottomPage> createState() => _MainBottomPageState(); } class _MainBottomPageState extends State<MainBottomPage> { int _currentIndex = 0; final List<Widget> _pages = const [ HomePage(), AddBillPage(), StatisticPage(), MinePage(), ]; @override Widget build(BuildContext context) { return Scaffold( body: _pages[_currentIndex], bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, type: BottomNavigationBarType.fixed, selectedItemColor: Colors.teal, unselectedItemColor: Colors.grey, onTap: (index) => setState(() => _currentIndex = index), items: const [ BottomNavigationBarItem(icon: Icon(Icons.home_outlined), label: '首页'), BottomNavigationBarItem(icon: Icon(Icons.add_circle_outline), label: '记账'), BottomNavigationBarItem(icon: Icon(Icons.bar_chart_outlined), label: '统计'), BottomNavigationBarItem(icon: Icon(Icons.person_outlined), 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(); _loadBillData(); } // 读取本地账单 Future<void> _loadBillData() async { SharedPreferences prefs = await SharedPreferences.getInstance(); String? billStr = prefs.getString('billList'); if (billStr != null) { List<dynamic> list = jsonDecode(billStr); setState(() { billList = list.map((e) => e as Map<String, dynamic>).toList(); }); } } @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: (context, index) { var item = billList[index]; return Card( elevation: 3, margin: const EdgeInsets.symmetric(vertical: 6), child: ListTile( title: Text(item['title']), subtitle: Text(item['time']), trailing: Text( "${item['type']} ¥${item['money']}", style: TextStyle( color: item['type'] == '支出' ? Colors.red : Colors.green, fontSize: 16, fontWeight: FontWeight.w500, ), ), ), ); }, ), ); } } // 记账页面 class AddBillPage extends StatefulWidget { const AddBillPage({super.key}); @override State<AddBillPage> createState() => _AddBillPageState(); } class _AddBillPageState extends State<AddBillPage> { final TextEditingController _titleController = TextEditingController(); final TextEditingController _moneyController = TextEditingController(); String _billType = '支出'; Future<void> _saveBill() async { String title = _titleController.text.trim(); String moneyStr = _moneyController.text.trim(); if (title.isEmpty || moneyStr.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请填写完整信息')), ); return; } double? money = double.tryParse(moneyStr); if (money == null || money <= 0) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请输入正确金额')), ); return; } Map<String, dynamic> bill = { 'title': title, 'money': money, 'type': _billType, 'time': DateTime.now().toString().substring(0, 16), }; SharedPreferences prefs = await SharedPreferences.getInstance(); String? billListStr = prefs.getString('billList'); List<Map<String, dynamic>> billList = []; if (billListStr != null) { billList = List<Map<String, dynamic>>.from(jsonDecode(billListStr)); } billList.add(bill); await prefs.setString('billList', jsonEncode(billList)); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('记账成功 ✅')), ); } _titleController.clear(); _moneyController.clear(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('新增记账'), centerTitle: true, ), 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: InputDecoration( hintText: '例如:早餐、购物、工资', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), ), ), const SizedBox(height: 20), const Text("金额", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)), const SizedBox(height: 8), TextField( controller: _moneyController, keyboardType: TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration( hintText: '请输入金额', border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), ), ), const SizedBox(height: 25), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( children: [ Radio( value: '支出', groupValue: _billType, onChanged: (v) { setState(() { _billType = v.toString(); }); }, ), const Text("支出"), ], ), const SizedBox(width: 40), Row( children: [ Radio( value: '收入', groupValue: _billType, onChanged: (v) { setState(() { _billType = v.toString(); }); }, ), const Text("收入"), ], ), ], ), const SizedBox(height: 30), SizedBox( width: double.infinity, height: 50, child: ElevatedButton( onPressed: _saveBill, style: ElevatedButton.styleFrom( backgroundColor: Colors.teal, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), ), child: const Text("保存账单", style: TextStyle(fontSize: 18)), ), ), ], ), ), ); } } class StatisticPage extends StatelessWidget { const StatisticPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('统计')), body: const Center(child: Text('统计页面 - 待开发')), ); } } class MinePage extends StatelessWidget { const MinePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('我的')), body: const Center(child: Text('个人中心 - 待开发')), ); } }
3.2 pubspec.yaml 无需修改(沿用前面配置)

确保依赖已正确配置,若未配置,重新执行:

flutter pub get

📸四、运行效果

  1. 进入首页自动读取本地所有账单
  2. 有数据时以卡片列表逐条展示:用途、时间、收支类型、金额
  3. 支出金额红色、收入金额绿色,区分明显
  4. 无任何账单时,居中显示「暂无账单记录」
  5. 新增记账后,返回首页可自动查看最新数据

🔍五、重点代码解释

1. 页面初始化加载数据

@override void initState() { super.initState(); _loadBillData(); }
  • initState是页面初始化生命周期方法;
  • 页面一加载就自动调用读取本地账单方法,进入就能看到数据。

2. 读取本地存储并解析 JSON

Future<void> _loadBillData() async { SharedPreferences prefs = await SharedPreferences.getInstance(); String? billStr = prefs.getString('billList'); if (billStr != null) { List<dynamic> list = jsonDecode(billStr); setState(() { billList = list.map((e) => e as Map<String, dynamic>).toList(); }); } }
  • 获取SharedPreferences实例,读取 key 为billList的字符串;
  • 通过jsonDecode将字符串转为列表;
  • setState刷新 UI,把解析后的数据赋值给列表变量。

3. 无数据默认占位提示

body: billList.isEmpty ? const Center(child: Text('暂无账单记录')) : ListView.builder(...)
  • 三目运算符判断列表是否为空;
  • 空列表展示默认文案,有数据渲染列表,界面更友好。

4. ListView.builder 动态列表

ListView.builder( itemCount: billList.length, itemBuilder: (context, index) { ... } )
  • 按需加载条目,性能比普通 Column 更好;
  • itemCount指定条目总数,itemBuilder逐条构建每一项 UI。

5. Card + ListTile 账单条目

  • Card卡片带阴影,提升层次感;
  • ListTile快速实现标题、副标题、尾部文字布局;
  • 通过判断收支类型,动态设置文字颜色,视觉区分收支。

✅ 六、Day3 完成总结

今天完成核心功能:

  1. ✅ 实现首页账单列表 UI,包含卡片样式、收支金额颜色区分,适配鸿蒙风格
  2. ✅ 完成本地数据读取,通过shared_preferences加载并解析历史账单
  3. ✅ 用ListView.builder实现高性能懒加载列表,保证滑动流畅
  4. ✅ 完善空状态适配,无账单时显示友好占位提示
  5. ✅ 新增账单后,首页自动刷新列表,实现数据同步
  6. ✅ 代码结构清晰、无冗余,可直接运行,兼容多端

明日预告(Day4):

  • 开发统计页:实现当月总收入 / 总支出汇总
  • 完成收支数据可视化(简易饼图 / 条形图)
  • 优化统计页布局,适配鸿蒙多端显示

📚 七、系列推荐(后续文章)

  • Day1:项目初始化 + 底部导航框架搭建(已发布)
  • Day2:记账页面 + 本地数据持久化(已发布)
  • Day4:统计页数据可视化 + 收支汇总(待更新)
  • Day5:个人中心 + 数据重置 / 清空功能(待更新)
  • Day6:项目优化 + 鸿蒙适配 + 完整项目总结(待更新)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/14 9:51:07

终极指南:ComfyUI-BrushNet图像修复插件深度解析与实战配置

终极指南&#xff1a;ComfyUI-BrushNet图像修复插件深度解析与实战配置 【免费下载链接】ComfyUI-BrushNet ComfyUI BrushNet nodes 项目地址: https://gitcode.com/gh_mirrors/co/ComfyUI-BrushNet 在AI图像修复领域&#xff0c;ComfyUI-BrushNet以其卓越的修复效果和灵…

作者头像 李华
网站建设 2026/5/14 9:50:05

【硬件设计实战】电容选型避坑指南:从参数解析到场景应用

1. 电容参数不是数字游戏&#xff1a;关键指标深度解读 刚入行那会儿&#xff0c;我也以为电容选型就是看容量和耐压两个数。直到有次设计的电源模块批量烧毁&#xff0c;才发现电容参数表里藏着这么多门道。耐压值绝不是简单"大于工作电压"就行&#xff0c;比如钽电…

作者头像 李华
网站建设 2026/5/14 9:50:05

【Linux网络编程】8. 网络层协议 IP

文章目录一、IP 协议1、基本概念2、IP 协议头3、网段划分4、特殊 IP 地址5、IPv4 地址数量限制6、私有 IP 与公网 IP、NAT 技术7、路由与路由表一、IP 协议 网络层核心作用&#xff1a;用 IP 地址寻址&#xff0c;为数据包规划路由&#xff0c;实现跨网段的端到端转发。 1、基…

作者头像 李华
网站建设 2026/5/14 9:49:18

Windows远程桌面终极解锁方案:RDP Wrapper完整使用指南

Windows远程桌面终极解锁方案&#xff1a;RDP Wrapper完整使用指南 【免费下载链接】rdpwrap RDP Wrapper Library 项目地址: https://gitcode.com/gh_mirrors/rd/rdpwrap 还在为Windows家庭版无法使用远程桌面而烦恼吗&#xff1f;RDP Wrapper Library这款开源工具能够…

作者头像 李华