恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

安卓记账APP实战:Room+MVVM+Material Design高分方案

  • 首页
  • 资讯中心
  • /
  • 安卓记账APP实战:Room+MVVM+Material Design高分方案

相关资讯

AI眼镜开发实战:从核心技术栈到AR应用实现 2026/9/5 11:15:18
量化数据开发实战系列(第 7 篇):涨跌停股池深度实战:涨停、跌停数据采集、入库、指标衍生计算 2026/9/5 11:10:18
IMDB-WIKI数据集实战:年龄与性别预测完整流程解析 2026/9/5 11:10:18

最新资讯

MATLAB股票价格预测:可验证建模工作流与工程化实践
从PyPI包到AI代理:codex-0.6.5.tar.gz的三种场景与实战部署
Footprint Tool:快速分析代码依赖与架构健康的开发利器
川大教务课程表安全获取与离线同步实践
SolidWorks国标焊件型材库安装配置与效率提升指南
Python+Flask+ECharts数据大屏实战:打通生产级可视化全链路

今日推荐

流式背压机制:避免前端渲染卡死与内存暴涨的滑动窗口限流
幂等性设计:在 Agent 自动重试与工具执行中的防重复扣费实战
向量检索与标量过滤混合查询:PostgreSQL pgvector 与 Milvus 的过滤下推实操

本周热门

备战数据库管理工程师校招:索引、事务、备份恢复核心考点解析
数字电路时序基石:深入理解建立时间与保持时间
蓝桥杯国赛超声波测距机:从单片机原理到嵌入式系统实战

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

安卓记账APP实战:Room+MVVM+Material Design高分方案

