Android记账本App开发实战:从数据库存储到图表统计完整实现 📅 发布时间:2026/9/12 11:49:53 👁 浏览次数: 简介这是一份基于Android Studio实现的记账本App项目源码面向Android初学者、期末大作业或课程设计需要完整项目的学生。项目包含账单添加、分类统计等常见功能代码结构清晰可直接导入运行。压缩包共56个文件其中12个Java文件对应核心业务逻辑20个XML文件覆盖界面布局与配置另有Gradle构建脚本、ProGuard混淆配置及图标资源等整体仅166KB轻量易用。已有1878人学习下载属于高分结课项目。资源内附完整工程目录含app模块、gradle配置和项目级构建文件下载后无需修改即可编译运行适合作为参考模板快速理解Android开发流程或在此基础上扩展功能完成课程设计。1. 为什么“记账本 App”是安卓期末大作业最稳妥的题目期末大作业最怕两件事一是题目太偏做完没人看得懂二是项目太模板化答辩时老师一问实现细节就卡壳。记账本 App 恰好卡在中间功能直观、涉及数据持久化、列表展示、界面跳转、统计图表这些安卓课必考的知识点每一样都能在答辩时展开讲。更关键的是它能用“我加了一个功能”来拉开与同学的差距而不是靠改个颜色交差。这份基于 Android Studio 的记账本项目源码做的不是那种只存进内存、重启就清空的 Demo而是用数据库把每一笔收支真正落盘。无论你拿它当自己的期末大作业底子还是想拆开看一个标准的“界面 数据库 逻辑”三层结构怎么写都有可抄的作业。下面我按自己做项目时会走的路径来讲先把环境收拾利索再把一笔账从界面写进数据库然后补统计和导出最后聊打包和答辩自测。2. 开发前的准备Android Studio 环境与数据库选型2.1 把 Android Studio 环境调到能直接跑项目的状态拿到别人的源码第一步不是打开就编译而是确认三件事SDK 版本、JDK 版本、Gradle 版本。这三样只要有一个对不上同步的时候就报一堆莫名其妙的红叉。常见做法是先用 Android Studio 自带 SDK Manager 装好目标平台的 SDK。热词里有人问“android studio sdk 无法勾选怎么解决”多数情况是网络问题SDK 组件列表加载不出来。解决办法是配置国内镜像源在SDK Manager - SDK Update Sites里把下载源改成镜像地址。如果你用的是新版 Android Studio直接在设置里搜 “SDK” 就能看到。Gradle 同步慢是最容易让人中途放弃的坎。新建或导入项目时建议在用户目录下的~/.gradle/init.gradle里写入阿里云镜像allprojects { repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/gradle-plugin } maven { url https://jitpack.io } } }这段配置把 Google 仓库和 Maven Central 都指到了国内镜像。第一次同步项目时Gradle 会自动读取init.gradle你就不用在每个项目的build.gradle里逐个改仓库地址了。同步完成后记得检查File - Project Structure - SDK Location确认 JDK 路径用的是 Android Studio 自带的 JBR而不是你机器上装的别的版本。提示如果你导入项目后看到tag number over 30 is not supported说明项目里的 build-tools 版本太新而当前 SDK 没装全去 SDK Manager 勾选对应版本重新同步即可。2.2 新建项目的包名规划和目录结构我一般不建议拿别人的项目直接覆盖成自己的作业因为包名里常常带着原作者的名字或者学号答辩时老师一眼就看出问题。正确姿势是新建一个空项目然后把源码里的java目录和res目录按包路径复制进去最后统一改applicationId。在build.gradle里找到 applicationId改成你自己的域名倒序比如com.example.myaccount。注意AndroidManifest.xml里所有android:name.activity.MainActivity这类简写引用它们会自动拼上 applicationId所以只要包名一致不会报错。记账本项目至少分这几个包包名职责bean/entity存放数据实体类比如 TransactionBeandb数据库辅助类或 ORM 数据库类adapterRecyclerView 或 ListView 的适配器activity各个界面utils日期格式化、金额校验等工具类2.3 SQLite 与 Room为什么期末作业推荐用 Room记账本一定需要本地数据库。安卓上有三种选择直接用SQLiteOpenHelper写原生 SQL、用Room封装、或者用SharedPreferences存 JSON。SharedPreferences 只适合存设置项不适合存几十上百条账单记录因为每次读取都要全量反序列化。原生 SQLite 不是不能做但对期末作业来说写onCreate建表、ContentValues插入、Cursor遍历这一套太繁琐而且很容易在更新数据库版本时漏掉迁移逻辑。Room 是 Google 官方的 ORM 框架它在 SQLite 之上加了编译期检查——如果你的 SQL 写错了表名或字段名编译直接报错而不是运行时才崩溃。这是我的建议凡是“从列表页跳到详情页、又能增删改查”的项目直接用 Room。它帮你把模板代码省掉让你把精力留到功能差异上。注意Room 需要添加注解处理器依赖。如果你的项目因为引入 Room 后编译报错优先检查是否忘了加kapt或ksp插件。3. 记一笔账从界面输入到数据库落盘的核心链路3.1 定义实体类与数据表结构先建一个数据类对应数据库里的tb_transaction表。这张表记录每一笔收支的关键字段。我一般会加一个type字段区分收入还是支出再用一个account字段区分现金、微信、支付宝等支付方式这样后面做统计时不至于没东西可分组。Entity(tableName tb_transaction) data class Transaction( PrimaryKey(autoGenerate true) val id: Long 0, // 数据库生成的自增主键 val type: String, // 类型0 支出1 收入 val category: String, // 分类餐饮、交通、工资等 val amount: Double, // 金额保留两位小数 val account: String, // 支付方式现金、微信、支付宝 val remark: String, // 备注可选 ColumnInfo(name record_time) // 存储毫秒时间戳方便按时间排序和筛选 val time: Long )3.2 用 DAO 封装增删改查方法DAO 是 Room 里负责访问数据库的接口用注解声明 SQL 操作。最常用的四个方法插入、删除、更新、按条件查询。Dao interface TransactionDao { Insert suspend fun insert(transaction: Transaction): Long Delete suspend fun delete(transaction: Transaction) Update suspend fun update(transaction: Transaction) Query(SELECT * FROM tb_transaction ORDER BY time DESC) fun getAllTransactions(): LiveDataListTransaction Query(SELECT * FROM tb_transaction WHERE id :id) suspend fun getTransactionById(id: Long): Transaction? }Insert方法标注了suspend这是 Room 配合协程的推荐写法。getAllTransactions返回LiveData好处是当数据库里的数据变化时界面层观察的LiveData会自动收到新列表不需要你手动刷新 Adapter。这个特性在期末答辩时很加分因为“数据驱动 UI 更新”是面试和答辩都喜欢问的点。最后写数据库类Database(entities [Transaction::class], version 1, exportSchema false) abstract class AppDatabase : RoomDatabase() { abstract fun transactionDao(): TransactionDao companion object { Volatile private var INSTANCE: AppDatabase? null fun getInstance(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { INSTANCE ?: Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, account.db ).build().also { INSTANCE it } } } } }3.3 界面输入与数据写入记账页面一般有两个输入框或一个选择器一个填金额一个选分类。点击保存按钮后先做“金额是否为空”这类基础校验再组装 Transaction 对象最后通过 ViewModel 调用 DAO 的方法。推荐用 ViewModel 而不是直接在 Activity 里拿数据库实例因为旋转屏幕时 Activity 重建ViewModel 不会丢避免数据重复提交的问题。// 在 Activity 中获取 ViewModel val viewModel: TransactionViewModel by viewModels { TransactionViewModel.Factory( (application as MyApplication).database.transactionDao() ) } // 保存按钮点击事件 binding.btnSave.setOnClickListener { val amountStr binding.etAmount.text.toString() val amount amountStr.toDoubleOrNull() ?: 0.0 if (amount 0) { Toast.makeText(this, 请输入正确的金额, Toast.LENGTH_SHORT).show() returnsetOnClickListener } val transaction Transaction( type if (binding.rbIncome.isChecked) 收入 else 支出, category binding.spCategory.selectedItem.toString(), amount amount, account binding.spAccount.selectedItem.toString(), remark binding.etRemark.text.toString(), time System.currentTimeMillis() ) lifecycleScope.launch { viewModel.insertTransaction(transaction) Toast.makeText(thisAddActivity, 保存成功, Toast.LENGTH_SHORT).show() finish() } }校验逻辑放在toDoubleOrNull()这里。如果用户输入非法字符得到 null金额会被设成 0.0配合后面的if (amount 0)直接拦截。注意 Toast 后面returnsetOnClickListener表示只退出这个监听器的回调不影响 Activity 生命周期。生命周期问题要注意启动协程用的是lifecycleScope.launch在 Activity 被销毁时协程会自动取消避免出现界面关闭但数据库写入还在进行的悬空操作。3.4 列表展示与删除、编辑列表页用 RecyclerViewAdapter 接收ListTransaction。因为getAllTransactions()返回 LiveData只需要在 Activity 里观察它viewModel.allTransactions.observe(this) { list - adapter.submitList(list) }删除操作要在列表项上做一个长按监听弹一个 AlertDialog 确认确认后调用viewModel.delete(transaction)。编辑的常规做法是点击列表项跳转到编辑页通过 Intent 携带交易 id编辑页拿到 id 后先查询原数据再把原数据显示到输入框中。这里有一个设计细节值得提删除前应该保存被删对象的引用。Room 的 Delete 是根据主键定位的如果你在别处重新构造了一个只有 id 的 Transaction 对象其他字段为 nullDelete 依然能按主键删掉。但如果传入的对象的 id 是 0那 Room 会尝试删除主键为 0 的行不会报错但也不会删掉任何数据。3.5 不要在数据库访问上踩的线程坑Room 默认不允许在主线程操作数据库除非你调用allowMainThreadQueries()。我建议绝对不要图省事加这个方法因为当数据量到几百条时主线程查询会引发界面掉帧甚至触发 ANR。正确做法是把数据库操作全放进协程里。上面的insert、delete、update都声明为suspendRoom 会在后台线程自动执行。查询则用LiveData它内部已经处理了线程切换。提示Room 的suspend函数会在 Room 自己的事务执行器上跑你在代码里不需要再包一层withContext(Dispatchers.IO)Room 已经帮你做了。4. 给记账本加分统计图表、多账本与数据导出4.1 用 MPAndroidChart 展示消费趋势期末大作业一单加就加图表。Android 自带的控件不支持饼图和折线图所以最常用的方案是引入 MPAndroidChart。在build.gradle里添加依赖implementation com.github.PhilJay:MPAndroidChart:v3.1.0它需要通过 JitPack 仓库下载所以确保前面init.gradle或项目仓库配置里有maven { url https://jitpack.io }。你可能会看到别人用implementation com.github.PhilJay:MPAndroidChart:v3.1.0报错找不到仓库的问题八成就是少了 jitpack。绘制一张按日期聚合的支出折线图核心逻辑是把一段时间内的交易数据按“天”分组和值作为 Y 轴数据// 获取当前月的所有支出记录 val monthStart getMonthStartMillis() val monthEnd getMonthEndMillis() val transactions viewModel.getTransactionsByTimeRange(monthStart, monthEnd) // 按日期聚合金额 val map HashMapString, Float() for (t in transactions) { if (t.type ! 支出) continue val day SimpleDateFormat(MM-dd, Locale.getDefault()).format(Date(t.time)) map[day] (map[day] ?: 0f) t.amount.toFloat() } val entries map.map { Entry(it.value, it.key) } val dataSet LineDataSet(entries, 每日支出).apply { color Color.parseColor(#FF7043) setDrawFilled(true) fillColor Color.parseColor(#FFCCBC) }图表相关的清单参数推荐值作用setDrawFilled(true)true折线下方填充颜色观感提升description.isEnabledfalse去掉右下角默认的 DescriptionaxisRight.isEnabledfalse隐藏右侧 Y 轴刻度版面更干净xAxis.positionXAxis.XAxisPosition.BOTTOMX 轴标签默认在底部更习惯4.2 多账本一个数据表的设计方案有些人会加一个“账本”维度比如分成“日常账本”和“旅行账本”。实现时不需要改表结构只需要在 Transaction 表里新增一列book_id默认值是 0 代表默认账本。查询时加一个条件Query(SELECT * FROM tb_transaction WHERE book_id :bookId ORDER BY time DESC) fun getTransactionsByBook(bookId: Long): LiveDataListTransaction新建账本页用一个单独的book表保存账本名称、创建时间和图标。这样做的好处是添加新账本时不需要迁移原表只插入一条新记录。注意数据库版本要从 1 升到 2并在Database注解的version字段上改同时提供一个Migration对象否则旧用户升级时会崩溃。val MIGRATION_1_2 object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(ALTER TABLE tb_transaction ADD COLUMN book_id INTEGER NOT NULL DEFAULT 0) } }把MIGRATION_1_2传进Room.databaseBuilder().addMigrations(MIGRATION_1_2)。这个细节很多人忽略答辩时老师爱问“如果升级数据库版本怎么办”提前讲出Migration就能显得你考虑过真实场景。4.3 导出 CSV用 FileProvider 解决文件分享问题统计报表功能做完后可以加一个“导出账单为 CSV 文件”的功能。共享文件时需要一个content://的 Uri否则在不同应用间传递文件路径会失败。在AndroidManifest.xml里给 FileProvider 注册provider android:nameandroidx.core.content.FileProvider android:authoritiescom.example.myaccount.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /provider在res/xml/file_paths.xml中声明对外暴露的目录paths external-files-path nameexport pathexport/ / /paths导出代码的核心部分是拼接 CSV 字符串并用 Intent 发送出去。关键点是FileProvider.getUriForFile帮你把 File 转成合法的content://Uri对方的收件应用拿到这个 Uri 就能读取流并保存界面显示诸如content://开头的一长串路径是正常现象不必理会。4.4 进度条加载变“卡住”为“在处理”期末作业的数据量小基本不会出现长时间加载。但如果你加了“从导入 CSV 导入”这种 IO 操作那必须给个进度反馈。常见做法是用一个 ProgressDialog 或ProgressBar指示耗时任务进度。val dialog ProgressDialog(this).apply { setMessage(正在导入...) setCancelable(false) setProgressStyle(ProgressDialog.STYLE_SPINNER) } dialog.show() // 导入逻辑放在后台线程 lifecycleScope.launch(Dispatchers.IO) { importFromCsv(uri) withContext(Dispatchers.Main) { dialog.dismiss() Toast.makeText(thisMainActivity, 导入完成, Toast.LENGTH_SHORT).show() } }这里注意一个浅坑导入一定在 IO 线程执行但 UI 的撤回要切到主线程所以我用withContext(Dispatchers.Main)而不是继续留在 IO 线程里直接 dismiss否则会抛CalledFromWrongThreadException。5. 打包签名与答辩前的自测技巧答辩演示怕的不是功能少而是演示中途 App 崩溃。这里说几个我在交出期末作业前一定会做的自检项顺手能提升印象分。打开Build - Generate Signed App Bundle or APK选择 APK新建一个.jks签名文件。这一步做两个事生成可安装到别人手机的正式 APK同时把build.gradle里signingConfig配上方便下次直接 Build APK。如果老师要求演示时直接连手机跑 Android Studio那这一步可以跳过但如果你要把作业提交为安装包签名是必需的。答辩前模拟一次“从零安装”场景把打包好的 APK 通过微信或 QQ 发到手机上然后安装。连真机时优先使用adb install命令来安装来避免传输工具损坏安装包adb install -r app-release.apk-r表示覆盖安装如果手机上已经装了旧版保留数据并覆盖安装可以避免卸载重装导致的数据清空。功能自测建议按这条清单走一遍检查项预期表现新增一笔 0 元或负数的支出弹“金额不正确”不写入数据库旋转屏幕后列表数据仍在不报错不丢失数据删除某条记录后退出重进数据不复活统计页切换月份后图表刷新图表 Y 轴自动更新导出 CSV 后用 WPS 打开列名正确、中文不乱码其中“中文不乱码”是常见坑。导出 CSV 时默认用系统编码在国内手机上通常没问题但如果用 Excel 打开显示乱码应在写文件时加上 UTF-8 BOM 头。方法是在写内容之前先写\uFEFF这一个字符。还有一个演示细节如果演示时手机是锁屏状态务必先手动点亮屏幕解锁再开始操作。很多人在“演示”场景遇过这种尴尬屏幕黑着点击无反应主考官以为 App 卡死实际上是有的手机会在演示投屏时对锁屏状态做省电限制界面不自动点亮。如果到了答辩现场才发现某条记录不小心删了别慌张演示完主动补一句“删除功能刚才已演示数据库中的记录确实被移除了”。这不影响评分反而证明功能可用。正式答辩前可以把手机里的普通账本数据清空只留 2 ~ 3 条特定分类的账目。空数据演示图表不美观数据太多又会让列表滚动显得啰嗦。留三条一条餐饮支出、一条交通支出、一条工资收入正好覆盖收入和支出的两个统计口径。这样老师看折线图时一眼能看懂规律变化比留几十条假数据更保险。本文还有配套的精品资源点击获取