1. 项目概述:从零打造一款实用的Android记账应用
作为一名在移动开发领域摸爬滚打多年的开发者,我见过太多记账应用要么功能过于简单,要么操作复杂得让人望而却步。这次要分享的是一个适合作为毕业设计的完整Android记账本实现方案,它不仅包含了基础记账功能,还融入了数据可视化、多账户管理等实用特性。这个项目特别适合计算机相关专业的学生作为毕业设计选题,因为它的技术栈覆盖了Android开发的多个核心知识点,同时业务逻辑清晰,不会过于复杂。
这个记账本的核心功能包括:日常收支记录、消费分类管理、月度统计报表、多账户切换等。技术上采用了MVVM架构,使用Room数据库进行本地数据存储,配合LiveData实现数据响应式更新,图表展示则使用了MPAndroidChart这个强大的开源库。整个项目代码结构清晰,注释完整,完全达到了毕业设计的规范要求。
提示:这个项目源码已经过脱敏处理,移除了所有敏感信息,可以直接作为学习参考。我会重点讲解设计思路和关键实现,而不仅仅是代码罗列。
2. 核心功能设计与技术选型
2.1 功能模块划分
在动手编码之前,合理的功能规划至关重要。我将这个记账本划分为以下几个核心模块:
-
用户认证模块:虽然是个单机应用,但我还是设计了简单的登录注册功能,为后续可能的云端同步预留接口。这里使用了Android的SharedPreferences存储用户凭证。
-
记账核心模块:
- 收支记录:支持金额、分类、时间、备注等基本信息
- 分类管理:用户可以自定义收支分类
- 账户管理:支持多账户切换(如现金、银行卡、支付宝等)
-
数据统计模块:
- 日/周/月报表
- 分类占比饼图
- 收支趋势折线图
-
系统设置模块:
- 主题切换
- 数据备份/恢复
- 分类默认设置
2.2 技术栈选择与考量
选择合适的技术栈是项目成功的关键。经过多方比较,我确定了以下技术方案:
-
架构模式:MVVM(Model-View-ViewModel)
- 优势:数据驱动UI,避免Activity/Fragment过于臃肿
- 实现:使用Android Architecture Components
-
数据库:Room
- 相比SQLiteOpenHelper:编译时SQL检查、LiveData集成
- 版本管理:通过Migration处理数据库升级
-
图表库:MPAndroidChart
- 功能丰富:支持饼图、折线图、柱状图等
- 定制性强:可以调整各种视觉参数
-
异步处理:Kotlin协程
- 替代RxJava:更轻量级,学习曲线平缓
- 配合Room使用非常方便
注意:虽然可以使用更复杂的架构如Clean Architecture,但对于毕业设计级别的项目,MVVM已经足够清晰且易于实现。不要过度设计,适合的才是最好的。
3. 数据库设计与实现
3.1 实体关系模型
良好的数据库设计是应用稳定的基础。我设计了以下几个核心实体:
kotlin复制@Entity(tableName = "accounts")
data class Account(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
val balance: Double,
val createTime: Long = System.currentTimeMillis()
)
@Entity(tableName = "categories")
data class Category(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String,
val type: Int, // 0=支出, 1=收入
val iconRes: String
)
@Entity(
tableName = "records",
foreignKeys = [
ForeignKey(
entity = Account::class,
parentColumns = ["id"],
childColumns = ["accountId"],
onDelete = ForeignKey.CASCADE
),
ForeignKey(
entity = Category::class,
parentColumns = ["id"],
childColumns = ["categoryId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Record(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val amount: Double,
val type: Int, // 0=支出, 1=收入
val categoryId: Long,
val accountId: Long,
val remark: String?,
val createTime: Long = System.currentTimeMillis()
)
3.2 DAO接口设计
使用Room的DAO接口可以方便地进行数据库操作:
kotlin复制@Dao
interface RecordDao {
@Insert
suspend fun insert(record: Record): Long
@Update
suspend fun update(record: Record)
@Delete
suspend fun delete(record: Record)
@Query("SELECT * FROM records WHERE accountId = :accountId ORDER BY createTime DESC")
fun getRecordsByAccount(accountId: Long): LiveData<List<Record>>
@Query("""
SELECT SUM(amount)
FROM records
WHERE accountId = :accountId
AND type = :type
AND createTime BETWEEN :start AND :end
""")
suspend fun getSumByTypeAndDateRange(
accountId: Long,
type: Int,
start: Long,
end: Long
): Double?
}
3.3 数据库初始化
在Application类中初始化数据库:
kotlin复制class MyApp : Application() {
val database: AppDatabase by lazy {
Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "accounting.db"
)
.addMigrations(MIGRATION_1_2)
.build()
}
companion object {
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE accounts ADD COLUMN color INTEGER NOT NULL DEFAULT 0")
}
}
}
}
实操心得:数据库版本迁移是个容易出错的地方。建议在开发初期就考虑可能的字段变更,提前规划好Migration策略。每次修改表结构都要记得增加版本号并提供对应的Migration。
4. UI设计与实现要点
4.1 主界面布局
采用标准的Material Design设计规范,主界面使用BottomNavigationView+ViewPager2的组合:
xml复制<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/bottomNavigation"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottomNavigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:menu="@menu/bottom_nav_menu" />
</androidx.constraintlayout.widget.ConstraintLayout>
4.2 记账表单实现
记账表单使用了TextInputLayout提升用户体验:
kotlin复制fun showRecordDialog(record: Record? = null) {
val dialog = MaterialAlertDialogBuilder(requireContext())
.setTitle(if (record == null) "新增记录" else "编辑记录")
.setView(R.layout.dialog_record_form)
.setPositiveButton("保存", null)
.setNegativeButton("取消", null)
.create()
dialog.setOnShowListener {
val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
positiveButton.setOnClickListener {
if (validateForm()) {
saveRecord()
dialog.dismiss()
}
}
}
dialog.show()
// 初始化表单控件和值
if (record != null) {
binding.etAmount.setText(record.amount.toString())
// 其他字段初始化...
}
}
4.3 图表展示实现
使用MPAndroidChart展示消费数据:
kotlin复制private fun setupPieChart(data: List<Pair<String, Double>>) {
val entries = data.mapIndexed { index, pair ->
PieEntry(pair.second.toFloat(), pair.first).apply {
// 设置颜色等属性
}
}
val dataSet = PieDataSet(entries, "消费分类").apply {
colors = ColorTemplate.MATERIAL_COLORS.toList()
valueTextSize = 12f
valueTextColor = Color.WHITE
}
binding.pieChart.apply {
data = PieData(dataSet)
description.isEnabled = false
legend.isEnabled = true
setEntryLabelColor(Color.BLACK)
animateY(1000, Easing.EaseInOutQuad)
invalidate()
}
}
注意事项:MPAndroidChart的性能在数据量较大时可能会下降。建议对显示的数据进行合理聚合,比如按月汇总而不是显示每天的详细数据。同时,在onDestroy中记得调用chart.clear()释放资源。
5. 核心业务逻辑实现
5.1 记账流程控制
记账的核心流程如下:
- 用户选择账户和记录类型(收入/支出)
- 填写金额、选择分类、添加备注(可选)
- 保存记录并更新账户余额
对应的ViewModel实现:
kotlin复制class RecordViewModel(private val repository: AccountingRepository) : ViewModel() {
private val _saveResult = MutableLiveData<Result<Unit>>()
val saveResult: LiveData<Result<Unit>> = _saveResult
fun saveRecord(record: Record) {
viewModelScope.launch {
try {
// 1. 保存记录
repository.insertRecord(record)
// 2. 更新账户余额
val account = repository.getAccount(record.accountId)
val newBalance = if (record.type == Record.TYPE_INCOME) {
account.balance + record.amount
} else {
account.balance - record.amount
}
repository.updateAccount(account.copy(balance = newBalance))
_saveResult.value = Result.success(Unit)
} catch (e: Exception) {
_saveResult.value = Result.failure(e)
}
}
}
}
5.2 数据统计与分析
按月统计收支情况的实现:
kotlin复制fun getMonthlySummary(year: Int, month: Int): LiveData<MonthlySummary> {
val calendar = Calendar.getInstance().apply {
set(year, month, 1, 0, 0, 0)
}
val startTime = calendar.timeInMillis
calendar.add(Calendar.MONTH, 1)
val endTime = calendar.timeInMillis - 1
return repository.getRecordsByDateRange(startTime, endTime).map { records ->
val income = records.filter { it.type == Record.TYPE_INCOME }.sumByDouble { it.amount }
val expense = records.filter { it.type == Record.TYPE_EXPENSE }.sumByDouble { it.amount }
val byCategory = records.groupBy { it.categoryId }
.mapValues { (_, records) -> records.sumByDouble { it.amount } }
MonthlySummary(
income = income,
expense = expense,
byCategory = byCategory,
startDate = startTime,
endDate = endTime
)
}
}
5.3 数据备份与恢复
使用Android的Storage Access Framework实现数据导出/导入:
kotlin复制fun backupData(uri: Uri) {
context.contentResolver.openOutputStream(uri)?.use { output ->
val records = database.recordDao().getAllSync()
val json = Gson().toJson(records)
output.write(json.toByteArray())
}
}
fun restoreData(uri: Uri): Boolean {
return try {
context.contentResolver.openInputStream(uri)?.use { input ->
val json = input.bufferedReader().readText()
val records = Gson().fromJson<List<Record>>(json)
database.recordDao().insertAll(records)
true
} ?: false
} catch (e: Exception) {
false
}
}
实操心得:数据备份是个容易被忽视但非常重要的功能。建议至少实现两种备份方式:本地文件导出和简单的云端备份(如Firebase)。同时要考虑数据版本兼容性问题,备份文件中最好包含版本信息。
6. 项目优化与扩展建议
6.1 性能优化技巧
-
数据库查询优化:
- 为常用查询字段添加索引
- 避免在UI线程执行数据库操作
- 使用Paging3库实现分页加载
-
图表性能优化:
- 限制显示的数据点数量
- 使用硬件加速
- 在后台线程准备图表数据
-
内存管理:
- 及时取消协程任务
- 对大图进行适当压缩
- 使用WeakReference持有Context
6.2 功能扩展方向
-
预算管理:
- 设置月度预算
- 超支提醒
- 预算执行情况分析
-
多设备同步:
- 基于Firebase实现
- 冲突解决策略
- 增量同步
-
智能分析:
- 消费习惯识别
- 异常消费提醒
- 节省建议
-
账单提醒:
- 周期性账单设置
- 还款提醒
- 自定义提醒规则
6.3 毕业设计答辩准备建议
-
文档整理:
- 需求规格说明书
- 设计文档
- 用户手册
- 测试报告
-
演示准备:
- 录制演示视频作为备份
- 准备典型用户场景
- 突出技术亮点
-
常见问题准备:
- 为什么选择这些技术?
- 遇到的最大挑战是什么?
- 如何保证数据安全?
- 有哪些可以改进的地方?
最后的小技巧:在项目根目录的README.md中详细记录开发环境和构建步骤,这样答辩时老师可以直接运行你的项目。同时,记得在代码中添加足够的注释,特别是关键算法和业务逻辑部分。