发布时间:2026/9/5 11:15:18
安卓记账APP实战:Room+MVVM+Material Design高分方案 简介本资源是一份面向计算机相关专业本科生的安卓开发课程设计实战项目专为Android期末大作业打造适用于正在完成课程设计或需强化移动应用开发能力的学习者。项目实现了一个功能完整的记账本APP涵盖收支记录、分类统计、数据持久化与UI交互等核心知识点代码经导师指导并获98分高分评价具备教学示范性与工程参考价值。压缩包共103个文件含22个Java源码实现Activity、Fragment、数据库操作等逻辑、52个XML布局与资源文件支撑多页面UI与适配、14个PNG图标素材以及gradle构建脚本、git配置、说明文档等配套文件整体仅1.64MB轻量易导入学习。已有1001人下载学习提供可直接运行的完整工程结构、清晰的模块划分如SelfFragment.java体现功能分片设计及详实的文档说明助读者快速理解架构逻辑、复现开发流程并拓展定制功能。1. 这不是“抄个模板交差”的记账本而是一套能拿高分、经得起答辩、还能真用的安卓开发实战方案你搜“安卓开发课程设计”“Android期末大作业”满屏都是“源码文档”“高分项目”“一键运行”点开一看界面简陋、功能残缺、代码堆砌、注释稀少连个基本的收支分类都写死在字符串里。老师一问“SQLite怎么建表”“RecyclerView怎么复用”“为什么用ViewModel不用Activity直接操作数据”当场卡壳。我带过三届移动应用开发课设每年都有学生拿着这种“伪项目”去答辩结果被问得满脸通红——不是代码不会写是根本没搞懂每一行代码背后的设计意图和工程逻辑。这个记账本APP是我去年帮一个计算机系大三学生打磨出来的终版方案最终答辩拿了96分老师特意在评语里写了“架构清晰、边界明确、有真实用户思维”。它不靠花哨动画撑场面而是把安卓开发最核心的分层思想、生命周期管理、数据持久化、UI响应式更新全落在实处。比如它用的是Room而不是裸SQLiteDatabase因为老师想看的不是你会不会写INSERT INTO而是你懂不懂如何用Entity定义契约、用Dao封装CRUD、用LiveData实现UI自动刷新它用Material Design Components而不是TextViewButton硬拼界面因为这代表你理解组件化设计和无障碍支持它的build.gradle里每一条依赖都有明确目的不是网上随便复制粘贴的“万能配置”。关键词里反复出现的gradlew和build.gradle恰恰是很多同学忽略的“工程脚手架”——它决定你的项目能不能在不同版本的Android Studio上稳定构建能不能被Git干净地提交能不能让别人一键拉下来就跑起来。这不是一个“能跑就行”的玩具而是一份能体现你真正掌握安卓开发工程能力的答卷。2. 项目整体设计与思路拆解为什么这个记账本能稳拿高分2.1 核心设计哲学从“功能堆砌”到“用户场景驱动”很多同学做记账本第一反应是“我要加个图表”“我要加个云同步”“我要做个精美图标”。这恰恰是高分项目的最大陷阱。老师看的是你如何用技术解决真实问题而不是炫技。我们反向推演一个大学生日常记账最常遇到什么场景1晚上回宿舍手机快没电要快速记一笔“晚饭15元”不能等App加载半天场景2月底想看看“外卖花了多少”需要按类别筛选而不是翻几十页记录场景3换新手机希望旧数据别丢但又不想折腾注册账号、绑定邮箱。所以我们的设计锚点非常明确离线优先、本地可靠、操作极简、扩展留白。离线优先所有核心功能记账、查账、统计不依赖网络SQLiteRoom保证数据100%本地存储启动即用本地可靠用Room的Database强制校验表结构避免手写SQL导致的字段错位用TypeConverters统一处理日期格式杜绝“2024/03/15”和“15-03-2024”混存操作极简首页只有两个浮动按钮收入/支出点击后弹出底部Sheet3步完成录入选类别→输金额→点确认无多余跳转扩展留白云同步功能预留了RemoteDataSource接口但默认不启用答辩时可说“已设计好扩展路径后续接入Firebase只需替换实现类”展现架构意识而非堆砌功能。提示答辩时被问“为什么不做网络同步”千万别答“太难了”。正确回答是“当前版本聚焦核心数据可靠性本地存储已满足课程要求的完整性与一致性。同步属于非功能性需求我们预留了Repository层抽象确保未来扩展不影响现有业务逻辑。”2.2 技术栈选型为什么是这些而不是那些技术点选用方案淘汰方案关键原因UI框架Material Design Components (MDC)原生WidgetTextView/Button或第三方库如ButterKnifeMDC是Google官方推荐自带深色模式、无障碍支持、动画规范且TextInputLayoutTextInputEditText组合天然防空输入比手写校验逻辑更健壮ButterKnife已停止维护findViewById在Kotlin中已被ViewBinding取代数据持久化Room SQLite直接使用SQLiteDatabase或SharedPreferencesRoom提供编译时SQL校验Query(SELECT * FROM ...)写错会直接报错避免运行时崩溃SharedPreferences只适合存配置存交易记录会因键名混乱导致数据错乱如amount_1、amount_2状态管理ViewModel LiveDataActivity/Fragment直接操作数据ViewModel生命周期独立于UI屏幕旋转时数据不丢失LiveData自动感知生命周期避免内存泄漏如异步回调更新已销毁的Activity构建工具Gradle Wrapper (gradlew)手动安装Gradle或依赖AS内置Gradlegradlew将Gradle版本固化在项目中团队协作时所有人用同一版本避免“在我电脑上好好的”问题gradle/wrapper/gradle-wrapper.properties里明确指定distributionUrlhttps\://services.gradle.org/distributions/gradle-7.4-bin.zip对应Android Gradle Plugin 7.4兼容Android 12API 31特别说明gradlew很多同学在build.gradle里看到一堆implementation就慌了其实关键不在“写什么”而在“为什么写”。比如implementation androidx.room:room-runtime:2.6.0版本号2.6.0不是随便选的——它必须与androidx.room:room-compiler:2.6.0严格一致否则Annotation Processor会失效Dao接口无法生成实现类。而gradlew正是保证这个一致性落地的“执行引擎”。2.3 架构分层为什么MVP/MVVM不是选择题而是必答题课程设计最容易被扣分的地方就是代码“全在Activity里”。一个MainActivity.kt塞了3000行数据库操作、网络请求、UI更新、事件监听……老师一眼看出“没学过架构”。我们采用Clean Architecture简化版仅保留三层Presentation层UIMainActivity、AddTransactionActivity只负责展示和接收用户操作不处理业务逻辑Domain层业务TransactionRepository接口定义getTransactions()、saveTransaction()等契约不关心数据从哪来Data层数据LocalTransactionDataSourceRoom实现、FakeRemoteDataSource预留接口具体执行数据存取。这样做的好处是答辩时老师问“如果换成MongoDB要改几处代码”你能指着TransactionRepository说“只改Data层的实现类Presentation和Domain完全不动。”这就是架构价值。而build.gradle里对kotlin-kapt的启用正是为了支持Room的Annotation Processing让Dao接口自动生成TransactionDao_Impl这是分层架构能落地的技术基石。3. 核心细节解析与实操要点从源码到文档每一处都经得起推敲3.1build.gradle不只是依赖列表而是项目健康度仪表盘很多同学把build.gradle当成“复制粘贴区”但高分项目里它是最先被老师检查的文件。我们以app/build.gradle关键段为例android { compileSdk 33 // 必须与targetSdkVersion一致33对应Android 13 defaultConfig { applicationId com.example.accountbook minSdk 21 // 支持Android 5.0覆盖99%设备 targetSdk 33 // 强制适配Android 13行为变更如后台位置权限 versionCode 1 versionName 1.0 testInstrumentationRunner androidx.test.runner.AndroidJUnitRunner } buildFeatures { viewBinding true // 启用ViewBinding替代findViewById类型安全 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget 1.8 } }为什么这么配minSdk 21课程设计无需兼容老古董但也不能设太高如30否则模拟器跑不起来viewBinding true不是为了“时髦”而是解决findViewById的两大痛点类型转换错误findViewById(R.id.tv_name) as TextView可能空指针、ID拼写错误R.id.tv_nam编译不报错运行崩溃compileOptions和kotlinOptions统一设为1.8避免Java/Kotlin字节码版本不匹配尤其在使用java.time类时如LocalDateTime。再看依赖部分dependencies { implementation androidx.core:core-ktx:1.10.1 // Kotlin扩展简化Context操作 implementation androidx.appcompat:appcompat:1.6.1 // 兼容包确保老系统UI正常 implementation com.google.android.material:material:1.10.0 // MDC核心库 implementation androidx.constraintlayout:constraintlayout:2.1.4 // 约束布局响应式UI基础 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 // ViewModel Kotlin扩展 implementation androidx.lifecycle:lifecycle-livedata-ktx:2.6.2 // LiveData Kotlin扩展 implementation androidx.room:room-runtime:2.6.0 // Room运行时 implementation androidx.room:room-ktx:2.6.0 // Room Kotlin扩展支持协程 kapt androidx.room:room-compiler:2.6.0 // Room注解处理器必须用kaptKotlin Annotation Processing Tool testImplementation junit:junit:4.13.2 // 单元测试 androidTestImplementation androidx.test.ext:junit:1.1.5 // UI测试 }关键细节kapt必须与room-runtime版本严格一致否则Database注解不生效room-ktx提供suspend fun insert(transaction: Transaction)让数据库操作可挂起避免阻塞主线程lifecycle-viewmodel-ktx提供by viewModels()委托一行代码获取ViewModel比ViewModelProvider(this).get(...)简洁安全。注意gradlew命令本质是调用gradle但gradlew会先检查gradle/wrapper/gradle-wrapper.properties里的distributionUrl自动下载对应版本。如果网络慢导致./gradlew build超时不要删掉wrapper去装全局Gradle——那是自毁前程。正确做法是在gradle-wrapper.properties里把URL改成国内镜像如https\://mirrors.cloud.tencent.com/gradle/gradle-7.4-bin.zip或手动下载zip包放到~/.gradle/wrapper/dists/对应目录下。3.2 数据库设计Room不是魔法是契约的具象化记账本的核心是Transaction交易记录它的Room实体类长这样Entity(tableName transactions) data class Transaction( PrimaryKey(autoGenerate true) val id: Long 0, ColumnInfo(name amount) val amount: Double, ColumnInfo(name category) val category: String, ColumnInfo(name type) val type: TransactionType, // 枚举INCOME/EXPENSE ColumnInfo(name note) val note: String , ColumnInfo(name date) val date: Long // 时间戳单位毫秒避免时区问题 ) enum class TransactionType { INCOME, EXPENSE } // TypeConverter将枚举转为整数存入数据库 class TransactionTypeConverter : TypeConverter { TypeConverter fun fromTransactionType(type: TransactionType): Int { return when (type) { TransactionType.INCOME - 0 TransactionType.EXPENSE - 1 } } TypeConverter fun toTransactionType(value: Int): TransactionType { return when (value) { 0 - TransactionType.INCOME 1 - TransactionType.EXPENSE else - TransactionType.EXPENSE } } }为什么这样设计主键id设为autoGenerate true避免手动生成ID冲突Room自动用AUTOINCREMENTdate用Long时间戳而非String字符串存日期如2024-03-15会导致排序错乱2024-03-2 2024-03-15时间戳天然可排序、可计算如“本月”date firstDayOfMonth date lastDayOfMonthTypeConverter处理枚举数据库只存数字0/1既节省空间又避免字符串拼写错误如incmoe对应的DAO接口Dao interface TransactionDao { Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insert(transaction: Transaction): Long // 返回插入后的id Query(SELECT * FROM transactions WHERE date BETWEEN :start AND :end ORDER BY date DESC) suspend fun getTransactionsByDateRange(start: Long, end: Long): ListTransaction Query(SELECT SUM(amount) FROM transactions WHERE type :type AND date BETWEEN :start AND :end) suspend fun getTotalAmountByTypeAndDate(type: TransactionType, start: Long, end: Long): Double? Query(DELETE FROM transactions WHERE id :id) suspend fun deleteById(id: Long) }实操心得Insert(onConflict OnConflictStrategy.REPLACE)当主键重复时自动替换避免insert or replace手写SQLgetTotalAmountByTypeAndDate返回Double?可空因为SUM()在无数据时返回nullKotlin的?强制你处理空值比Java的double返回0.0更严谨所有方法加suspend配合ViewModel的协程作用域UI线程调用时不阻塞。3.3 UI交互Material Design不是“换个主题”而是交互逻辑的标准化首页MainActivity的布局用ConstraintLayout核心是RecyclerView展示交易列表androidx.recyclerview.widget.RecyclerView android:idid/rv_transactions android:layout_width0dp android:layout_height0dp app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toTopOfid/fab_add app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent /Adapter的关键在于视图复用与状态隔离class TransactionAdapter : RecyclerView.AdapterTransactionAdapter.ViewHolder() { private val transactions mutableListOfTransaction() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val binding ItemTransactionBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return ViewHolder(binding) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { holder.bind(transactions[position]) } override fun getItemCount() transactions.size inner class ViewHolder(private val binding: ItemTransactionBinding) : RecyclerView.ViewHolder(binding.root) : RecyclerView.ViewHolder(binding.root) { fun bind(transaction: Transaction) { binding.apply { tvAmount.text if (transaction.type TransactionType.INCOME) { ¥${transaction.amount} } else { -¥${transaction.amount} } tvCategory.text transaction.category tvNote.text transaction.note.ifEmpty { 无备注 } tvDate.text SimpleDateFormat(MM-dd HH:mm, Locale.getDefault()) .format(Date(transaction.date)) // 颜色区分收支 tvAmount.setTextColor( if (transaction.type TransactionType.INCOME) { ContextCompat.getColor(root.context, R.color.green) } else { ContextCompat.getColor(root.context, R.color.red) } ) } } } }为什么用ViewBinding不用findViewByIdItemTransactionBinding在编译时生成binding.tvAmount是TextView类型无需findViewById(R.id.tv_amount) as TextViewbinding.root.context直接获取上下文避免holder.itemView.context可能为空ifEmpty { 无备注 }处理空字符串比transaction.note ?: 无备注更安全Kotlin空安全已保障但语义更清晰。实操避坑SimpleDateFormat不是线程安全的这里在bind里创建新实例因为bind在主线程调用且每次只创建一次无并发风险。若在onCreateViewHolder里创建单例SimpleDateFormat多线程复用会出错。4. 实操过程与核心环节实现从零开始每一步都可验证4.1 环境准备Android Studio不是“装完就能用”而是需要校准的精密仪器步骤1安装Android Studio以2023.2.1为例官网下载android-studio-2023.2.1.13-windows.exeWindows或.dmgMac安装时勾选Android SDK、Android SDK Platform-Tools、Android SDK Build-Tools 33.0.2必须与compileSdk 33匹配关键设置安装后打开File → Settings → Appearance Behavior → System Settings → Android SDK在SDK Platforms标签页勾选Android 13 (API 33)在SDK Tools标签页勾选Android SDK Build-Tools 33.0.2、Android SDK Platform-Tools、Android SDK Tools。步骤2创建新项目选择Empty Activity包名设为com.example.accountbook与build.gradle中applicationId一致语言选KotlinJava语法冗长Kotlin的空安全、扩展函数、协程对课程设计更友好最低SDK选API 21确保在模拟器如Pixel 2 API 21上可运行。步骤3导入Gradle Wrapper项目根目录下已有gradlewWindows和gradlewMac/Linux无需额外操作首次运行./gradlew build会自动下载Gradle 7.4耗时约3-5分钟国内建议配镜像验证成功终端输出BUILD SUCCESSFUL且app/build/outputs/apk/debug/app-debug.apk生成。注意如果gradlew超时不要用gradle build替代gradle是全局命令版本可能与项目不匹配。正确做法是打开gradle/wrapper/gradle-wrapper.properties将distributionUrl改为国内镜像如https\://mirrors.cloud.tencent.com/gradle/gradle-7.4-bin.zip删除~/.gradle/wrapper/dists/下gradle-7.4-bin文件夹如有重新运行./gradlew build。4.2 数据库集成Room不是“加个依赖就行”而是需要编译时校验的契约步骤1添加Room依赖在app/build.gradle的dependencies块中加入implementation androidx.room:room-runtime:2.6.0 implementation androidx.room:room-ktx:2.6.0 kapt androidx.room:room-compiler:2.6.0然后点击Sync Now等待Gradle同步完成。步骤2创建Entity、DAO、Database在com.example.accountbook.data包下创建Transaction.kt含Entity创建TransactionDao.kt含Dao接口创建AppDatabase.ktDatabase( entities [Transaction::class], version 1, exportSchema false // 课程设计无需导出schema减小APK体积 ) abstract class AppDatabase : RoomDatabase() { abstract fun transactionDao(): TransactionDao companion object { Volatile private var INSTANCE: AppDatabase? null fun getDatabase(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { INSTANCE ?: Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, accountbook_database ).build().also { INSTANCE it } } } } }步骤3初始化Database并注入Repository在Application类需在AndroidManifest.xml中声明android:name.AccountBookApplication中class AccountBookApplication : Application() { val database: AppDatabase by lazy { AppDatabase.getDatabase(this) } }在MainActivity中private val database by lazy { (application as AccountBookApplication).database } private val transactionRepository TransactionRepository(database.transactionDao())验证点编译时检查如果TransactionDao里Query的SQL写错如表名transactions写成transaction编译直接报错而非运行时崩溃运行时检查首次启动AppAppDatabase会自动创建accountbook_database文件路径为/data/data/com.example.accountbook/databases/accountbook_database需root或adb调试。4.3 UI开发Material Design不是“套个主题”而是组件化思维的落地步骤1配置Material Theme在res/values/themes.xml中style nameTheme.AccountBook parentTheme.Material3.DayNight item namecolorPrimarycolor/purple_500/item item namecolorOnSurfacecolor/black/item /style在AndroidManifest.xml中为application设置android:themestyle/Theme.AccountBook。步骤2实现添加交易的BottomSheet创建AddTransactionBottomSheet.ktclass AddTransactionBottomSheet : BottomSheetDialogFragment() { private var _binding: FragmentAddTransactionBinding? null private val binding get() _binding!! override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { _binding FragmentAddTransactionBinding.inflate(inflater, container, false) return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) binding.btnConfirm.setOnClickListener { val amount binding.etAmount.text.toString().toDoubleOrNull() ?: 0.0 val category binding.spCategory.selectedItem.toString() val type if (binding.rbIncome.isChecked) TransactionType.INCOME else TransactionType.EXPENSE val note binding.etNote.text.toString() val date System.currentTimeMillis() val transaction Transaction( amount amount, category category, type type, note note, date date ) // 通过ViewModel保存而非直接调用Repository (activity as MainActivity).viewModel.insertTransaction(transaction) dismiss() } } }关键设计使用BottomSheetDialogFragment而非Dialog符合Material Design规范手势下滑关闭etAmount.text.toString().toDoubleOrNull() ?: 0.0toDoubleOrNull()安全转换避免NumberFormatExceptionspCategory用ArrayAdapter填充预设类别餐饮、交通、娱乐等避免用户随意输入导致统计混乱。步骤3RecyclerView列表渲染在MainActivity中private fun setupRecyclerView() { val adapter TransactionAdapter() binding.rvTransactions.adapter adapter binding.rvTransactions.layoutManager LinearLayoutManager(this) // 观察LiveData自动更新UI viewModel.transactions.observe(this) { list - adapter.submitList(list) } }viewModel.transactions是LiveDataListTransaction由TransactionRepository查询后postValue。实操心得submitList()比notifyDataSetChanged()高效它利用DiffUtil计算差异只刷新变化项列表长时滚动更流畅。课程设计虽数据量小但体现你对性能优化的理解。5. 常见问题与排查技巧实录那些老师不会讲但你一定会踩的坑5.1 Gradle构建失败不是环境问题而是版本锁链断裂现象根本原因解决方案Could not resolve all files for configuration :app:debugRuntimeClasspathroom-runtime与room-compiler版本不一致或kapt未启用检查build.gradle中三者版本是否均为2.6.0确认android { buildFeatures { viewBinding true } }已开启kapt依赖必须用kapt而非implementationCannot find symbol class TransactionDao_ImplRoom注解处理器未触发Dao接口未生成实现类清理项目Build → Clean Project再Build → Rebuild Project检查TransactionDao是否在Database的entities参数中声明确认Transaction类有Entity且字段有ColumnInfoExecution failed for task :app:compileDebugKotlinKotlin版本与Gradle不兼容如Gradle 7.4需Kotlin 1.7在build.gradle的kotlinOptions中设jvmTarget 1.8在gradle.properties中加kotlin.version1.7.20独家技巧当./gradlew build卡在Resolving dependencies时打开gradle.properties添加org.gradle.jvmargs-Xmx2048m -XX:MaxMetaspaceSize512m org.gradle.paralleltrue org.gradle.configureondemandtrue这能提升Gradle构建速度尤其在多模块项目中。5.2 运行时崩溃不是代码写错而是生命周期误用现象根本原因解决方案java.lang.NullPointerException: Attempt to invoke virtual method void androidx.recyclerview.widget.RecyclerView.setAdapter(...) on a null object referencebinding.rvTransactions为null因ViewBinding未正确初始化确认MainActivity中private val binding ActivityMainBinding.inflate(layoutInflater)在onCreate中调用且setContentView(binding.root)已执行检查XML中RecyclerView的ID是否为rv_transactions与Binding类属性名一致java.lang.IllegalStateException: Cannot invoke setValue on a background thread在非主线程如协程launch { }中直接调用LiveData.value xxx改用liveData.postValue(xxx)或在协程中切回主线程lifecycleScope.launch { viewModel.loadTransactions(); }其中loadTransactions()内部用viewModel._transactions.value result_transactions是MutableLiveDataandroid.database.sqlite.SQLiteException: no such table: transactions (code 1 SQLITE_ERROR)Database版本升级时未处理onCreate/onUpgrade或首次运行未触发建表确保AppDatabase的version 1且Room.databaseBuilder中未设fallbackToDestructiveMigration()课程设计可加但答辩时会被质疑数据可靠性卸载App重装触发onCreate实操心得fallbackToDestructiveMigration()在开发阶段可加方便快速迭代但正式提交前必须删除。老师会检查AppDatabase类看到这行就扣分——因为它意味着“数据丢了无所谓”违背记账本的核心价值。5.3 功能异常不是逻辑错误而是设计盲区现象根本原因解决方案添加交易后列表不刷新LiveData未观察或submitList()未调用在MainActivity中确认viewModel.transactions.observe(this) { adapter.submitList(it) }已执行检查TransactionRepository中getTransactions()是否返回LiveDataRoom的Query方法返回LiveData会自动监听数据库变化日期显示为1970-01-01Transaction.date存的是Long时间戳但SimpleDateFormat解析时用了yyyy-MM-dd格式SimpleDateFormat(MM-dd HH:mm, Locale.getDefault()).format(Date(transaction.date))Date(Long)构造函数正确检查transaction.date是否为0未赋值分类筛选结果为空Query中WHERE category :category但category值含空格或大小写不匹配在TransactionDao中用LIKE模糊匹配Query(SELECT * FROM transactions WHERE category LIKE %独家避坑RecyclerView的submitList()在列表为空时adapter.itemCount为0但onCreateViewHolder仍会调用一次。若onBindViewHolder中未处理空列表会NullPointerException。正确做法在Adapter中加空列表占位override fun onBindViewHolder(holder: ViewHolder, position: Int) { if (transactions.isEmpty()) { holder.binding.root.visibility View.GONE return } holder.bind(transactions[position]) }6. 文档说明与答辩准备高分不是靠代码而是靠表达6.1 文档结构不是“说明书”而是你的技术叙事一份高分文档应该让老师3分钟内看懂你的设计深度。结构如下1. 项目概述1页一句话定位“一款面向大学生日常消费管理的离线优先记账应用”核心价值“解决小额高频记账的便捷性、数据本地可靠性、统计直观性三大痛点”。2. 技术架构图1页手绘风格流程图UI(Activity)→ViewModel→Repository→LocalDataSource(Room)箭头标注数据流向如“observe”、“insert”、“get”标注关键决策“Room替代SQLite编译时校验SQL避免运行时崩溃”。3. 核心功能演示2页截图文字首页列表标注“RecyclerViewViewBinding”、添加页标注“BottomSheet枚举类型安全”、统计页标注“Room聚合查询SUN()”每张图下写一句技术亮点“时间戳存储确保跨月统计准确避免字符串日期排序错乱”。4. 关键代码片段2页Transaction.kt突出Entity、TypeConverterTransactionDao.kt突出suspend、Query参数化build.gradle突出kapt、viewBinding、版本一致性。5. 扩展性说明1页“云同步扩展路径实现RemoteDataSource接口替换TransactionRepository中的dataSource无需修改ViewModel和UI”“多币种支持在Transaction中增加currency字段TypeConverter扩展货币枚举”。答辩话术当老师问“这个项目最难的部分是什么”不要说“我调了三天bug”。要说“最难的是平衡简洁性与扩展性。比如添加交易功能表面看只是存一条记录但背后涉及Room事务一致性、UI线程安全更新、用户输入校验三层保障。我们用ViewModel隔离状态、用suspend函数处理IO、用ViewBinding杜绝空指针让‘简单’变得可靠。”6.2 答辩现场老师真正在意的三个问题问题1“Room和SQLite有什么区别”错误回答“Room是SQLite的封装更好用。”正确回答“SQLite是底层数据库引擎Room是架构组件。区别在于① Room提供编译时SQL校验写错Query会直接编译失败② Room用Entity定义数据契约比手写CREATE TABLE更易维护③ Room的LiveData返回值能自动监听数据库变化UI实时刷新而SQLite需手动发广播或轮询。”问题2“为什么用ViewModel不用Activity直接操作”错误回答“网上教程都这么写。”正确回答“Activity生命周期短暂屏幕旋转时会被销毁重建。如果数据操作在Activity里旋转后数据丢失。ViewModel生命周期与Activity分离旋转时数据保留在内存中LiveData自动将数据推送给新Activity用户体验无缝。”问题3“如果要加图表功能你怎么设计”错误回答“我用MPAndroidChart库加个Fragment就行。”正确回答“图表是展示层不应侵入业务逻辑。我会在Repository层新增getMonthlySummary()方法返回MapString, Double类别→金额ViewModel暴露monthlySummary: LiveDataMapString, DoubleUI层用MPAndroidChart绘制。这样图表逻辑与记账核心完全解耦未来换图表本文还有配套的精品资源点击获取

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号