
简介这套基于安卓开发工具构建的记账本系统面向初学安卓开发的学生或需要日常记账的个人用户完整覆盖登录注册、账单增删改查、分类筛选与个人信息管理等功能流程。压缩包共70个文件整体约10.68MB其中16份Java源代码承载业务逻辑22份布局与配置文件搭建界面18张图片资源提供视觉素材另有构建脚本、安装包、演示视频及运行文档类型配置齐全便于从源码到安装运行全流程理解与实操。目前已有493人学习下载。开发者可借助开放的源代码研究数据存储、用户验证与界面交互等实现方式结合演示视频和说明文档快速完成导入调试普通用户可直接安装使用通过分类记账和账单筛选掌握收支情况。资源内另附开发环境配置说明与运行文档适合作为课程设计、毕业设计或安卓入门实践项目的参考模板也可帮助开发者快速上手安卓应用开发的常见模块。1. 安卓记账本系统比想象中更考验工程功底的练手项目很多人以为记账本是安卓入门项目真做完才发现它卡人的地方根本不在「记一笔」——而是在数据怎么存才不丢、列表刷新怎么不卡、统计图怎么画得准、打包出来在别人手机上能不能装得上。基于 Android Studio 开发的安卓记账本系统本质上是一套「本地数据库 界面交互 图表统计 打包交付」的完整闭环正好覆盖了一个安卓应用从开发到上架前的大部分环节。适合刚学完四大组件、想交付完整应用的新手也适合拿它做课设、毕设或内部工具再往深走还能加备份恢复、桌面小组件和多账户。这篇按我自己的做法从建工程讲到打包避坑照着走能少踩一半的坑。2. 先把工程搭对技术选型与 Android Studio 项目骨架2.1 为什么记账本不需要后端本地数据库反而更合适记账数据是强隐私数据绝大多数个人记账场景根本不需要云同步。本地数据库的好处是离线可用、响应快、没有服务器成本坏处是一旦手机丢了数据就没了所以后面必须做导出备份。常见做法是 Room SQLiteRoom 在 SQLite 之上做了一层编译期校验和 LiveData/Flow 的响应式封装比裸写 SQLiteOpenHelper 适合现代安卓开发。如果只是课设级别SQLiteOpenHelper 也能跑但你要自己处理游标关闭、线程切换、类型转换代码量翻倍还容易漏。Room 的学习成本大约半天换来的是 DAO 接口编译期检查 SQL 语法错误这个收益在后期改表结构时特别明显。我一般会直接选 Room哪怕是最简版本。2.2 新建工程与 Gradle 配置依赖下载慢和设置中文的落地处理Android Studio 新建 Empty Views Activity 项目后第一件事不是写代码而是确认 Gradle 能顺利拉依赖。国内网络环境下google() 和 mavenCentral() 不一定稳常见做法是在项目级 build.gradle 里加阿里云镜像注意顺序要放在 google() 前面才生效。// 项目级 build.gradle 的 repositories 部分 buildscript { repositories { maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/gradle-plugin } google() mavenCentral() } }这段配置把 Google Maven 和中央仓库的请求优先转发到国内镜像能明显缩短首次 Sync 时间。如果之前已经卡在下载阶段改完后执行 File Sync Project with Gradle Files。还有个小坑个别镜像同步有延迟拉不到最新版本库时把 dependencies 里的版本号降到稳定版别追最新。新手还会找 Android Studio 怎么设置中文界面语言在 Settings Appearance Behavior System Language 里切跟工程本身无关不影响编译。真正要花时间的是搞清楚 Gradle JDK 版本和 compileSdk 的匹配关系默认模板一般没问题但如果你装了多个 JDK在 Settings Build Tools Gradle 里指定 JDK 17不然老项目会报 Unsupported Java version。2.3 按业务拆包一个能撑到毕设结束的目录结构很多记账本项目最后的结局是Activity 里塞了两千行代码数据库操作写在 onResume 里改一个查询要翻半天。我一般会按数据层、界面层、工具类三层拆包哪怕项目很小也这么拆后面加功能不用重构。com.example.ledger/ ├── data/ │ ├── db/ # RoomDatabase、Entity、DAO、Migration │ ├── repository/ # BillRepository统一对外暴露数据接口 ├── ui/ │ ├── activity/ # MainActivity、统计页面 │ ├── adapter/ # RecyclerView 的 Adapter │ ├── dialog/ # 新增/编辑账单的对话框 ├── utils/ │ ├── DateUtils.kt # 时间格式化 │ ├── MoneyUtils.kt # 分转元、金额校验 ├── App.kt # Application初始化数据库拆包不是形式主义。记账本的核心数据流只有一条「界面 → ViewModel/Repository → DAO → Room」按层拆开后每一层都能单独测试。比如你觉得统计算错了直接查 Repository 里的聚合查询不用从界面往下追。Activity 里只做两件事绑视图、观察数据其它一律不碰。3. 数据层是记账本的命根子Room 建表、DAO 与仓储设计3.1 Room 还是 SQLiteOpenHelper核心差异在哪Room 对比 SQLiteOpenHelper 最大的优势不是性能而是编译期检查。你写一个 SQL 查询字段名拼错SQLiteOpenHelper 要跑到那行代码才崩溃Room 在编译期直接报错。另一个优势是响应式——DAO 返回 Flow数据一变界面自动更新不用手动 notifyDataSetChanged。这对记账本非常实用新增一笔、删除一笔列表自动刷新不会出现「记了账界面没反应」的玄学问题。Room 也有黑匣子的一面它生成的实现代码在 build/generated 目录里出问题时要会看编译日志。常见做法是先让它跑起来再逐步加复杂查询。首次接入需要这三样Entity 实体类、DAO 接口、RoomDatabase 子类一个都不能少。3.2 定义 Entity 与 DAO金额用分存时间用 Long 存记账本最容易翻车的不是界面是金额精度和时间格式。Java/Kotlin 的 double 在连续加减时会出 0.1 0.2 ! 0.3 这种问题所以金额统一用 Long 存「分」界面显示时再转成元。时间统一用 Long 存毫秒时间戳排序和范围查询都靠它界面展示时才格式化成 yyyy-MM-dd。Entity(tableName bill) data class Bill( PrimaryKey(autoGenerate true) val id: Long 0, val amountFen: Long, // 金额单位分避免浮点精度问题 val category: String, // 消费分类餐饮、交通、购物等 val note: String, // 备注可为空字符串 val timestamp: Long, // Unix 毫秒时间戳排序、统计都靠它 val type: Int // 0 支出1 收入统计时按此区分 )amountFen 加注释说明单位不然三个月后你自己都会忘。type 用 Int 而不是布尔值是因为后面可能加「退款」「转账」等类型Int 扩展性更好。timestamp 存的是 System.currentTimeMillis()查询某月账单时用 start 和 end 两个时间戳做 BETWEEN 条件比存字符串再用 LIKE 匹配快一个数量级。DAO 接口是数据层的脸面所有 SQL 都集中在这里Dao interface BillDao { Insert suspend fun insert(bill: Bill): Long Update suspend fun update(bill: Bill) Delete suspend fun delete(bill: Bill) Query(SELECT * FROM bill ORDER BY timestamp DESC) fun getAllBills(): FlowListBill Query(SELECT COALESCE(SUM(amountFen), 0) FROM bill WHERE type 0 AND timestamp BETWEEN :start AND :end) suspend fun getExpenseSum(start: Long, end: Long): Long Query(SELECT COALESCE(SUM(amountFen), 0) FROM bill WHERE type 1 AND timestamp BETWEEN :start AND :end) suspend fun getIncomeSum(start: Long, end: Long): Long }getAllBills 返回 FlowRoom 会在表数据变化时自动重新发射列表界面订阅后自动刷新。getExpenseSum 里的 COALESCE 很关键某个月没有支出时 SUM 返回 null不包一层默认值后面转 Long 直接 NPE。这是新手最常踩的坑SQLite 的聚合函数对空表返回 null不是 0。3.3 Repository 包一层Activity 里不写 SQL 也不管线程有了 DAO 之后很多人直接在 Activity 里调 dao.insert()短期没问题一旦业务变复杂就失控。我习惯加一层 Repository把「数据从哪来」的细节全部收进去。Activity 只知道 repository.addBill(bill)至于 Room 还是以后换网络接口Activity 完全不用改。class BillRepository(private val billDao: BillDao) { fun getAllBills(): FlowListBill billDao.getAllBills() suspend fun addBill(amountFen: Long, category: String, note: String, type: Int) { billDao.insert(Bill( amountFen amountFen, category category, note note, timestamp System.currentTimeMillis(), type type )) } suspend fun deleteBill(bill: Bill) billDao.delete(bill) suspend fun getMonthExpense(monthStart: Long, monthEnd: Long): Long { return billDao.getExpenseSum(monthStart, monthEnd) } }注意 addBill 是 suspend 函数说明它要在协程里调用。Room 的 suspend DAO 方法会自动切到后台线程执行你在主线程调也安全但还是要养成「所有数据操作都走 suspend ViewModel」的习惯。如果项目不引入 ViewModel直接在 Activity 里用 lifecycleScope.launch 包一层也行只是 Activity 会慢慢变胖。Android Studio 自带抽取方法快捷键CtrlAltM 或 CmdAltM写 Activity 时看到超 5 行的逻辑块就抽出去界面代码能一直保持清爽。提示Room 必须在 Application 里初始化不能每次 new 一个实例否则会创建多个数据库连接偶发磁盘占用异常。初始化代码放在 App.kt 里用 companion object 暴露单例。4. 界面层怎么组织RecyclerView 列表、统计图表与输入防呆4.1 账单列表RecyclerView 适配器与点击删除记账本的主界面是典型的「列表 悬浮按钮」结构列表用 RecyclerView 而不是 ListView性能差异在数据量超过几百条时就能感觉到。适配器要接收 List 数据用 ListAdapter 加 DiffUtil 是最稳的做法数据更新时列表自动做最小刷新不会闪烁。class BillAdapter(private val onItemClick: (Bill) - Unit) : ListAdapterBill, BillAdapter.BillViewHolder(DiffCallback) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BillViewHolder { val binding ItemBillBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return BillViewHolder(binding) } override fun onBindViewHolder(holder: BillViewHolder, position: Int) { val bill getItem(position) holder.binding.tvAmount.text MoneyUtils.fenToYuan(bill.amountFen) holder.binding.tvCategory.text bill.category holder.binding.tvNote.text bill.note holder.binding.root.setOnClickListener { onItemClick(bill) } } companion object DiffCallback : DiffUtil.ItemCallbackBill() { override fun areItemsTheSame(oldItem: Bill, newItem: Bill) oldItem.id newItem.id override fun areContentsTheSame(oldItem: Bill, newItem: Bill) oldItem newItem } }适配器只负责绑定数据点击事件通过回调抛出去由 Activity 决定是弹窗改还是删。注意 DiffCallback 里 areItemsTheSame 用的是 idareContentsTheSame 用的是整个对象比较两者语义不同前者判断是不是同一条数据后者判断内容变没变。如果内容比较字段太多影响性能可以只比 amountFen 和 timestamp。4.2 月度趋势MPAndroidChart 画收支曲线统计页面是记账本区别于「流水账」的核心功能。常见做法是用 MPAndroidChart 画一张月度收支趋势图x 轴是日期y 轴是金额。这个库已经快十年没大更新但够稳定配合 build.gradle 里的 implementation com.github.PhilJay:MPAndroidChart:v3.1.0 就能用。val lineChart binding.lineChart val entries monthBills.groupBy { it.dayOfMonth } .map { (day, bills) - Entry(day.toFloat(), bills.sumOf { it.amountFen } / 100f) } val dataSet LineDataSet(entries, 每日支出).apply { color Color.parseColor(#FF6B6B) lineWidth 2f circleRadius 3f setDrawValues(false) } lineChart.data LineData(dataSet) lineChart.xAxis.valueFormatter object : IndexAxisValueFormatter() { override fun getFormattedValue(value: Float): String { return ${value.toInt()}日 } }注意汇总逻辑在 Kotlin 侧做而不是 SQL 侧做因为每天一个点的数据量很小Kotlin 的 groupBy 足够快而且 SQL 按天分组还要再处理日期格式化代码可读性更差。x 轴用 IndexAxisValueFormatter 把数字转成「3日」「15日」不然默认显示 3.0、15.0 很丑。图表的坑在空数据一天都没记录时 entries 为空LineDataSet 会崩先判断 size 0 就显示空视图。4.3 输入防呆金额、日期、分类三个容易翻车的地方记账本每天打开输入体验决定用户会不会持续用。金额输入框用 EditText 的 inputTypenumberDecimal键盘只弹数字和小数点但用户还是能输入「1.2.3」这种必须在提交时做一次校验。日期默认给今天提供 DatePickerDialog 选择分类用预置列表加「其他」兜底不要做自由输入否则统计图里会出现十几个相似分类。fun parseAmount(input: String): Long? { val trimmed input.trim() if (trimmed.isEmpty()) return null return try { val yuan trimmed.toDouble() if (yuan 0) null else Math.round(yuan * 100) } catch (e: NumberFormatException) { null } }这段把用户输入的「12.34」元转成 1234 分。注意用 toDouble 再四舍五入而不是直接字符串处理小数位因为用户可能输入「12.345」这时精度丢就丢了记本金句界面允许的输入精度决定了数据精度你不可能通过后端补偿界面丢掉的精度。校验失败时 Toast 提示具体原因别只弹「输入有误」用户不知道自己错哪了。5. 打包与真机调试避坑签名、资源错误、版本兼容与数据库迁移5.1 模拟器连不上 adb、模拟器不联网的排查开发阶段最常见的翻车是模拟器起不来、adb 连不上、或者跑起来没网。现象千奇百怪adb devices 列表空、Android Studio 一直显示 waiting for target device to come online、模拟器里浏览器打不开网页。原因基本集中在三处ADB 版本和模拟器不匹配、模拟器 DNS 配置错误、或者 AVD 冷启动没完成就急着装应用。先确认 adb 能识别设备再查网络。常见做法是命令行执行 adb kill-server 后重启 adb再 adb devices。模拟器不联网时先看系统时间对不对不对的话 DNS 解析必然失败再在模拟器设置里把 Wi-Fi 的 DNS 改成 223.5.5.5 这类公共 DNS重启模拟器。如果你用的是 Android Studio 自带的 AVD冷启动慢是常态别在启动动画没结束时就操作等桌面图标出现再装 APK。5.2 签名打包jks 密钥、Gradle 签名配置与安装失败发布给别人安装的 APK 必须签名。Android Studio 的 Build Generate Signed Bundle / APK 向导能帮你一步步生成 jks 密钥库但有个坑很多人生成完密钥就丢等下次要更新版本时找不到 jks只能换包名重新上架之前的用户全部要手动卸载。所以 jks 文件要备份密码要放在自己能找到的地方。签名配置写进 build.gradle 后以后打 release 包就不用手动点向导了android { signingConfigs { release { storeFile file(keystore/ledger.jks) storePassword your_password_here keyAlias ledger keyPassword your_password_here v1SigningEnabled true v2SigningEnabled true } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled false shrinkResources false } } }v1 和 v2 签名必须都打开v2 是 Android 7.0 以上校验方式v1 兼容老设备只开 v2 会导致部分 6.0 以下机型无法安装。还有一个经典报错Installation failed with message INSTALL_FAILED_UPDATE_INCOMPATIBLE因为手机里已经装了同一个包名的旧版本但签名不一样。解决就两条卸载旧版或者把应用包名改掉。测试阶段我一般用 debug 包发布才切 release 签名。打出来的 release APK 传到手机安装时又会遇到「解析包出现问题」或「该文件包含恶意代码」的提示。前者多半是 APK 没下载完整后者是部分国产 ROM 对未知来源应用的风控让你在设置里允许「安装未知应用」。这一步跟代码没有关系但第一次做的人会以为包坏了白折腾半天。5.3 数据库升级从 1.0 到 2.0 的迁移脚本怎么写记账本上线后最容易出事故的就是改表结构。用户手机里已经有数据你升级版本时如果 Room 发现 schema 不匹配默认行为是崩溃并提示需要迁移或重建。新手常见的处理方式是卸载重装但记账数据全没了用户直接流失。正确做法是每次改表结构都升版本号并写 Migrationval MIGRATION_1_2 object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(ALTER TABLE bill ADD COLUMN is_deleted INTEGER NOT NULL DEFAULT 0) } } Room.databaseBuilder(context, AppDatabase::class.java, ledger.db) .addMigrations(MIGRATION_1_2) .build()这里从版本 1 升到 2加了一个 is_deleted 字段用于软删除。ALTER TABLE 加列是轻量操作不需要重建表但如果要改列名、改约束SQLite 的 ALTER 能力有限常见做法是先建新表、拷贝数据、删旧表、重命名。写迁移脚本前先备份原数据库文件在 data/data/包名/databases/ 下迁移跑完再看数据是否完整这是唯一靠谱的后悔药。还有一类「release 正常、debug 正常、一上正式版就崩」的疑难杂症现象是 Room 报 Schema export directory 相关错误原因是 Room 编译期需要导出 schema 文件缺省配置下 release 构建找不到目录。解决方法是 build.gradle 里给 kapt/ksp 配 schemaLocation指向项目里的 schemas 目录并且把生成的 json 文件提交到 git。这些文件能让你在升级数据库时用命令行工具对比新旧 schema排查「为什么 Room 认为表结构不匹配」这类黑匣子问题。5.4 资源重复错误android resource linking failed 的排查改项目过程中会遇到编译报错提示 resource linking failed 或 Duplicate resources常见于你往 res 目录里放了非标准文件比如把 xlsx、zip 直接丢进 drawable 或 mipmap或者复制别处代码时把 layout 文件一起带进来同名字段冲突。定位方法是看 Build 输出里具体报哪个文件、哪个资源名然后删掉重复资源。还有一个高频误操作在 values 里重复定义了同名的 string 或 colorAndroid Studio 编辑器有时候不报错但 assemble 时翻车。养成改完 resource 就执行一次 gradlew clean 的习惯能省不少排查时间。6. 让记账本从「能跑」变成「能用」备份恢复与性能自查记账本能记录只是及格线真正的分水岭是数据安全和操作流畅度。导出备份用 CSV 是最通用的方案一份文件可以用 Excel 打开也能用于数据迁移。我把导出功能放在主界面的菜单里生成文件写到应用专属外部目录同时用 Intent 调起分享让用户直接发给自己或存网盘。fun exportBillsToCsv(context: Context, bills: ListBill): File { val file File( context.getExternalFilesDir(null), ledger_backup_${System.currentTimeMillis()}.csv ) file.bufferedWriter().use { writer - writer.write(id,amountFen,category,note,timestamp,type\n) bills.forEach { bill - writer.write(${bill.id},${bill.amountFen},${bill.category}, ${bill.note},${bill.timestamp},${bill.type}\n) } } return file }导出的字段和数据库列一一对应恢复时按 CSV 逐行解析重新 insert 进 Room。注意 note 里可能有逗号和换行写 CSV 时要用引号包一下否则恢复解析会错位。这个坑我踩过恢复出来几十条账单分类全串了从那以后凡是写 CSV 我都加引号包裹文本字段。性能自查用 Android Studio 的 Profiler 就能做。打开 CPU Profiler 操作一遍「新增账单 → 切统计页 → 切回列表」看主线程是否有超过 16ms 的任务再打开 Memory Profiler 看有没有内存抖动。记账本数据量在几万条以内性能问题几乎都出在列表 Item 里做了耗时操作比如在 bind 里加载图片、格式化日期用了 SimpleDateFormat 的实例化把日期格式化抽成工具类复用即可。我自己的习惯是每个月导一次 CSV 存到本地防止哪天调试把数据库玩坏。这个项目做完最大的收获不是学会了 Room 或 RecyclerView而是意识到安卓应用交付不是「编译过」就完事而是要在用户手里能装、能跑、数据不丢。希望你做完记账本后也会像我一样先备份再升级这个习惯能救你很多次。本文还有配套的精品资源点击获取