Android记账本开发实战:从Room数据库到MPAndroidChart图表集成
简介这是一款基于Android Studio开发的轻量级安卓记账本系统面向Android初学者与移动应用开发者解决个人日常收支记录、分类统计与账户管理等实际财务管理需求。资源包共70个文件包含16个Java源码实现登录注册、账单增删改查、SQLite本地存储等核心逻辑、22个XML布局与配置文件构建响应式UI界面、18个PNG图标资源支撑应用视觉体系以及APK安装包、MP4功能演示视频、DOCX运行说明文档等实用交付物整体压缩后仅10.68MB便于快速下载与本地调试。已有489人学习下载适合用于课程设计、毕业项目参考或Android开发技能实战训练。用户可直接安装APK体验完整功能亦可导入Android Studio研读源码掌握Activity生命周期管理、RecyclerView列表渲染、SQLite数据库操作及用户认证流程等关键开发技术。1. 项目概述一个安卓记账本系统的诞生最近在整理个人项目时翻出了一个几年前用Android Studio做的记账本应用。这个项目虽然不大但麻雀虽小五脏俱全从数据库设计、UI交互到数据统计完整地走了一遍安卓应用开发的核心流程。对于想入门安卓开发或者想找一个有实际意义的练手项目的朋友来说一个记账本系统是个绝佳的选择。它需求明确功能模块清晰既能覆盖基础开发技能又能让你体会到从零到一构建一个完整应用的成就感。这个基于Android Studio开发的安卓记账本系统核心目标就是帮助用户便捷地记录日常收支并通过清晰的图表进行财务分析。它适合有一定Java或Kotlin基础希望将理论知识付诸实践并渴望了解一个完整App开发全貌的开发者。即使你是新手跟着这个思路一步步走下来也能对安卓开发的四大组件、SQLite数据库、RecyclerView列表、MPAndroidChart图表库等核心工具有一个扎实的掌握。接下来我就把这个项目的设计思路、关键技术实现以及我踩过的那些“坑”详细拆解一遍。2. 整体架构与核心模块设计2.1 为什么选择记账本作为练手项目在决定做点什么的时候我考虑过很多方向天气预报、新闻客户端、简单的游戏。但最终选择了记账本主要是基于以下几个考量第一需求真实且高频。记账是很多人的实际需求这意味着你在开发时会不自觉地以“用户”视角思考比如“添加一笔记录要几步”、“怎么快速查看本月超支了没”这种产品思维对开发者至关重要。第二技术栈覆盖全面。一个完整的记账本几乎必然涉及以下核心安卓技术数据持久化需要本地数据库如SQLite来存储每笔账单记录。用户界面需要多个Activity/Fragment如主列表页、添加/编辑详情页、统计图表页。列表展示需要RecyclerView来高效展示大量的历史账单。数据绑定与交互需要在UI和数据模型之间建立联系处理用户的点击、长按等事件。图表可视化需要引入第三方库如MPAndroidChart来展示收支趋势、分类占比让数据更直观。第三功能模块边界清晰。记账本可以很自然地划分为“记录”、“管理”、“统计”三大模块每个模块功能相对独立便于分阶段开发和测试降低了项目复杂度。基于这些原因记账本项目成了一个完美的“教学案例”它能串联起安卓开发中多个孤立的知识点形成一个有机的整体。2.2 应用的核心功能模块拆解我的记账本系统主要设计了四个核心界面对应四大功能模块主页账单列表这是应用的入口和核心。以时间倒序展示所有账单记录每条记录清晰显示类型收入/支出、金额、分类、账户和备注。顶部有汇总信息如本月总收入、总支出和结余。这里的关键是RecyclerView的高效利用和数据实时刷新。记录编辑页添加/修改用于创建新的账单或修改已有记录。核心组件包括数字键盘自定义或使用系统键盘确保金额输入准确。分类选择器通常用下拉菜单或弹出对话框让用户选择“餐饮”、“交通”、“工资”等预设分类。日期时间选择器方便用户记录非当前时间的账单。账户选择记录这笔钱是从哪个银行卡、微信或支付宝流出的。备注输入框记录额外信息。 这个页面的重点是表单数据的验证与回显。统计页这是应用的“价值升华”部分。通过图表直观展示财务状况我主要实现了两种图表饼图展示某一时间段内如本月各个支出分类的占比一眼看出“钱花哪儿了”。折线图或柱状图展示收入与支出随时间按日或按月的变化趋势反映财务健康度。 这里需要学习如何集成MPAndroidChart库并从数据库中聚合、查询出图表所需的数据。分类管理页一个相对后台的功能允许用户自定义收入/支出的分类。比如增加“宠物”、“学习”等个性化分类。这涉及到对另一张数据库表的增删改查操作。2.3 技术选型与开发环境搭建工欲善其事必先利其器。以下是本项目主要的技术选型和工具以及我为什么这样选。开发工具Android Studio这是谷歌官方的IDE对安卓开发的支持最完善自带模拟器、代码提示、布局编辑器等是唯一推荐的选择。开发语言Kotlin虽然项目初期我用的是Java但现在我强烈推荐使用Kotlin。它语法更简洁、空安全特性避免了大量NullPointerException与现代安卓开发如Jetpack组件结合得更好。对于新项目没有理由再选Java。数据库SQLite Room Persistence Library记账数据需要本地持久化SQLite是安卓内置的轻量级数据库。但直接操作SQLite比较繁琐因此我使用Google Jetpack组件中的Room库。它是一个SQLite的对象映射封装让你能用注解和Kotlin/Java对象来操作数据库大大简化了代码并提供了编译时SQL语法检查。图表库MPAndroidChart这是一个功能强大且文档齐全的图表库支持线形图、柱状图、饼图、雷达图等。虽然集成步骤稍多但灵活性和效果远超自己绘制。它的API设计相对直观通过官方示例能较快上手。UI框架采用Jetpack Compose可选但推荐如果你开始的是一个全新项目并且想接触最前沿的安卓UI开发技术可以尝试Jetpack Compose。它用声明式的Kotlin API来构建UI比传统的XML布局方式更直观、代码更少。但请注意学习曲线稍陡且网络资源相对传统View系统少一些。对于求稳的首个项目使用传统的View XML布局仍然是完全可行且学习资料更丰富的选择。架构模式MVVMModel-View-ViewModel为了代码结构清晰、易于测试和维护我采用了MVVM架构。这是Google官方推荐的架构。Model代表数据和业务逻辑这里主要是Room数据库相关的实体Entity和数据访问对象Dao。ViewUI层即Activity和Fragment负责显示数据和接收用户输入。ViewModel作为View和Model之间的桥梁它持有UI相关的数据并在数据变化时通知View更新。它能在配置变更如屏幕旋转时存活保证数据不丢失。注意对于第一个完整项目如果觉得MVVM和Jetpack Compose一起学压力太大可以先用传统的View系统XML布局配合MVVM。把核心的数据流和生命周期管理掌握好之后再探索Compose。3. 数据库设计与核心数据模型数据库是记账本应用的“心脏”设计的好坏直接影响到后续所有功能的开发效率和应用的性能。3.1 实体Entity定义如何设计账单记录表我设计了三张核心表账单记录表、分类表、账户表。这里以最重要的BillRecord账单记录实体为例讲解Room库的使用。Entity(tableName bill_records, foreignKeys [ForeignKey(entity Category::class, parentColumns [id], childColumns [categoryId], onDelete ForeignKey.SET_NULL), ForeignKey(entity Account::class, parentColumns [id], childColumns [accountId], onDelete ForeignKey.SET_NULL)]) data class BillRecord( PrimaryKey(autoGenerate true) val id: Long 0, ColumnInfo(name type) val type: Int, // 0-支出 1-收入 ColumnInfo(name amount) val amount: Double, // 金额使用Double需注意精度实际项目中可考虑使用BigDecimal或以分为单位存储整数 ColumnInfo(name categoryId) val categoryId: Long?, // 关联分类ID ColumnInfo(name accountId) val accountId: Long?, // 关联账户ID ColumnInfo(name remark) val remark: String?, ColumnInfo(name date) val date: Long // 使用时间戳存储方便排序和按时间范围查询 )设计要点与避坑指南主键与自增PrimaryKey(autoGenerate true)让id自动增长无需手动管理。金额存储使用Double或Float存储金额在简单场景下可以但可能存在精度丢失问题如0.10.2 ! 0.3。更严谨的做法是将金额以“分”为单位用Long类型存储例如1.25元存为125在显示时再格式化为元。或者使用BigDecimal但Room对其支持需要类型转换器。时间存储使用Long类型的时间戳System.currentTimeMillis()存储日期远比存字符串“2023-10-27”要高效便于进行范围查询和排序。外键关联通过ForeignKey注解定义了与Category和Account表的关联。onDelete ForeignKey.SET_NULL表示当分类或账户被删除时对应的账单记录中的关联ID设为NULL避免数据不一致。这是数据库完整性的重要保障。空值处理remark备注和关联ID使用了可空类型String?和Long?符合实际场景备注可能为空账单可能不关联特定账户。3.2 数据访问对象DAO与数据库抽象定义了实体后我们需要定义访问它们的方法。这就是DAOData Access Object接口。Dao interface BillRecordDao { Insert suspend fun insert(record: BillRecord): Long // 返回插入行的ID Update suspend fun update(record: BillRecord) Delete suspend fun delete(record: BillRecord) // 查询所有记录按日期降序排列最新的在前 Query(SELECT * FROM bill_records ORDER BY date DESC) fun getAllRecords(): FlowListBillRecord // 查询指定月份的所有记录用于统计 Query(SELECT * FROM bill_records WHERE date :start AND date :end ORDER BY date DESC) fun getRecordsByTimeRange(start: Long, end: Long): FlowListBillRecord // 查询某分类下的总支出/收入用于饼图数据聚合 Query(SELECT SUM(amount) FROM bill_records WHERE categoryId :categoryId AND type :type AND date :start AND date :end) suspend fun getSumByCategoryAndTime(categoryId: Long, type: Int, start: Long, end: Long): Double? }核心技术解析协程与挂起函数所有数据库操作insert,update,delete, 复杂查询都被标记为suspend函数。这意味着它们必须在协程Coroutine或另一个挂起函数中调用。这保证了耗时IO操作不会阻塞主线程避免应用出现“应用程序未响应”ANR错误。这是现代安卓开发的标配。Flow响应式流注意getAllRecords()和getRecordsByTimeRange的返回类型是FlowListBillRecord。这是Room一个极其强大的特性。Flow是一种“冷流”当数据库中的bill_records表有任何数据变化增、删、改时这个Flow会自动发射新的数据列表。在UI层ViewModel中收集这个Flow就可以实现数据的实时、自动更新。当你在添加页面插入一条新记录后返回主页列表时列表会自动刷新无需手动调用刷新函数。灵活的QueryRoom允许你编写原生的SQL查询语句并通过:parameterName绑定方法参数非常灵活。这对于实现复杂的统计查询如上面的求和查询至关重要。3.3 创建数据库实例最后我们需要创建一个抽象的AppDatabase类它将所有实体和DAO组合起来。Database(entities [BillRecord::class, Category::class, Account::class], version 1, exportSchema false) abstract class AppDatabase : RoomDatabase() { abstract fun billRecordDao(): BillRecordDao abstract fun categoryDao(): CategoryDao abstract fun accountDao(): AccountDao companion object { // 单例模式避免重复打开数据库连接 Volatile private var INSTANCE: AppDatabase? null fun getDatabase(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, account_book_db // 数据库文件名 ).build() INSTANCE instance instance } } } }实操心得exportSchema false在开发初期可以设为false简化流程。如果需要记录数据库版本迁移的历史则应设为true并指定路径。单例模式确保整个应用只有一个数据库实例这是Room官方推荐的做法能有效管理资源。数据库升级当你的实体类发生变化如增加字段时需要增加version号并提供Migration对象。对于个人练手项目一种简单的但不推荐用于生产环境策略是销毁重建.fallbackToDestructiveMigration()。但在真正开发中必须妥善处理数据迁移。4. UI层实现与用户交互4.1 主页RecyclerView与数据绑定主页的核心是一个显示账单列表的RecyclerView。实现一个高效的RecyclerView通常包含以下几个部分列表项布局item_bill.xml设计每条账单记录的显示样式。通常包含类型图标、分类名称、金额、账户、日期和备注。可以使用ConstraintLayout进行灵活布局。ViewHolder继承自RecyclerView.ViewHolder持有列表项布局中各个视图的引用如TextView、ImageView避免每次绑定数据时都调用findViewById这是RecyclerView提升性能的关键。Adapter继承自RecyclerView.Adapter负责将数据列表ListBillRecord与ViewHolder绑定起来。在onBindViewHolder方法中将当前数据项设置到ViewHolder的视图上。现代做法使用ListAdapter替代RecyclerView.Adapter我推荐使用ListAdapter它内部使用了DiffUtil工具类能自动计算新旧数据集的差异并只更新发生变化的项而不是刷新整个列表性能更优。class BillListAdapter(private val onItemClick: (BillRecord) - Unit) : ListAdapterBillRecord, BillListAdapter.ViewHolder(BillDiffCallback()) { class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { // 持有视图引用 val tvAmount: TextView itemView.findViewById(R.id.tv_amount) val tvCategory: TextView itemView.findViewById(R.id.tv_category) // ... 其他视图 } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view LayoutInflater.from(parent.context).inflate(R.layout.item_bill, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val record getItem(position) holder.tvAmount.text formatCurrency(record.amount) // 格式化金额显示 holder.tvCategory.text getCategoryNameById(record.categoryId) // 根据ID查找分类名 // ... 绑定其他数据 holder.itemView.setOnClickListener { onItemClick(record) } } class BillDiffCallback : DiffUtil.ItemCallbackBillRecord() { override fun areItemsTheSame(oldItem: BillRecord, newItem: BillRecord): Boolean { // 判断是否为同一个Item通常用唯一ID return oldItem.id newItem.id } override fun areContentsTheSame(oldItem: BillRecord, newItem: BillRecord): Boolean { // 判断内容是否相同用于决定是否要更新该Item的视图 return oldItem newItem } } }数据绑定与LiveData/Flow在MainActivity或MainFragment对应的ViewModel中我们会从数据库获取FlowListBillRecord并将其转换为LiveData或直接在协程中收集然后提交给Adapter。// 在 MainViewModel 中 val allRecords: LiveDataListBillRecord billRecordDao.getAllRecords().asLiveData() // 在 MainFragment 中观察 viewModel.allRecords.observe(viewLifecycleOwner) { records - // 将新数据提交给ListAdapter adapter.submitList(records) }4.2 添加/编辑页表单处理与数据验证这个页面通常是一个新的Activity或Fragment。核心逻辑是数据回显编辑时如果是从主页点击某条记录跳转过来需要将记录的ID传入并在页面初始化时从数据库加载该记录填充到各个输入框中。数据保存用户点击保存按钮时收集所有输入框的数据构建一个BillRecord对象然后调用ViewModel的insert或update方法。输入验证这是容易忽略但很重要的一步。金额非空且大于0。分类必须选择。日期格式正确。 验证失败应在对应输入框旁给出明确提示如TextInputLayout.setError(“金额不能为空”)而不是简单的Toast。ViewModel中的保存逻辑class EditViewModel(private val billRecordDao: BillRecordDao) : ViewModel() { // 用于保存页面的临时数据可以使用DataBinding或StateFlow // ... fun saveRecord(record: BillRecord) { viewModelScope.launch { if (record.id 0L) { // 新记录 billRecordDao.insert(record) } else { // 更新记录 billRecordDao.update(record) } // 保存成功后可以通过LiveData或Event通知UI层关闭页面 } } }4.3 统计页集成MPAndroidChart统计页是展示数据分析结果的地方集成MPAndroidChart的步骤如下添加依赖在app/build.gradle.kts文件中添加依赖implementation com.github.PhilJay:MPAndroidChart:v3.1.0。布局文件中加入图表视图在XML中加入com.github.mikephil.charting.charts.PieChart ... /。准备图表数据在ViewModel中通过DAO的聚合查询如getSumByCategoryAndTime获取到用于绘制饼图的数据列表。数据需要转换为PieDataSet所需的ListPieEntry格式其中PieEntry(value, label)。配置与渲染// 在Fragment或Activity中 val entries ArrayListPieEntry() entries.add(PieEntry(300f, 餐饮)) entries.add(PieEntry(150f, 交通)) // ... 从数据库获取真实数据 val dataSet PieDataSet(entries, 支出分类) dataSet.colors ColorTemplate.MATERIAL_COLORS.toList() // 设置颜色 dataSet.valueTextSize 12f dataSet.valueTextColor Color.WHITE val pieData PieData(dataSet) pieChart.data pieData pieChart.description.isEnabled false // 关闭描述 pieChart.centerText 本月支出 // 中间文字 pieChart.animateY(1000) // 添加动画 pieChart.invalidate() // 刷新图表注意事项所有数据库查询和数据处理都应在后台线程协程中进行准备好数据后再在主线程更新UI。MPAndroidChart的配置项非常多建议从官方示例代码入手逐步调整到自己想要的样式。对于折线图/柱状图需要按时间维度日/月聚合收入和支出数据生成对应的LineDataSet或BarDataSet。5. 项目构建、调试与优化实录5.1 从零开始构建项目的步骤创建新项目打开Android Studio选择“Empty Activity”模板语言选择Kotlin最低API Level根据你的目标用户群选择如API 24可以覆盖大部分设备启用Jetpack Compose根据你的学习计划决定。添加依赖在app/build.gradle.kts的dependencies块中添加Room、Lifecycle用于ViewModel和LiveData、Coroutines等必要库。记得同步项目。设计数据库按照第3节内容创建Entity、DAO和Database类。实现Repository层可选但推荐在ViewModel和DAO之间增加一个Repository类用于集中处理数据来源本地数据库、未来可能的网络数据。这使架构更清晰。搭建UI界面从主页面开始依次实现列表页、编辑页、统计页的布局和逻辑。先让功能跑通再优化样式。连接数据在ViewModel中调用Repository或DAO获取数据并通过LiveData/StateFlow暴露给UI。在Activity/Fragment中观察数据并更新UI。测试与调试在模拟器或真机上运行测试增删改查所有功能。利用Android Studio的Logcat查看日志使用调试器定位问题。5.2 开发中遇到的典型问题与解决方案问题一数据库操作导致应用崩溃ANR现象在添加账单后点击保存应用卡顿甚至崩溃Logcat提示可能在主线程执行了数据库操作。排查检查所有调用billRecordDao.insert()等函数的地方是否在协程或后台线程中。解决确保所有DAO方法都是suspend函数并且在viewModelScope.launch { ... }或lifecycleScope.launch { ... }中调用。Room默认禁止在主线程执行数据库操作除非在构建数据库时调用.allowMainThreadQueries()但这强烈不推荐。问题二列表不更新或更新闪烁现象添加新记录后返回主页列表没有变化或者列表整体刷新有闪烁感。排查是否使用了ListAdapter和DiffUtil如果用的是普通RecyclerView.Adapter并调用notifyDataSetChanged()会导致整个列表重绘。在ViewModel中数据源是否是Flow或LiveData并且UI正确地观察了它是否在正确的生命周期范围内启动协程和观察数据例如在Fragment的onViewCreated中开始观察并使用viewLifecycleOwner。解决切换到ListAdapter。确保数据库操作成功后数据流能自动推送新数据。检查Room查询是否返回的是Flow。在Fragment中观察LiveData时使用viewLifecycleOwner而不是this防止因Fragment视图销毁而引发内存泄漏或崩溃。问题三MPAndroidChart图表不显示或数据错乱现象统计页面图表空白或数据显示不正确。排查数据为空检查从数据库查询聚合数据的逻辑是否正确查询的时间范围是否合理结果是否为null或空列表。给空数据情况设置一个默认显示如“暂无数据”。UI线程问题是否在主线程设置图表数据并调用invalidate()配置错误检查PieDataSet、LineDataSet等数据集的创建和赋值是否正确特别是entries列表。解决在给图表设置数据前先打印或Log一下准备的数据确认其正确性。确保图表操作在主线程。仔细阅读MPAndroidChart的文档和示例很多样式问题在示例中都有体现。问题四编辑页面传参复杂或类型安全现象从主页点击某条记录跳转到编辑页需要传递记录ID用Intent传递时担心类型错误或Key写错。解决使用Safe Args对于Navigation组件或显式定义Intent的Key常量。对于简单参数Safe Args是类型安全的首选。也可以考虑使用Bundle或直接将序列化后的对象通过Intent传递对象需实现Parcelable或Serializable。5.3 性能优化与体验提升点数据库索引如果账单记录表数据量变得非常大例如超过10万条在经常用于查询和排序的字段上建立索引可以大幅提升速度。例如经常按date字段排序和按时间范围查询可以为date字段添加索引ColumnInfo(name date, index true)。图片资源优化如果应用使用了图标确保使用适当尺寸的矢量图SVG转成的XML Vector Drawable或WebP格式以减少APK大小和内存占用。列表项复用优化确保RecyclerView的ViewHolder布局不要过于复杂嵌套层级过深会影响滚动性能。使用ConstraintLayout可以减少嵌套。内存泄漏预防在Fragment中观察LiveData时使用viewLifecycleOwner在协程中避免引用可能被销毁的Activity/Fragment上下文对于耗时操作确保在合适的生命周期如onCleared中取消协程。用户体验细节空状态提示当账单列表为空时显示一个友好的插画和文字提示引导用户添加第一条记录。操作反馈成功保存、删除后使用Snackbar可撤销操作或Toast给予轻量级提示。加载状态在数据加载时显示一个进度条或骨架屏避免界面空白。6. 功能扩展与进阶思考完成基础版本后这个记账本应用还有很大的扩展空间可以尝试加入以下功能来提升自己的技术数据备份与恢复将数据库文件导出到手机存储或云盘如手机内置的“文件”目录并支持从备份文件恢复。这涉及到文件系统操作和可能的数据迁移。多账户与账户间转账扩展账户模型支持记录账户间的资金转移这需要更复杂的业务逻辑和数据库事务处理。预算功能为每个分类设置月度预算并在主页或统计页用进度条显示预算使用情况超支时给出警告。数据可视化增强使用MPAndroidChart绘制更复杂的图表如结合收入支出的双柱状图或者显示净资产变化的曲线图。暗色主题遵循Material Design规范为应用添加暗色主题支持提升夜间使用体验。桌面小部件开发一个桌面小部件快速展示今日支出或快速添加入口。使用Jetpack Compose重写UI如果你最初是用XML布局实现的可以尝试用Compose重新实现一两个页面体验声明式UI的便捷。开发这个记账本系统的过程实际上是一个将安卓开发知识系统化、工程化的过程。从最初的需求分析、技术选型到中期的数据库设计、UI编码再到后期的调试优化和功能扩展每一步都会遇到具体的问题而解决这些问题的过程就是最有效的学习。我建议你在实现基本功能后不要就此停止挑一两个扩展功能去实践过程中你会发现对安卓架构组件、协程、状态管理等概念的理解会深刻得多。最后别忘了将代码托管到GitHub上一份清晰、完整的项目代码是你技术能力最好的证明之一。本文还有配套的精品资源点击获取

相关新闻

最新新闻

日新闻

周新闻

月新闻