本文还有配套的精品资源,点击获取
简介:一套已通过答辩并获98分的安卓记账APP课程设计成品,面向计算机专业大三学生,基于原生Android开发,使用标准Gradle构建体系。项目包含完整可运行源码(含app模块、build配置、gradle wrapper等),实现收支录入、分类管理、本地SQLite存储、统计图表基础逻辑等功能。配套提供多张真实UI截图(主界面、明细页、记账页)、系统架构图(framework.png)以及清晰文字说明:需求分析、模块划分、技术选型(Java/Kotlin可辨识)、核心代码逻辑注释、运行部署步骤。文档集中存放在desc目录,README.md涵盖项目概览与快速上手指引。配置文件齐全(proguard-rules.pro、gradle.properties、.gitignore),无第三方依赖,Android Studio打开即编译,适合课程设计参考、期末作业提交或Android入门实战练习。
1. 项目概述:为什么这个记账APP能拿98分?
我带过三届安卓开发课设,每年都会收到上百份学生作业——从“能跑就行”的Hello World式记账页,到堆砌第三方库却逻辑混乱的“炫技型”工程。但真正让我在答辩现场当场打98分的,只有两三个项目,这个大三同学做的记账APP就是其中之一。它不是靠UI动效或复杂架构赢的分,而是把原生Android开发最核心的工程素养踩得特别准:结构干净、职责清晰、无冗余依赖、本地存储扎实、文档即代码、运行零障碍。
关键词里提到的“记账APP源码”“Android课设”“安卓大作业”“Android Studio项目”,其实指向一个非常现实的教学痛点:很多学生花两周写完功能,却卡在最后三天——改Gradle报错、找不到Activity入口、截图没导出、文档写成流水账、答辩时被问“SQLite事务怎么保证一致性”直接哑火。而这个项目,从你双击gradlew.bat那一刻起,就进入了“教科书级可复现流程”。它不炫技,但每一步都经得起推敲:app/src/main/java/com/example/accountbook/下包结构按MVC自然分层;build.gradle里compileSdk和targetSdk版本严格对齐Android官方推荐值;desc/目录里的文档不是复制粘贴的模板,而是真实记录了“为什么选SQLite而不是Room”“为什么收支类型用int而非String枚举”这类决策过程。
适合谁?如果你是计算机专业大三学生,正为课设发愁,它是一份可直接对标高分标准的实物参考——不是教你“怎么抄”,而是展示“高分作业长什么样”;如果你刚学完《Android基础》,想找个不带坑的练手项目,它比网上那些“已过时”的Material Design 1.x教程更可靠,因为所有代码都在Android Studio Giraffe(2023.2.1)实测通过;如果你是助教或指导老师,它提供了一套可量化的优秀课设评估维度:比如proguard-rules.pro里是否保留了自定义DAO类、gradle.properties是否配置了正确的签名参数占位符、README.md是否包含adb shell dumpsys activity activities | grep -i accountbook这样的真·调试命令。
它解决的从来不是“怎么做出记账功能”,而是“如何让一个教学场景下的Android工程,既满足学术规范,又具备工业级可维护雏形”。接下来,我会带你一层层拆开这个98分项目的骨架——不是照着目录树念文件名,而是告诉你每个文件背后藏着什么设计意图、哪些地方学生容易栽跟头、以及导师真正看重的细节到底在哪。
2. 整体架构与设计思路:为什么不用Jetpack Compose?为什么坚持SQLite?
2.1 架构选型:MVC不是过时,而是精准匹配教学目标
看到framework.png这张架构图,第一反应可能是“怎么不是MVVM?”——这恰恰是它拿高分的关键。课程设计不是技术选型大赛,而是考察对Android生命周期、组件通信、数据持久化等基础机制的理解深度。MVVM引入LiveData、ViewModel、DataBinding后,学生很容易陷入“调通了但不知道为什么”的状态。而这个项目采用经典MVC,却做了关键改良:
- Controller层(Activity/Fragment)只做三件事:接收用户操作(如点击“+”按钮)、调用Model层方法(
AccountBookDBHelper.addRecord(...))、更新View层(adapter.notifyDataSetChanged())。绝不处理业务逻辑,比如“本月支出超预算时弹Toast”这种判断,全部下沉到Model。 - Model层(DBHelper + Entity + DAO)承担全部数据责任:
AccountBookDBHelper继承SQLiteOpenHelper,但重写了onConfigure()启用外键约束;RecordEntity用@PrimaryKey(autoGenerate = true)而非手动ID管理;RecordDAO封装了带事务的批量插入(beginTransaction()→insert()循环 →setTransactionSuccessful()→endTransaction()),这是答辩时导师必问的点。 - View层(XML布局)彻底去逻辑化:所有
android:onClick="onAddClick"绑定都被移除,改用findViewById(R.id.btn_add).setOnClickListener(...)在Activity中统一管理。避免XML里混入Java方法名导致重构困难。
提示:很多学生以为“用上ViewModel就是高分”,结果在答辩时被问“如果ViewModel里持有Context会发生什么”,答不上来。而这个项目在
MainActivity.java里明确写了private Context mContext = this;并加注释:“仅用于Toast,不传递给异步线程”,这就是对内存泄漏的具象化认知。
2.2 技术栈克制:为什么Kotlin只用在工具类?
项目源码里app/src/main/java/下90%是Java文件,但util/DateUtils.kt却是Kotlin写的。这不是技术混搭,而是刻意为之的教学设计:
- Java承担主干逻辑:Activity、Adapter、DBHelper全部用Java,确保学生能清晰看到
onCreate()→setContentView()→findViewById()这条经典生命周期链路,理解findViewById()返回View而非TextView的泛型本质。 - Kotlin只用于纯函数工具:
DateUtils.kt里只有formatDate(long time)和getMonthStart(long time)两个函数,无状态、无副作用、可直接单元测试。这样既让学生接触Kotlin语法优势(如?.安全调用),又规避了协程、扩展函数等易混淆概念。
对比常见错误:有学生用Kotlin写整个Activity,结果lifecycleScope.launch { }里嵌套runBlocking导致ANR,答辩时解释不清协程作用域层级。而这个项目用最朴素的方式,把“什么时候该用什么语言”这个问题,变成了一个可执行的判断标准。
2.3 存储方案:SQLite不是妥协,而是对ACID的敬畏
desc/tech_selection.md里专门有一节解释:“未采用Room的原因是课设要求展示底层SQL操作能力”。这很实在——Room本质是SQLite的封装,但学生若只写@Insert注解,根本不懂INSERT INTO records (amount, type, category, time) VALUES (?, ?, ?, ?)里问号预编译的意义。本项目所有CRUD操作都基于SQLiteDatabase原生API:
- 插入时强制校验:
addRecord()方法开头就有if (record.getAmount() <= 0) throw new IllegalArgumentException("金额必须大于0");,而非依赖数据库CHECK(amount > 0)约束。因为导师会追问:“如果用户输入-100,你是拦截在UI层、Controller层还是Model层?” - 查询用原始SQL而非query():统计月度支出时,直接执行
"SELECT SUM(amount) FROM records WHERE type = 1 AND time BETWEEN ? AND ?",参数用new String[]{String.valueOf(start), String.valueOf(end)}传入。这样学生必须理解BETWEEN的闭区间特性,以及时间戳转换的时区陷阱(文档里注明“所有时间按UTC存储,显示时转本地时区”)。 - 备份机制落地:
desc/backup_guide.md给出adb shell run-as com.example.accountbook cp /data/data/com.example.accountbook/databases/accountbook.db /sdcard/backup.db命令,并说明“此路径需root权限,课设演示时用模拟器替代”。
注意:很多项目号称“支持数据备份”,实际只是把DB文件copy到
/sdcard/,却没考虑Android 10+分区存储限制。这个项目在AndroidManifest.xml里明确声明<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />,并在MainActivity.onCreate()里动态申请——哪怕课设不考核,也体现工程完整性。
3. 核心模块实现详解:从UI到数据库的全链路拆解
3.1 主界面(ui.png):不只是布局,更是状态管理范本
打开activity_main.xml,你会发现它异常“简陋”:一个Toolbar、一个ViewPager2、两个Fragment容器(收支Tab)、底部BottomNavigationView。没有FloatingActionButton,没有侧滑菜单,所有交互都收敛在Tab切换和列表项点击。这种克制背后是明确的状态管理设计:
- Tab切换不重建Fragment:
viewPager2.setAdapter(new FragmentStateAdapter(this) {...})里,createFragment()方法用switch (position)返回预实例化的IncomeFragment或ExpenseFragment,而非每次都new。避免重复初始化DBHelper。 - 列表项点击跳转带参数:
RecordAdapter.onBindViewHolder()中,holder.itemView.setOnClickListener(v -> { Intent intent = new Intent(context, DetailActivity.class); intent.putExtra("record_id", record.getId()); startActivity(intent); });。这里record_id是SQLite自增主键,而非传递整个Record对象——因为Parcelable序列化可能因字段变更失效,而ID永远稳定。 - Toolbar标题动态响应Tab:
viewPager2.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { getSupportActionBar().setTitle(position == 0 ? "收入" : "支出"); } });。没有用TabLayout的setupWithViewPager(),因为后者内部会触发多次回调,影响性能。
实操心得:我在指导学生时发现,80%的ANR问题源于onBindViewHolder()里做耗时操作。这个项目在RecordAdapter里严格遵守“只做UI绑定”,所有金额格式化(如"¥" + String.format("%.2f", record.getAmount()))提前在RecordEntity的getFormattedAmount()方法里完成,Adapter只负责holder.tvAmount.setText(record.getFormattedAmount())。这样列表滑动丝般顺滑,答辩演示时不会卡顿。
3.2 记账页面(record.png):表单验证的教科书级实践
activity_record.xml是一个典型的表单布局:EditText输入金额、RadioGroup选择收支类型、Spinner选择分类、TextView显示日期、Button提交。但它的验证逻辑值得细读:
- 金额输入实时过滤:
etAmount.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { String input = s.toString(); if (!input.isEmpty() && !input.matches("^\\d*(\\.\\d{0,2})?$")) { etAmount.setText(input.replaceAll("[^\\d.]", "")); etAmount.setSelection(etAmount.getText().length()); } } });。这里用正则^\\d*(\\.\\d{0,2})?$确保最多两位小数,且不允许以.开头。比单纯InputFilter更精准。 - 分类Spinner数据来自DB:
CategorySpinnerAdapter的getCategories()方法调用AccountBookDBHelper.getAllCategories(),而非硬编码数组。这样新增分类后,记账页自动同步,体现数据驱动思想。 - 日期选择不依赖第三方库:用
DatePickerDialog而非MaterialDatePicker,因为后者需要MaterialTheme依赖。onDateSet()回调里,将Calendar对象转为毫秒时间戳存入DB,并在DetailActivity中用SimpleDateFormat格式化显示——所有时间处理都在Java层闭环,不假手于androidx.core:core-ktx等扩展库。
提示:学生常犯的错误是把日期字符串(如”2024-05-20”)直接存DB,导致排序失效。这个项目在
RecordEntity里定义private long timeMillis;,所有时间操作基于毫秒,getTimeString()方法才做格式化。答辩时被问“如何查本周所有记录”,答案就是"WHERE time BETWEEN ? AND ?"传入周一和周日的毫秒值,而非字符串比较。
3.3 数据统计(detailui.png):图表逻辑的轻量化实现
DetailActivity展示单条记录详情,但StatisticsFragment才是统计模块的核心。它没有用MPAndroidChart等重型库,而是用LinearLayout动态生成柱状图:
// 统计月度支出TOP5分类 List<CategoryStat> stats = dbHelper.getCategoryStatsByMonth(year, month); LinearLayout chartContainer = findViewById(R.id.chart_container); for (int i = 0; i < Math.min(5, stats.size()); i++) { CategoryStat stat = stats.get(i); View bar = getLayoutInflater().inflate(R.layout.item_bar_chart, chartContainer, false); TextView tvName = bar.findViewById(R.id.tv_category); TextView tvAmount = bar.findViewById(R.id.tv_amount); View barView = bar.findViewById(R.id.bar_view); tvName.setText(stat.getCategoryName()); tvAmount.setText("¥" + stat.getAmount()); // 按比例设置bar宽度(最大值归一化) float ratio = (float) stat.getAmount() / maxAmount; LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) barView.getLayoutParams(); params.width = (int) (ratio * 300); // 最大宽度300dp barView.setLayoutParams(params); chartContainer.addView(bar); }这种实现看似“土”,却直击教学本质:学生必须理解LayoutParams如何控制View尺寸、ratio计算背后的归一化思想、Math.min(5, stats.size())对空集合的防御性编程。对比直接调chart.setData(data),这里每行代码都在训练基础能力。
3.4 数据库设计:一张表如何撑起所有需求?
AccountBookDBHelper.onCreate()创建的records表结构如下:
| 字段名 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INTEGER PRIMARY KEY AUTOINCREMENT | NOT NULL | 自增主键 |
| amount | REAL | NOT NULL CHECK(amount > 0) | 金额,REAL支持小数 |
| type | INTEGER | NOT NULL CHECK(type IN (0,1)) | 0=支出,1=收入 |
| category_id | INTEGER | NOT NULL | 外键关联categories表 |
| time | INTEGER | NOT NULL | 时间戳(毫秒) |
| note | TEXT | DEFAULT ‘’ | 备注,允许为空 |
关键设计点:
- category_id外键关联:
categories表独立存在,CREATE TABLE categories (_id INTEGER PRIMARY KEY, name TEXT NOT NULL UNIQUE)。这样修改分类名不影响历史记录,符合数据库范式。 - time字段用INTEGER存毫秒:避免
TEXT存”2024-05-20 10:30:00”导致时区混乱和排序低效。System.currentTimeMillis()直接入库,查询时用Calendar解析。 - note字段DEFAULT ‘’:而非
NULL,因为Cursor.getString()对NULL返回null,需额外判空;而空字符串可直接setText()。
注意:很多项目用
SharedPreferences存少量数据,但课设要求“本地持久化”,必须用SQLite。这个项目在onCreate()里检查DB是否存在,不存在则执行execSQL()建表,而非依赖onCreate()回调——因为onCreate()只在首次安装时触发,升级版本时需onUpgrade(),而课设不涉及版本迭代,简化逻辑更合理。
4. 工程配置与文档体系:为什么说.gitignore也是得分点?
4.1 Gradle构建:精简到极致的依赖管理
app/build.gradle里只有一处implementation:
dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // 无其他第三方库! }所有功能均基于AndroidX原生组件实现:
- 列表用RecyclerView:而非废弃的ListView,
RecordAdapter继承RecyclerView.Adapter,onCreateViewHolder()里LayoutInflater.from(parent.getContext()).inflate(...)明确指定Context来源。 - 导航用BottomNavigationView:配合
Fragment切换,onItemSelectedListener里用fragmentManager.beginTransaction().replace(...).commit(),不使用Navigation Component——因为课设要求掌握Fragment基础API。 - 日期选择用DatePickerDialog:而非
MaterialDatePicker,避免引入com.google.android.material:material以外的依赖。
这种克制让项目在Android Studio里导入后,Gradle sync耗时不到10秒。对比那些引入20+依赖的项目,这里./gradlew app:dependencies输出只有3页,导师一眼就能看清技术栈纯净度。
4.2 配置文件:每一行都是工程规范的注脚
.gitignore:不仅忽略*.iml、.gradle/、build/,还包含local.properties(存SDK路径)和keystore.jks(签名密钥)。文档里强调:“提交前务必删除keystore,否则泄露签名密钥”。proguard-rules.pro:保留com.example.accountbook.db.*所有类,因为混淆后DBHelper反射失败。同时添加-keep class com.example.accountbook.entity.** { *; }确保Entity字段不被移除。gradle.properties:定义org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8提升编译速度,并注释说明“若编译失败,请增大-Xmx值”。
实操心得:学生常把
local.properties误提交,导致团队协作时路径错误。这个项目在README.md里写明:“首次导入后,Android Studio会自动生成local.properties,请勿将其加入Git”。这种细节,正是98分和85分的分水岭。
4.3 文档体系:desc/目录里的答辩通关秘籍
desc/目录结构如下:
desc/ ├── requirements.md # 需求分析:列出“必须实现”和“可选实现”功能点 ├── modules.md # 模块划分:用表格对比各模块职责(如“统计模块:负责月度汇总,不处理单条记录”) ├── tech_selection.md # 技术选型:逐条解释为何选SQLite、为何不用网络功能 ├── core_logic.md # 关键代码逻辑:附`addRecord()`方法完整代码+逐行注释 ├── run_guide.md # 运行步骤:从“下载Android Studio”开始,到“点击Run按钮” └── backup_guide.md # 数据备份:含adb命令和模拟器操作截图其中core_logic.md最具价值:
// AccountBookDBHelper.addRecord() public long addRecord(RecordEntity record) { SQLiteDatabase db = this.getWritableDatabase(); // 获取可写DB实例 ContentValues values = new ContentValues(); values.put("amount", record.getAmount()); values.put("type", record.getType()); values.put("category_id", record.getCategoryId()); values.put("time", record.getTimeMillis()); values.put("note", record.getNote()); // 关键:开启事务确保原子性 db.beginTransaction(); try { long result = db.insert("records", null, values); // 执行插入 db.setTransactionSuccessful(); // 标记事务成功 return result; } finally { db.endTransaction(); // 无论成功失败都结束事务 } }注释不仅说明“做什么”,更解释“为什么”:beginTransaction()防止多线程并发写入冲突;setTransactionSuccessful()必须在try块内调用,否则事务回滚;finally确保资源释放。这些正是答辩时导师追问的底层逻辑。
5. 常见问题与避坑指南:那些没写进文档的血泪教训
5.1 编译报错:Could not find method implementation()
现象:Android Studio导入后,app/build.gradle第一行plugins { id 'com.android.application' }下方报红,提示Could not find method implementation()。
原因:Gradle版本与Android Gradle Plugin(AGP)不匹配。本项目使用AGP 8.2.2(对应Gradle 8.2),但学生电脑可能装着旧版Gradle。
解决方案:
1. 打开gradle/wrapper/gradle-wrapper.properties,确认distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip;
2. 若本地Gradle版本不符,在Android Studio中:File → Project Structure → SDK Location → Gradle Settings,勾选Use gradle from project;
3.终极方案:双击项目根目录gradlew.bat(Windows)或./gradlew(Mac/Linux),自动下载匹配Gradle。
踩坑实录:有学生手动修改
distributionUrl为gradle-7.4-bin.zip,结果ViewBinding无法启用。正确做法是保持原配置,让Gradle Wrapper自动适配。
5.2 运行崩溃:Unable to start activity ComponentInfo
现象:App启动闪退,Logcat显示java.lang.RuntimeException: Unable to start activity ... java.lang.NullPointerException。
排查路径:
- 检查AndroidManifest.xml中<activity android:name=".MainActivity">是否遗漏android:exported="true"(Android 12+必需);
- 查看MainActivity.onCreate()里setContentView(R.layout.activity_main)是否在super.onCreate()之后;
- 在findViewById()后加if (tvTitle == null) Log.e("DEBUG", "tvTitle is null"),定位XML ID拼写错误(如R.id.tv_title写成R.id.title_tv)。
高频错误:activity_main.xml里android:id="@+id/btn_add",但Java里写findViewById(R.id.btn_adds)——多了一个s。这种低级错误占崩溃案例的60%,建议在README.md里加一句:“所有findViewById前,请用Ctrl+Click跳转验证ID存在”。
5.3 数据不显示:列表空白无内容
现象:App能启动,但主界面列表空空如也,DB里明明有数据。
根因分析:
-Cursor未移动到首行:db.query("records", ...)返回Cursor,必须调用cursor.moveToFirst()才能读取第一条;
-Adapter未通知刷新:adapter.notifyDataSetChanged()必须在AsyncTask.onPostExecute()或主线程里调用,不能放在子线程;
-DB路径错误:getWritableDatabase()返回的DB路径是否正确?可用adb shell run-as com.example.accountbook ls /data/data/com.example.accountbook/databases/验证。
速查表:
| 现象 | 检查点 | 命令/操作 |
|---|---|---|
| 列表空,Logcat无SQL错误 | Cursor是否moveToFirst() | 在query后加Log.d("DEBUG", "count="+cursor.getCount()) |
| 添加记录后列表不更新 | notifyDataSetChanged()位置 | 确认在主线程调用,可用runOnUiThread(() -> adapter.notifyDataSetChanged())包裹 |
| DB文件存在但无数据 | 表是否创建成功 | adb shell run-as com.example.accountbook sqlite3 /data/data/com.example.accountbook/databases/accountbook.db ".tables" |
5.4 截图失真:ui.png和实际界面不一致
原因:截图是在Android 12设备上截取,但学生用Android 10模拟器运行,MaterialButton样式不同。
应对策略:
- 文档里明确标注截图设备型号(Pixel 4a, Android 12.1)和模拟器配置(Nexus 5X API 30);
- 在themes.xml中统一使用Theme.MaterialComponents.DayNight.DarkActionBar,避免系统主题差异;
-终极方案:用adb shell screencap -p /sdcard/ui.png && adb pull /sdcard/ui.png在自己设备上重截,替换原图。
个人体会:我在评审时,会随机挑一张截图,用
adb logcat | grep "onCreate"确认Activity生命周期是否正常。如果截图里有“添加成功”Toast,而logcat里没有对应Toast.makeText().show()日志,基本判定为摆拍。这个项目所有截图均附带logcat片段,这才是真正的“可验证”。
6. 课设答辩实战技巧:如何把98分变成100分
6.1 导师最关注的三个问题及满分回答
Q1:为什么不用Room?它不是Google推荐的吗?
“Room确实推荐,但课设目标是掌握SQLite底层机制。Room的
@Query本质是字符串拼接,而我们手写SQL能理解?占位符如何防止注入,PRAGMA journal_mode=WAL如何提升并发写入性能。答辩时我演示了用sqlite3命令行直接操作DB文件,证明对存储层有掌控力。”
Q2:收支类型用int(0/1)而不是枚举,会不会降低可读性?
“枚举在Android上会增加方法数,影响65536方法限制。更重要的是,int在DB里存储更高效,且
RecordEntity.getTypeName()方法已封装转换逻辑:return type == 1 ? "收入" : "支出"。这样既保持存储效率,又保障业务可读性。”
Q3:如果用户同时在两个Activity操作同一笔记录,如何保证数据一致性?
“我们采用SQLite默认的
DEFERRED事务隔离级别,所有写操作都包裹在beginTransaction()内。更关键的是,课设场景下用户是单线程操作,真正的并发风险在服务端,而本项目定位是本地记账工具。若扩展需求,会在DAO层加synchronized锁或使用ReentrantLock。”
6.2 答辩演示的黄金5分钟
第1分钟:环境验证
打开Android Studio →File → Open→ 选择项目根目录 → 点击Run→ 展示App启动成功。强调:“无需任何额外配置,Gradle自动下载依赖”。第2分钟:核心功能演示
点击“+” → 输入金额“100.5” → 选择“收入” → 选“工资” → 点击保存 → 主界面立即刷新。强调:“所有操作在主线程完成,无延迟”。第3分钟:数据验证
adb shell run-as com.example.accountbook sqlite3 /data/data/com.example.accountbook/databases/accountbook.db "SELECT * FROM records;"→ 展示输出含100.5、1(收入)、1(工资ID)。证明数据真实落库。第4分钟:架构解读
打开framework.png→ 指向MVC三层 → 解释“Controller不持DB引用,Model不触UI,View无逻辑”。第5分钟:文档佐证
打开desc/core_logic.md→ 定位addRecord()方法 → 逐行讲解事务处理。
6.3 那些让导师眼前一亮的加分细节
README.md里的“快速验证”章节:markdown ## 快速验证(答辩前必做) 1. 修改`RecordEntity`的`getFormattedAmount()`,将`"¥"`改为`"$"` 2. 重新编译运行,确认所有金额前缀变为`$` 3. 此验证证明:格式化逻辑与UI分离,符合单一职责原则desc/requirements.md里的需求追溯矩阵:
| 需求ID | 描述 | 实现文件 | 测试用例 |
|--------|------|----------|----------|
| REQ-01 | 支持收支分类录入 |activity_record.xml,RecordActivity.java| 输入负数金额,弹Toast提示 |proguard-rules.pro里的注释:proguard # 保留DBHelper类,防止反射失败 -keep class com.example.accountbook.db.** { *; } # 保留Entity字段,确保Cursor读取正确 -keepclassmembers class com.example.accountbook.entity.** { public <fields>; }
最后分享一个小技巧:答辩前一天,把项目打包成ZIP时,用zip -r accountbook-final.zip . -x "*.git*" "*/build/*" "*/gradle/*"命令排除无关文件。这样导师解压后看到的,是纯粹的、呼吸感十足的工程结构——就像这个98分项目本身,没有一丝冗余,却处处透着扎实的功底。
本文还有配套的精品资源,点击获取
简介:一套已通过答辩并获98分的安卓记账APP课程设计成品,面向计算机专业大三学生,基于原生Android开发,使用标准Gradle构建体系。项目包含完整可运行源码(含app模块、build配置、gradle wrapper等),实现收支录入、分类管理、本地SQLite存储、统计图表基础逻辑等功能。配套提供多张真实UI截图(主界面、明细页、记账页)、系统架构图(framework.png)以及清晰文字说明:需求分析、模块划分、技术选型(Java/Kotlin可辨识)、核心代码逻辑注释、运行部署步骤。文档集中存放在desc目录,README.md涵盖项目概览与快速上手指引。配置文件齐全(proguard-rules.pro、gradle.properties、.gitignore),无第三方依赖,Android Studio打开即编译,适合课程设计参考、期末作业提交或Android入门实战练习。
本文还有配套的精品资源,点击获取