简介:这是一份面向高校学生与Android初学者的移动开发期末大作业完整资料,主题为记账类App,适合需要完成课程设计、实训项目或自学Java安卓开发的人群参考。资源包共1065个文件,约19.33MB,涵盖36个java源码文件、262个xml布局与配置、146张png及webp图片素材、130个json数据文件,以及apk安装包、gradle构建脚本、jar依赖库等,完整保留了Android Studio工程结构与可运行产物。项目实现了收支记录、分类账目管理、数据库存储、记录隐藏与删除、图表化收支分析及一键清空数据等功能,并附有运行截图便于对照界面效果。目前已有3630人学习下载,读者可借此获得一套可直接编译运行的课程设计范例,理解Java语言下Activity、SQLite数据库与图表组件的实际配合方式,同时参考评分较高的实现思路与目录组织,用于快速搭建自己的记账应用或完成答辩准备。
1. 安卓期末大作业记账app:从源码到导出APK,一套能交差的完整链路
每年期末季,安卓开发课的群里总会冒出同一个问题:大作业做什么最稳?我的答案是记账app。原因很直接——它覆盖了安卓课程里几乎全部核心考点:Activity生命周期、RecyclerView列表、SQLite本地存储、Fragment切换、自定义Dialog、数据统计图表。老师一看就知道你确实动手写了,而不是从网上扒了个商城项目改了个名字。但真正让大多数人翻车的不是功能本身,而是最后一步:源码在Android Studio里跑得好好的,导出APK装到手机上要么闪退,要么白屏,要么数据存不进去。这篇内容就是围绕「安卓期末大作业-记账app」这条链路,把源码结构、关键实现、导出APK的完整流程和运行截图该截什么,一次讲透。适合正在赶大作业的在校生,也适合想拿一个轻量项目练手安卓开发的新手。下面从项目结构开始拆。
2. 记账app的源码结构怎么搭:从工程目录到数据库设计
2.1 工程目录与技术选型:为什么不用Room而用SQLite
拿到一个安卓期末大作业的记账app,第一件事是看它的工程结构是否清晰。我一般会按下面的方式组织目录,这也是最常见、老师最容易看懂的搭法:
app/src/main/java/com/example/accountbook/ ├── MainActivity.java // 主界面,承载Fragment ├── AddRecordActivity.java // 添加账目 ├── EditRecordActivity.java // 编辑账目 ├── adapter/ │ └── RecordAdapter.java // RecyclerView适配器 ├── db/ │ └── DBHelper.java // SQLiteOpenHelper ├── model/ │ └── Record.java // 账目实体类 ├── fragment/ │ ├── HomeFragment.java // 首页流水 │ └── StatsFragment.java // 统计页 └── util/ └── DateUtil.java // 日期格式化工具选型上有一个常见纠结:用Room还是裸SQLite?对于期末大作业,我的建议是裸SQLite。原因有三点。第一,课程通常教的就是SQLiteOpenHelper,老师看代码时能直接对应上知识点。第二,Room需要引入annotationProcessor,Gradle配置一旦出问题,新手排查成本很高。第三,裸SQLite的增删改查逻辑全部可见,答辩时你能逐行讲清楚,不会被问住。Room适合真实项目,但期末作业的核心是展示你理解了安卓本地存储的基本原理。
注意:如果你的课程明确要求使用Jetpack组件,那就老老实实上Room,不要跟分数过不去。
2.2 数据库表设计与DBHelper的关键参数
记账app的数据模型不复杂,一张表就够了。字段设计如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | INTEGER PRIMARY KEY AUTOINCREMENT | 主键,自增 |
| amount | REAL | 金额,支持小数 |
| type | INTEGER | 0支出 1收入 |
| category | TEXT | 分类名称,如餐饮、交通 |
| note | TEXT | 备注 |
| date | TEXT | 日期,格式yyyy-MM-dd HH:mm |
建表语句和DBHelper的核心代码如下:
public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "account.db"; private static final int DB_VERSION = 1; public static final String TABLE_NAME = "record"; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } @Override public void onCreate(SQLiteDatabase db) { String sql = "CREATE TABLE " + TABLE_NAME + " (" + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + "amount REAL NOT NULL, " + "type INTEGER NOT NULL, " + "category TEXT, " + "note TEXT, " + "date TEXT)"; db.execSQL(sql); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS " + TABLE_NAME); onCreate(db); } }这里有几个参数值得说清楚。DB_VERSION设为1,意味着你第一次发布。如果后续改了表结构,必须把版本号加1,否则onUpgrade不会触发,用户升级后会出现「列不存在」的崩溃。amount用REAL而不是INTEGER,因为记账金额经常有小数,用整数会丢精度。date存字符串而不是时间戳,是为了查询和显示方便,期末作业不需要考虑时区问题。
提示:onUpgrade里直接DROP TABLE会导致用户数据丢失。真实项目要做迁移,但期末作业里这样写没问题,答辩时主动说明「生产环境需要做数据迁移」反而是加分项。
2.3 增删改查的封装与RecyclerView数据绑定
DBHelper建好之后,增删改查的封装决定了你后面写Activity时顺不顺手。我一般会在DBHelper里直接加方法,而不是再抽一层Dao,因为期末作业的体量不值得过度设计。
public long insert(Record r) { SQLiteDatabase db = getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("amount", r.getAmount()); cv.put("type", r.getType()); cv.put("category", r.getCategory()); cv.put("note", r.getNote()); cv.put("date", r.getDate()); long id = db.insert(TABLE_NAME, null, cv); db.close(); return id; } public List<Record> queryAll() { List<Record> list = new ArrayList<>(); SQLiteDatabase db = getReadableDatabase(); Cursor cursor = db.query(TABLE_NAME, null, null, null, null, null, "date DESC"); while (cursor.moveToNext()) { Record r = new Record(); r.setId(cursor.getInt(cursor.getColumnIndexOrThrow("id"))); r.setAmount(cursor.getDouble(cursor.getColumnIndexOrThrow("amount"))); r.setType(cursor.getInt(cursor.getColumnIndexOrThrow("type"))); r.setCategory(cursor.getString(cursor.getColumnIndexOrThrow("category"))); r.setNote(cursor.getString(cursor.getColumnIndexOrThrow("note"))); r.setDate(cursor.getString(cursor.getColumnIndexOrThrow("date"))); list.add(r); } cursor.close(); db.close(); return list; }queryAll里排序用date DESC,保证最新的账目在最上面,这是记账app的基本体验。注意cursor用完必须close,db也要close,否则多次操作后会出现「database connection pool full」的异常。RecyclerView的Adapter里,onBindViewHolder根据type字段设置金额颜色:支出显示红色带负号,收入显示绿色带正号。这个细节虽小,但运行截图里一眼就能看出来,老师会觉得你用心了。
3. 导出APK的完整流程:从签名配置到真机安装
3.1 生成签名密钥并配置build.gradle
源码在Android Studio里跑通只是第一步,导出APK才是期末作业交差的关键。很多人卡在「Build APK」之后装到手机上提示「应用未安装」或「解析包错误」,根本原因通常是签名问题。Debug版APK虽然能装,但有些手机的安全策略会拦截,而且老师如果要求「正式签名」你就交不了差。
第一步,生成签名密钥。在Android Studio菜单栏选择Build → Generate Signed Bundle / APK → APK → Create new。填写密钥库路径、密码、别名等信息。也可以用命令行生成:
keytool -genkeypair -v -keystore account.jks -keyalg RSA -keysize 2048 -validity 10000 -alias account参数说明:-keystore指定密钥库文件名,-keyalg RSA用RSA算法,-keysize 2048是密钥长度,-validity 10000表示有效期10000天,-alias account是密钥别名。执行后会提示你输入密码和姓名等信息,随便填就行,期末作业不需要真实身份。
第二步,在app模块的build.gradle里配置signingConfigs:
android { signingConfigs { release { storeFile file('../account.jks') storePassword '123456' keyAlias 'account' keyPassword '123456' } } buildTypes { release { minifyEnabled false signingConfig signingConfigs.release } } }minifyEnabled设为false,因为期末作业不需要代码混淆,开了反而可能因为反射导致崩溃。storeFile的路径用相对路径,方便把整个工程拷给老师后还能编译。
3.2 构建Release APK并处理常见报错
配置好签名后,执行构建命令:
./gradlew assembleReleaseWindows下用gradlew.bat assembleRelease。构建成功后,APK会生成在app/build/outputs/apk/release/目录下,文件名通常是app-release.apk。
这一步常见的报错有三个。第一个是Keystore file not found,说明storeFile路径写错了,检查相对路径的基准是app模块还是根目录。第二个是password verification failed,说明storePassword或keyPassword填错了,注意这两个密码可以不同。第三个是Lint found fatal errors,这是Lint检查拦截了构建,可以在build.gradle的android块里加lintOptions { checkReleaseBuilds false }临时绕过,但更好的做法是点开报错看具体问题,通常是无用资源或缺少内容描述。
注意:如果你的项目用了第三方库,Release构建时可能因为混淆规则缺失而崩溃。minifyEnabled false已经规避了这个问题,但如果你开了混淆,记得在proguard-rules.pro里保留模型类和数据库相关类。
3.3 真机安装与运行截图该截哪几张
APK构建出来后,传到手机上安装。如果提示「应用未安装」,先检查手机是否允许安装未知来源应用,再检查APK的minSdkVersion是否高于手机系统版本。如果安装成功但打开闪退,用adb logcat看崩溃日志,最常见的原因是数据库操作在主线程之外没有处理好Context,或者某个findViewById返回了null。
运行截图是期末作业的硬性要求,通常需要3到5张。我建议截这几张:第一张,首页流水列表,展示多条账目记录,金额有正有负,颜色区分明显。第二张,添加账目界面,展示分类选择、金额输入、日期选择器。第三张,统计页面,展示饼图或柱状图,体现数据汇总能力。第四张,删除或编辑操作的确认弹窗,体现交互完整性。第五张,关于页面或设置页面,展示版本号和作者信息。
截图时注意两点:一是用真机截图,不要用模拟器,模拟器的状态栏一眼就能看出来;二是数据要真实,提前录入十几条不同分类的账目,不要只有两三条空荡荡的列表。老师看截图的第一印象就是「这个学生有没有认真跑过」。
4. 避坑与排查:记账app从编译到运行最容易翻车的5个地方
4.1 数据库版本号没改导致升级后崩溃
现象:第一次安装运行正常,改了表结构后重新安装,app一打开就闪退,日志显示no such column: xxx。
原因:DB_VERSION还是1,onUpgrade没有被触发,旧数据库文件里没有新列。
解决:每次修改表结构,把DB_VERSION加1,并在onUpgrade里写对应的ALTER TABLE或DROP后重建。期末作业如果不想处理迁移,可以在onUpgrade里直接删表重建,但要提醒自己这只是作业做法。
4.2 RecyclerView不显示数据但日志无报错
现象:数据库里明明有数据,RecyclerView就是空白,Logcat没有任何异常。
原因:Adapter的getItemCount返回了0,或者数据列表在setAdapter之后才加载,没有调用notifyDataSetChanged。
解决:先确认getItemCount返回的是list.size(),再确认数据加载完成后调用了adapter.notifyDataSetChanged()。如果用的是Fragment,注意在onViewCreated里初始化RecyclerView,不要在onCreateView里做数据绑定。
4.3 导出APK后安装提示「解析包错误」
现象:Android Studio里直接Run没问题,导出APK传到手机安装时提示「解析包错误」。
原因:最常见的是minSdkVersion设置过高,手机系统版本低于最低要求。其次是APK文件传输过程中损坏,或者签名不一致导致覆盖安装失败。
解决:检查build.gradle里的minSdkVersion,期末作业建议设为21或23,覆盖绝大多数手机。如果手机里已经装了Debug版,先卸载再装Release版,因为两者签名不同。
4.4 日期显示为乱码或格式不对
现象:列表里的日期显示成2024-01-01 00:00:00.000或者一串时间戳数字。
原因:插入数据库时用了new Date().toString(),或者用了SimpleDateFormat但pattern写错了。
解决:统一用SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())格式化后再存入数据库。查询出来直接显示,不需要再解析。注意Locale参数不要省略,否则在某些系统语言下会出现月份名称异常。
4.5 统计页面数据对不上流水列表
现象:首页流水显示支出100元,统计页面饼图显示支出80元。
原因:统计查询的SQL条件写错了,比如漏了type字段的过滤,或者日期范围边界处理不对。
解决:统计查询用SELECT category, SUM(amount) FROM record WHERE type=0 GROUP BY category,确保type条件和列表页一致。日期范围用date >= ? AND date <= ?,注意字符串比较的边界包含问题。调试时先把SQL打印出来,在SQLite工具里跑一遍看结果对不对。
5. 让记账app多拿5分:统计图表的轻量实现与答辩演示技巧
期末大作业的评分不只是「能跑就行」,同样功能的两个项目,一个有统计图表、一个只有列表,分数能差出10分。但引入MPAndroidChart这类第三方库会增加构建风险,我一般推荐用自定义View画一个简单的饼图或柱状图,代码量不大,答辩时还能讲出原理。
核心思路是继承View,重写onDraw方法,用Canvas画扇形。下面是一个饼图的关键代码片段:
public class PieChartView extends View { private List<Float> values; private List<Integer> colors; private Paint paint; private RectF rectF; public PieChartView(Context context) { super(context); paint = new Paint(Paint.ANTI_ALIAS_FLAG); paint.setStyle(Paint.Style.FILL); rectF = new RectF(); } public void setData(List<Float> values, List<Integer> colors) { this.values = values; this.colors = colors; invalidate(); // 触发重绘 } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (values == null || values.isEmpty()) return; float total = 0; for (float v : values) total += v; if (total == 0) return; float startAngle = -90; // 从正上方开始 int centerX = getWidth() / 2; int centerY = getHeight() / 2; int radius = Math.min(centerX, centerY) - 20; rectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius); for (int i = 0; i < values.size(); i++) { float sweep = values.get(i) / total * 360; paint.setColor(colors.get(i)); canvas.drawArc(rectF, startAngle, sweep, true, paint); startAngle += sweep; } } }参数说明:startAngle = -90让第一个扇形从12点钟方向开始,符合阅读习惯。sweep是每个分类占比乘以360度。rectF定义了饼图的外接矩形,半径留了20像素边距避免贴边。invalidate()在数据更新后调用,触发onDraw重绘。
在StatsFragment里,从数据库查出各分类的支出总额,组装成values列表,再配一组颜色值传进去。颜色可以用Color.parseColor("#FF6B6B")这样的十六进制值,选五六个区分度高的就行。
答辩演示时,我习惯按这个顺序讲:先打开首页展示流水列表,说明RecyclerView和SQLite的配合;再点添加按钮,演示分类选择和日期选择器;然后切到统计页,指着饼图说「这是自定义View,根据数据库的GROUP BY查询结果实时绘制」;最后打开设置页,展示版本号和导出功能。整个过程控制在3分钟内,重点突出「数据从哪来、怎么存、怎么展示」这条链路。
还有一个加分细节:在关于页面加一个「导出数据」按钮,把数据库里的记录导成CSV文件存到外部存储。代码只需要用到FileWriter和Cursor遍历,十几行就能搞定,但答辩时能体现你对文件存储的理解。老师如果问「数据能不能备份」,你就有东西可讲。
我自己做第一个记账app时,光顾着堆功能,忘了在onPause里保存输入框的内容,结果用户切出去接个电话回来,填了一半的账目全没了。后来养成的习惯是:任何用户输入界面,onPause里必须做临时保存,onResume里恢复。这个习惯比多写十个功能都值钱。希望帮到你。
本文还有配套的精品资源,点击获取