安卓记账本APP课程设计:从SQLite存储到图表展示的完整实现

发布时间:2026/10/9 15:39:55
安卓记账本APP课程设计:从SQLite存储到图表展示的完整实现
简介面向计算机相关专业学生的安卓开发课程设计高分项目提供记账本APP完整源代码与配套文档说明适合作为期末大作业或课程设计参考。项目经导师指导并获98分评审功能围绕日常记账场景展开涵盖账目录入、记录管理、数据展示等核心需求可用于理解Android界面搭建、数据存储与业务逻辑分层实现。压缩包共103个文件以52个xml布局与配置文件、22个java源码文件为主辅以14张png图片及gradle构建脚本等整体仅1.64MB结构清晰便于阅读和二次开发。内容预览显示其中既有Activity/Fragment等Java业务代码也有构建配置与资源适配文件典型Android工程组织方式一目了然。目前已有1002人学习下载适合正在完成大作业或希望进行项目实战练习的开发者参考借鉴。资源内附文档说明有助于快速梳理项目模块与设计思路对提升Android开发实践能力有直接帮助。1. 记账本APP课程设计为什么说它是安卓期末最稳的高分选题临近安卓课程设计提交很多人纠结选什么题目。写外卖APP怕功能做不完写校园助手又怕答辩被追问细节问倒。记账本APP看起来朴素反而是拿高分概率最高的选题之一需求边界天然清晰数据模型只有「一笔账、一个分类、一个日期」完全不需要网络、登录、后端这些容易翻车的依赖。高分的关键不是用了多少新技术而是完成度——能不能做到「记一笔账、列表看得到、统计算得对、图表展示得出来」这套闭环。这篇笔记从需求拆解、SQLite存储实现、界面与图表到答辩前的避坑清单完整展开怎么做出一份能稳稳过评、经得起追问的记账本课程设计。2. 需求拆解与架构选型把「记账本」做成拿分结构课程设计评分看的不是你会多少花哨技术而是「功能是否完整闭环、代码是否规范、答辩能否自圆其说」。我的习惯是动手写代码前先把功能清单写在纸上逐条标注必须有、可加分、坚决不做。记账本的功能边界特别清晰这一步做好了后面每个模块写起来都有章法。2.1 功能模块与优先级先完成闭环再谈加分记账本的必要功能数来数去就五块记一笔账支出/收入、分类选择、账单流水列表、统计报表、账单的编辑与删除。预算提醒、数据导出属于加分项。登录、云同步、多用户这类功能在实际课程设计里反而是负担功能越重需要自圆其说的点越多答辩翻车概率越高。第一优先级必须完成 - 记一笔账金额、分类、日期、备注 - 首页按时间倒序展示账单流水 - 支持编辑和删除一笔账 - 按月份统计总支出/总收入 - 按分类统计支出占比 第二优先级加分 - 预算设置与超支提醒 - 分类管理允许自定义新增分类 - 账单导出为JSON或文本备份 明确不做避免答辩翻车 - 用户注册登录 - 云端同步 - 多端联动有同学一上来就做分类管理、预算提醒、图表动画结果最基本的「记一笔账然后首页能显示出来」都没跑通这就是典型的没闭环。我一般按「端到端验收路径」来定开发顺序先完成记一笔 → 首页可见 → 月度统计正确 → 图表展示 → 删除后统计同步变化。这条链路全通了项目就已经有七十分后面的预算、导出都是锦上添花。2.2 存储选型为什么是 SQLite 而不是 Room 或云端记账本的核心数据是账单天然适合关系型存储。选型时我直接排除了文件存储和云端主要在 SQLite 和 Room 之间比较。Room 是官方ORM框架功能确实强但它的注解处理器和编译环境的版本兼容问题不少一旦和当前 compileSdk 对不上编译期报错能折腾掉好几天。课程设计周期通常只有两三周把时间赌在环境配置上不划算。方案依赖与配置聚合查询能力答辩风险推荐度SQLite系统内置零依赖强支持 GROUP BY低最推荐Room需要注解处理器强中版本兼容问题可选SharedPreferences/文件无弱全量读入手动算低不推荐云端后端需要网络权限和服务端取决于后端高会被追问部署不推荐SQLite 最实在的优势是系统内置不需要额外依赖SQL 能力足够完成按月聚合、按分类分组这些统计需求。云端方案最大的坑在答辩老师一定会问「后端谁部署的挂了怎么办数据安全怎么保证」这些问题对一个期末项目来说是致命的。记账本身是单机高频操作数据不出本地反而更符合个人账本的隐私预期。2.3 包结构与命名规范让老师一眼看出工程素养很多同学习惯把 MainActivity 当仓库用数据库操作、业务逻辑、界面刷新全部堆在一个类里一个文件写了上千行。老师看到这种结构印象分基本就低了。我一般会按 model、db、ui、util 四层来组织工程职责清晰答辩时按包结构讲一遍比背一万字文档都有说服力。app/src/main/java/com/example/bookkeeping/ ├── model/ // 数据模型RecordBean、CategoryBean、StatItem ├── db/ // 数据库DBHelper、RecordDao ├── ui/ │ ├── activity/ // 主界面、记账输入页、统计页 │ ├── adapter/ // 账单列表适配器 │ └── dialog/ // 日期选择、分类选择弹窗 ├── util/ // 金额格式化、日期工具类 └── MainActivity.java命名规范同样影响观感。数据类统一用 Bean 后缀数据库帮助类叫 DBHelper数据访问层叫 RecordDao一眼能看出职责。布局文件加前缀区分activity_main.xml、item_record.xml、dialog_category.xml。资源里的颜色、字符串不要散落各处集中放在 values 目录下统一管理。另外一个加分细节是AndroidManifest 里只声明必要权限记账本这种单机应用完全可以做到一个运行时权限都不要答辩被问「为什么不需要网络权限」时这是个很漂亮的答案。3. SQLite 记账核心建表、CRUD、统计的完整实现数据层是记账本的心脏。我见过不少项目把建表语句写在 Activity 的 onCreate 里每次进入界面都重新执行一遍这种代码跑起来不出错是运气出错是必然。正确做法是单独建一个数据库帮助类把建表、升级、增删改查都收敛到统一入口。3.1 建表和金额单位用「分」代替「元」先讲一个决定数据准确性的关键设计金额字段用整数「分」而不是浮点数「元」。0.1 加 0.2 在浮点运算里是 0.30000000000000004记一百笔小额开销月底统计差几毛钱答辩现场一对账就对不上属于硬伤。把「元」转成「分」存整型展示时再转回「元」精度问题从设计层面就消失了。CREATE TABLE record ( id INTEGER PRIMARY KEY AUTOINCREMENT, type INTEGER NOT NULL, -- 0支出1收入 category_id INTEGER NOT NULL, -- 关联分类表 amount_cents INTEGER NOT NULL, -- 金额单位分避免浮点误差 note TEXT, -- 备注可为空 record_date TEXT NOT NULL, -- 业务日期固定格式 yyyy-MM-dd create_time INTEGER NOT NULL -- 记录创建时间戳毫秒 ); CREATE TABLE category ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, type INTEGER NOT NULL, -- 0支出分类1收入分类 sort_order INTEGER DEFAULT 0 ); CREATE INDEX idx_record_date ON record(record_date);record_date 存成固定长度的字符串而不是时间戳是为了方便后面用字符串比较做区间查询写法直观不容易错。create_time 单独存自增时间戳负责同一天内多笔账单的顺序排列。给 record_date 建索引是个容易被忽略但很加分的细节说明你考虑到了统计查询的效率。字段类型说明idINTEGER PRIMARY KEY AUTOINCREMENT主键自增typeINTEGER0 支出1 收入category_idINTEGER关联 category 表amount_centsINTEGER金额单位分noteTEXT备注record_dateTEXT业务日期yyyy-MM-ddcreate_timeINTEGER创建时间戳毫秒决定当天排序3.2 写入与事务一笔账怎么安全落库数据库帮助类继承 SQLiteOpenHelper在 onCreate 里执行建表语句。课程设计阶段 onUpgrade 可以直接 DROP 掉旧表再重建开发期最省事。但如果想体现工程质量我建议按版本号做增量升级比如新增预算表时用单独一条 CREATE TABLE IF NOT EXISTS 处理避免用户升级应用后数据被清空。public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME account.db; private static final int DB_VERSION 1; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(SQL_CREATE_RECORD); db.execSQL(SQL_CREATE_CATEGORY); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 开发期可以全部重建正式一点的写法是按版本增量升级 if (oldVersion 2) { db.execSQL(CREATE TABLE IF NOT EXISTS budget(...)); } } }单笔记账插入用 ContentValues 和 insert 方法就够了不需要手动开事务。真正的重点在批量数据操作比如初始化内置分类列表、批量导入账单时必须用事务包住保证要么全部成功要么全部回滚。public long insertRecord(RecordBean bean) { SQLiteDatabase db dbHelper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(type, bean.getType()); values.put(category_id, bean.getCategoryId()); values.put(amount_cents, bean.getAmountCents()); values.put(note, bean.getNote()); values.put(record_date, bean.getRecordDate()); values.put(create_time, System.currentTimeMillis()); return db.insert(record, null, values); } public void insertRecordsBatch(ListRecordBean list) { SQLiteDatabase db dbHelper.getWritableDatabase(); db.beginTransaction(); try { for (RecordBean bean : list) { long id insertRecord(bean); if (id -1) { throw new SQLException(insert failed: bean.toString()); } } db.setTransactionSuccessful(); } finally { db.endTransaction(); } }insert 方法返回 long 类型的主键 id这个返回值很关键后续编辑、删除、统计定位都要靠它。事务的正确姿势是 beginTransaction 和 endTransaction 成对出现setTransactionSuccessful 之后 end 才会提交finally 里 end 保证异常时也能正常回滚。insertRecordsBatch 里我特意判断了 id -1insert 失败时 SQLite 返回 -1这样能及时发现写入异常而不是带着问题继续往下跑。3.3 统计查询按月、按分类的聚合 SQL统计是记账本区别于普通记事本的核心能力也是老师最容易直观评估工作量的模块。月度收支汇总用 GROUP BY 一行就能算出来这种表达力是文件存储很难比的。-- 某月总支出、总收入 SELECT type, SUM(amount_cents) AS total FROM record WHERE record_date 2025-06-01 AND record_date 2025-07-01 GROUP BY type; -- 某月支出按分类占比 SELECT c.name AS category_name, SUM(r.amount_cents) AS total FROM record r LEFT JOIN category c ON r.category_id c.id WHERE r.type 0 AND r.record_date LIKE 2025-06% GROUP BY r.category_id ORDER BY total DESC;第一个查询用大于等于和小于做区间过滤而不是 BETWEEN是因为 BETWEEN 是闭区间边界日期很容易记错。第二个查询用 LEFT JOIN 关联分类表取分类名称而不是 category_id这样图表上可以直接展示中文标签。注意 record_date 能安全使用 LIKE 的前提前提是所有日期都是固定格式的 yyyy-MM-dd不会混入带时分秒的字符串这一点在建表时就定死了。public ListStatItem queryMonthStats(String month) { ListStatItem list new ArrayList(); String start month -01; String end month -99; // 简化写法下月1号更严谨 String sql SELECT type, SUM(amount_cents) AS total FROM record WHERE record_date ? AND record_date ? GROUP BY type; Cursor cursor db.rawQuery(sql, new String[]{start, end}); while (cursor.moveToNext()) { StatItem item new StatItem(); item.setType(cursor.getInt(0)); item.setTotalCents(cursor.getLong(1)); list.add(item); } cursor.close(); return list; }这里有个容易踩坑的细节查询结果要按 create_time 倒序排而不是按 record_date 排。因为同一天可能记十几笔账record_date 相同的情况下只有 create_time 能区分先后顺序。Cursor 用完后必须 close虽然单次查询泄漏问题不明显但养成习惯总没错。4. 界面与图表把数据变成「看得见的高分」数据层跑通之后界面是让评委直观感知工作量的部分。记账本这种工具类应用不需要炫酷动效但要做到界面清爽、结构统一、演示顺畅。我用底部导航分三个页签账单流水、统计图表、设置记一笔用悬浮按钮唤起输入页这是记账类应用最常见也最顺手的信息架构。4.1 首页账单流水RecyclerView 与空视图首页是账单流水列表用 RecyclerView 实现。这个模块看起来简单但有一个高频翻车点删完数据界面不刷新。我会把所有加载逻辑收敛到一个方法里任何增删改操作完成后统一调用避免多处维护数据状态。public class RecordAdapter extends RecyclerView.AdapterRecordAdapter.VH { private ListRecordBean data new ArrayList(); public void refresh(ListRecordBean list) { this.data.clear(); this.data.addAll(list); notifyDataSetChanged(); // 数据量小全量刷新最简单 } NonNull Override public VH onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View v LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_record, parent, false); return new VH(v); } Override public void onBindViewHolder(NonNull VH holder, int position) { RecordBean bean data.get(position); holder.tvAmount.setText(formatAmount(bean.getAmountCents(), bean.getType())); holder.tvCategory.setText(bean.getCategoryName()); holder.tvDate.setText(bean.getRecordDate()); } }适配器不直接持有数据库引用通过 refresh 方法把新的数据列表传进来clear 后 addAll 再通知刷新这样数据源是唯一的。金额显示用工具方法格式化支出显示-¥23.50收入显示¥5000.00颜色也分开支出用红、收入用绿这是财务类应用的通用视觉约定。空视图处理是稳定性的体现当列表为空时显示居中的提示文案「还没有账单点击下方按钮记一笔」不能让用户面对一个空白页。activity_main.xml 结构 ├── BottomNavigationView // 底部三个页签 ├── FrameLayout // Fragment 容器 └── FloatingActionButton // 记一笔悬浮按钮4.2 统计图表图表库的引入与三个必调参数统计页用图表展示比纯文字数字直观得多。我一般引入开源的图表组件 MPAndroidChart项目里只多一个依赖但展示效果是两个量级。在 build.gradle 里声明依赖后同步一下即可注意确认项目根目录的 repositories 配置了 maven 仓库默认的 google() 加上 mavenCentral() 通常够用。PieChart pieChart findViewById(R.id.pieChart); pieChart.setUsePercentValues(true); pieChart.getDescription().setEnabled(false); // 去掉右下角默认描述文字 pieChart.setHoleRadius(55f); // 环形半径让饼图变成甜甜圈 pieChart.setTransparentCircleRadius(60f); Legend legend pieChart.getLegend(); legend.setVerticalAlignment(Legend.LegendVerticalAlignment.TOP); legend.setHorizontalAlignment(Legend.LegendHorizontalAlignment.RIGHT); legend.setOrientation(Legend.LegendOrientation.VERTICAL);参数作用建议值setUsePercentValues(true)显示百分比truegetDescription().setEnabled(false)去掉默认描述falsesetHoleRadius(55f)环形半径55fsetValueTextSize(13f)数据标签字号13finvalidate()重绘图表必须调用真正容易翻车的是数据刷新。很多人 setData 之后发现图没变其实少调了 invalidate()这个方法是重绘入口不调用的话哪怕数据变了界面也不会更新。我把刷新数据封装成一个方法内部先构造 PieEntry 列表再设置样式最后 invalidate这样统计页无论从哪个入口进入数据展示逻辑都是同一条路径。public void showCategoryPie(ListStatItem items) { ArrayListPieEntry entries new ArrayList(); for (int i 0; i items.size(); i) { StatItem item items.get(i); entries.add(new PieEntry(item.getTotalYuan(), item.getCategoryName())); } PieDataSet set new PieDataSet(entries, ); set.setColors(new int[]{0xFF4CAF50, 0xFFFF9800, 0xFF2196F3, 0xFFF44336, ...}); set.setValueTextSize(13f); PieData data new PieData(set); data.setValueFormatter(new PercentFormatter(pieChart)); pieChart.setData(data); pieChart.invalidate(); // 少了这一步图表数据永远不更新 }图表模块是项目工作量最直观的展示窗口。有同学纠结要不要用 Canvas 自绘我的看法是课程设计用成熟开源库完全正当答辩时重点讲清楚「我怎么把数据库聚合查询的结果喂给图表」才是你的工作量所在而不是让库背锅。饼图配个柱状图展示月度趋势会更好看但时间紧张时一个饼图已经足够支撑统计页的主体功能。4.3 输入交互金额、日期、分类三个控件记账输入页有三个核心控件要调校。金额输入使用 EditText 限制输入类型只允许数字和小数点。但注意从输入框拿到的是字符串不能直接用 Double.parseDouble 乘以 100 转分这个转换过程也会引入精度误差。统一用 BigDecimal 处理。private long yuanToCents(String text) { if (text null || text.trim().isEmpty()) { return 0; } BigDecimal yuan new BigDecimal(text.trim()); return yuan.multiply(BigDecimal.valueOf(100)).longValue(); }BigDecimal 构造方式要传字符串而不是浮点数new BigDecimal(0.1) 依然有精度问题new BigDecimal(0.1) 才是精确的。这个细节说出来很加分。日期选择我直接用 DatePickerDialog默认显示今天回调里用 SimpleDateFormat 格式化成 yyyy-MM-dd 再存入数据库注意指定 Locale 避免不同语言环境格式化结果不一致。分类选择用底部弹出列表这里有一个很关键的交互设计支出和收入的分类必须分开加载用户切换「支出/收入」时重新加载对应的分类列表防止把「工资」选成支出分类这种数据错乱。5. 期末大作业避坑清单五条真实踩过的坑这一章整理的是做记账本课程设计最容易翻车的五个问题每条都是实际发生过、且答辩时被老师当场抓过的。按「现象 → 原因 → 解决」的顺序写照着排查能省下大量调试时间。5.1 金额越加越少、统计莫名差几分浮点精度坑现象连续记了十几笔 0.1 元、0.2 元的小额开销月度统计比手算少了那么几分钱或者删掉一笔账再重新记一遍总额跟之前对不上。原因用 Float 或 Double 存金额浮点数无法精确表示十进制小数累加次数多了误差就显出来了。解决数据库全部改成整数「分」存储UI 展示层再用 BigDecimal 转换。转换时注意必须用字符串构造 BigDecimalnew BigDecimal(0.1) 没问题new BigDecimal(0.1) 照样有精度隐患。5.2 七月份的账跑进了六月日期与查询边界坑现象六月三十号晚上记的账月份统计跑到了七月份或者月度查询明明这个月记了账统计结果却是 0。原因日期字段要么存了带时分秒的完整字符串比如 2025-06-30 23:59:59要么用了默认时区转换导致时间偏移LIKE 2025-06% 时边界匹配不上。解决统一用 SimpleDateFormat 指定 Locale 格式化存固定长度的 yyyy-MM-dd。查询时用大于等于起始月和小于下月起始月的半开区间避免边界判断失误。创建时间单独存时间戳不要混在业务日期里。5.3 删除记录后界面纹丝不动刷新机制坑现象删除按钮点了数据库里查不到这条记录了但首页列表还显示着重启应用才消失。原因删完没有刷新适配器或者刷新时没有重新加载数据库数据界面显示的是内存里的旧数据。解决所有列表操作收敛到一个方法。我封装一个 loadRecords()内部重新查询数据库、填充列表、调用 adapter.refresh()增删改完成后统一调用根上杜绝状态不一致。5.4 图表库引入后编译期报错依赖配置坑现象依赖声明写好后一同步项目就报错或者编译时提示某个类找不到、版本冲突。原因常见是项目根目录的 repositories 里没有配置对应的 maven 仓库或者图表库版本和当前 compileSdk 不兼容。解决先检查 project 级别的 build.gradle 里是否配了 google() 和 mavenCentral()再确认图表库版本与当前 compileSdk 的兼容性。报错信息要完整读它通常会指名道姓告诉你哪个类冲突比盲猜有效得多。5.5 清空账单后一点统计就闪退空数据坑现象演示时把账单全删了然后切到统计页或者点开图表应用直接崩溃。原因图表组件 setData 时传了空列表或者统计代码里没对空结果做判断拿到空 Cursor 还在 moveToNext 之外取数据。解决所有列表和统计入口先判空。图表空数据时显示一个「暂无数据」的占位布局空列表时适配器显示空视图。这是一个代价最低但提升稳定性最明显的改动建议放到功能完成后第一优先级处理。6. 提交前最后一遍检查演示、文档与答辩的四个细节文档说明和代码同样重要很多高分项目输在「说不清」。我建议文档按三层写需求分析写明目标用户、核心用例和功能范围总体设计画清楚包结构、表结构和模块调用关系测试部分列出五到八个关键测试用例包括正常记账、金额边界输入、删除后统计联动、空数据展示、切换月份统计每一条配一张运行截图。文档不追求长篇大论二十页左右结构完整就够重点是让评分的人快速看到你的设计思路。演示路径要提前排练冷启动应用 → 记一笔支出 → 首页出现新记录 → 切到统计页看饼图 → 编辑这条记录改金额 → 删除它 → 统计页同步变化 → 再展示一下空数据占位。这条路径走完功能完整性、数据联动、稳定性三个维度全部展示到位。答辩时大概率被问「为什么不用云端」「数据丢了怎么办」我一般这样答记账是单机高频操作本地存储启动快、无网络依赖数据不出本地更符合记账隐私预期备份功能已支持导出 JSON恢复逻辑在导入处集中处理。提交前顺手清一遍工程目录把 build 缓存删掉确认 README 写清楚运行环境和最低安卓版本。我吃过最大的亏就是文档写得潦草代码再完整也说不出来答辩被老师连续追问后只能尬住。从那以后我习惯把「别人看完文档能不能复现这个项目」当作文档质量的唯一标准。这个习惯帮我省了好几次补救的时间希望你也能用上。本文还有配套的精品资源点击获取