Flutter + 开源鸿蒙实战 | 极简记账本 Day3:读取本地账单 + 首页列表展示
Flutter 开源鸿蒙实战 | 极简记账本 Day3读取本地账单 首页列表展示欢迎加入开源鸿蒙跨平台社区https://openharmonycrossplatform.csdn.net系列项目极简记账本全程实战功能要点读取 SharedPreferences 本地存储、JSON 解析、ListView 动态渲染账单列表本文导读必看本文是极简记账本系列第三篇承接 Day2 的记账页面核心目标✅ 实现「首页账单列表」展示读取本地存储的所有账单✅ 完成 JSON 数据解析适配 shared_preferences 持久化数据✅ 实现收支金额颜色区分、空数据默认占位✅ 保证代码可复用、无冗余适配鸿蒙多端✅ 衔接 Day2 框架为 Day4 收支统计做铺垫适合人群Flutter 初学者、练手本地存储、ListView 列表渲染、需要快速迭代项目的学生全程复制代码可直接运行。一、Day3 核心任务拆解完善 HomePage首页UI列表布局、卡片样式、收支颜色区分实现本地数据读取从 shared_preferences 加载账单列表用 ListView.builder 完成高性能账单列表渲染完善空状态适配无数据时的友好占位文案测试数据同步新增账单后首页可自动读取并展示兼容鸿蒙与安卓双端保证样式无错位、数据正常读取⚙️二、核心知识点回顾shared_preferences继续使用轻量级本地存储插件读取 Day2 保存的账单列表数据适配鸿蒙系统无需额外适配操作。JSON 编解码使用 dart:convert 的 jsonDecode将本地存储的字符串还原为 Dart 列表是持久化存储的必备环节。ListView.builderFlutter 官方推荐的高性能列表组件采用懒加载机制仅渲染屏幕可见条目数据量大时也能保持流畅。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 StateMainBottomPage createState() _MainBottomPageState(); } class _MainBottomPageState extends StateMainBottomPage { int _currentIndex 0; final ListWidget _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 StateHomePage createState() _HomePageState(); } class _HomePageState extends StateHomePage { ListMapString, dynamic billList []; override void initState() { super.initState(); _loadBillData(); } // 读取本地账单 Futurevoid _loadBillData() async { SharedPreferences prefs await SharedPreferences.getInstance(); String? billStr prefs.getString(billList); if (billStr ! null) { Listdynamic list jsonDecode(billStr); setState(() { billList list.map((e) e as MapString, 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 StateAddBillPage createState() _AddBillPageState(); } class _AddBillPageState extends StateAddBillPage { final TextEditingController _titleController TextEditingController(); final TextEditingController _moneyController TextEditingController(); String _billType 支出; Futurevoid _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; } MapString, 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); ListMapString, dynamic billList []; if (billListStr ! null) { billList ListMapString, 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. 页面初始化加载数据override void initState() { super.initState(); _loadBillData(); }initState是页面初始化生命周期方法页面一加载就自动调用读取本地账单方法进入就能看到数据。2. 读取本地存储并解析 JSONFuturevoid _loadBillData() async { SharedPreferences prefs await SharedPreferences.getInstance(); String? billStr prefs.getString(billList); if (billStr ! null) { Listdynamic list jsonDecode(billStr); setState(() { billList list.map((e) e as MapString, 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 完成总结今天完成核心功能✅ 实现首页账单列表 UI包含卡片样式、收支金额颜色区分适配鸿蒙风格✅ 完成本地数据读取通过shared_preferences加载并解析历史账单✅ 用ListView.builder实现高性能懒加载列表保证滑动流畅✅ 完善空状态适配无账单时显示友好占位提示✅ 新增账单后首页自动刷新列表实现数据同步✅ 代码结构清晰、无冗余可直接运行兼容多端明日预告Day4开发统计页实现当月总收入 / 总支出汇总完成收支数据可视化简易饼图 / 条形图优化统计页布局适配鸿蒙多端显示 七、系列推荐后续文章Day1项目初始化 底部导航框架搭建已发布Day2记账页面 本地数据持久化已发布Day4统计页数据可视化 收支汇总待更新Day5个人中心 数据重置 / 清空功能待更新Day6项目优化 鸿蒙适配 完整项目总结待更新