尧图精选

Android期末大作业:从0到1实现纪念日APP的SQLite存储与倒计时

🕒 发布时间:2026/9/12 2:40:01 📁 来源:尧图网络
简介面向Android初学者与课程设计人群这份2023年期末大作业「纪念日APP」完整项目包涵盖了纪念日记录、日历视图、自定义提醒等常用功能涉及XML布局、Activity跳转、SQLite/SharedPreferences存储、日期计算、通知服务与运行时权限等核心开发知识适合用来系统梳理Android应用开发流程。压缩包共468个文件以155个Java源码、121个XML布局配置、112个PNG和31个JPG图片资源为主另有12个MP3音频、APK安装包及签名文件等整体大小27.23MB结构清晰便于对照阅读和二次开发。该资源已有3257人浏览学习既能帮助快速掌握项目骨架与常见模块写法也可作为课程设计演示或功能扩展的起点。1. 纪念日 APP 作为 Android 期末大作业到底在考什么期末大作业最怕的不是功能难而是不知道老师想看到什么。选题定为纪念日 APP恰好踩中了 Android 课程的全部核心考点四大组件、SQLite 存储、日期运算、RecyclerView 列表、通知栏提醒。一个项目把这些点全串起来既能体现工程能力又不至于像电商项目那样拖到答辩前一周还在补登录逻辑。另一个容易被忽略的现实是绝大多数同学的选题会在三到五天做完,撞车率极高。纪念日 APP 的切入点很窄但正因为窄反而容易做出差异化。日期倒计时是明牌的需求可如果加上农历生日、恋爱纪念日的在一起多少天、节日倒数、卡片式分享就比隔壁同学的普通版本多出一个量级的完整度。对 Android Studio 不熟也不用慌这个课题量的代码量通常在 800 到 1200 行之间核心就是数据库、列表、日期计算这三件事。这篇就按期末答辩的验收标准把完成一个能跑、能讲、能答上来的纪念日 APP 的完整路径拆开。2. 开发环境与项目骨架在 Android Studio 里把地基打稳2.1 新建项目的两个关键选项打开 Android Studio新建一个 Empty Views Activity 项目比用 Empty Activity 更合适。省得后面还要自己处理 Fragment 容器。项目名字直接叫AnniversaryApp包名建议用com.example.anniversary——期末场景下不需要反域名反倒要避免用个人域名导致编译报错。语言选 Java 还是一样?Kotlin 选 Java。理由很务实答辩时老师翻代码Java 的可读性对大部分教 Android 的传统课程更友好网上现成参考代码也以 Java 居多Debug 时能少走弯路。API Level 选API 24Android 7.0。低了要处理一堆兼容代码高了拿老师提供的测试机可能直接装不上。期末实验室里那批旧机型API 26 以上的 Target SDK 反而会出现应用未安装的尴尬。Minimum SDK 24 和 Target SDK 33 的搭配是当前阶段容错率最高的组合。2.2 在 build.gradle 里提前配好依赖界面复杂度不高的话系统自带控件就能撑住。但 RecyclerView 必须加没有它列表页写起来会非常痛苦。打开app/build.gradle的 dependencies 块加入这三项implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 implementation androidx.recyclerview:recyclerview:1.3.2同步完成后建议顺手把 vector 兼容和 viewBinding 打开在android节点里这样配置buildFeatures { viewBinding true }viewBinding是期末项目里最值得投入的一分钟。打开它之后findViewById那堆样板代码可以全部省掉Activity 里直接用Binding.editText.getText()这种形式取控件值答辩演示时代码会短一大截老师看源码时印象分会高不少。注意 viewBinding 要求 Android Studio 3.6 以上现在机器上正常都是 2023 年以后的版本完全够用。2.3 先跑通一个空 Activity 再动手写业务很多同学的翻车点不在逻辑而在环境。新建完项目后第一件事不是写功能而是先连着模拟器跑一次空壳。这一步的意义在于排除 SDK 构建工具、Gradle 下载、模拟器镜像三个环节的问题。其中 Gradle 首次同步卡住是国内最常见的状况参考 Android Studio 安装教程里提到的镜像源方案把distributionUrl里的services.gradle.org换成腾讯或阿里镜像地址即可distributionUrlhttps\://mirrors.cloud.tencent.com/gradle/gradle-8.4-bin.zip同步完如果控制台还报 SDK 组件缺失打开 SDK Manager把Android SDK Build-Tools和Platform-Tools勾上。这种问题属于环境装了但没装完整在期末作业里占了很大比重值得在开始时解决而非拖到答辩前一天。空壳跑通后接下来再进业务层就顺畅了。3. 数据层与存算逻辑把纪念日的 SQLite 存取写明白3.1 设计一张够用的纪念日数据表纪念日 APP 最核心的数据对象是一个事件。它要有名称、日期、类型、备注。类型决定了展示时的文案差异生日、恋爱纪念日、节日这三类是必须的提前加一个category字段做区分可以省去后期反复改表的麻烦。至于在一起多少天这种动态数字不要存进数据库。它是根据当前日期与事件的日期做差算出来的真实数据源是系统当前时间这也在 SQLite 的职责之外。不要把需要动态计算的数据落库这是数据表设计里最常见的反模式。用SQLiteOpenHelper建表兼容性和可读性都最好。Room 固然是 Google 官方推荐但期末答辩时老师很可能提问数据库版本升级怎么办SQLiteOpenHelper 的onUpgrade方法回答起来更直接。public class DBHelper extends SQLiteOpenHelper { public DBHelper(Context context) { super(context, anniversary.db, null, 1); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, date TEXT NOT NULL, category TEXT DEFAULT 纪念日, note TEXT, created_at TEXT DEFAULT CURRENT_TIMESTAMP)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP TABLE IF EXISTS events); onCreate(db); } }onCreate里的建表语句是整体项目的根基。date字段用TEXT而不是INTEGER原因是日期展示时离不开yyyy-MM-dd这种格式存文本省去每次查询时的格式化转换。category字段是排序和筛选的唯一依据可以设默认值但不允许为空。created_at存创建时刻用于列表按时间倒序排列新添加的事件排在最上面。3.2 封装 DAOCRUD 操作不散落在 Activity 里数据访问对象DAO的价值在期末阶段可能看不出来但统一封装后 Activity 会薄很多。常见做法是写一个EventDao类把增删改查包成四个方法后续不管是在列表页面还是编辑页面调用前只传对象进去就好。public class EventDao { private final DBHelper helper; public EventDao(Context context) { helper new DBHelper(context); } public void insert(EventModel event) { SQLiteDatabase db helper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(name, event.getName()); values.put(date, event.getDate()); values.put(category, event.getCategory()); values.put(note, event.getNote()); db.insert(events, null, values); db.close(); } public ListEventModel queryAll() { ListEventModel list new ArrayList(); SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.rawQuery(SELECT * FROM events ORDER BY date ASC, null); if (cursor.moveToFirst()) { do { EventModel event new EventModel( cursor.getInt(0), cursor.getString(1), cursor.getString(2), cursor.getString(3), cursor.getString(4) ); list.add(event); } while (cursor.moveToNext()); } cursor.close(); db.close(); return list; } }DAO 层一定要负责关闭Cursor和Database。Cursor 忘记关闭虽然不一定立刻崩溃但长时间运行后会报 Library group not closed 之类的警告期末答辩前很难排查。查询用ORDER BY date ASC保证总是离当前日期近的排前面不用到 Java 层二次排序。3.3 日期差值计算与闰年边界日期倒计时是纪念日 APP 的功能心脏。这部分的坑集中在闰年、跨年、跨月三种情况上。用Calendar做差值而不是拿两个时间戳相减原因是时间戳相减得到的毫秒数在跨夏令时时区时可能差出一小时导致天数少算一天。Acivity 里做展示层换算的代码如下public static long[] calculateDays(String targetDate) { java.text.SimpleDateFormat sdf new java.text.SimpleDateFormat(yyyy-MM-dd, Locale.CHINA); Calendar target Calendar.getInstance(); try { Date date sdf.parse(targetDate); target.setTime(date); } catch (Exception e) { return new long[]{0, 0}; } Calendar today Calendar.getInstance(); today.set(Calendar.HOUR_OF_DAY, 0); today.set(Calendar.MINUTE, 0); today.set(Calendar.SECOND, 0); today.set(Calendar.MILLISECOND, 0); long diffMillis target.getTimeInMillis() - today.getTimeInMillis(); long days diffMillis / (24 * 60 * 60 * 1000); if (days 0) { return new long[]{days, days}; } else { return new long[]{days, -days}; } }多个关键点值得展开。today必须先归零时分秒再取毫秒否则当天算出的差异数可能是 0.9 天强转 long 后直接变成 0展示成今天就闹笑话了。目标日期解析失败时的兜底返回值在期末排错时省了不少事。返回值数组第一个元素适合传给String.format直接拼接文案,正数显示还有 N 天、负数显示已过去 N 天第二元素始终是绝对值适配在一起第 100 天这种固定句式。注意闰年二月二十九号这种日期在非闰年是不存在的如果存了 2 月 29 日解析时会把日期顺延到 3 月 1 日这个问题可以在编辑页面提前用日期选择器限制不在底层处理。4. 界面与交互实现列表、日期选择器与通知栏4.1 RecyclerView 适配器绑定倒计时文案列表页面是用户每天打开都看到的界面信息密度要克制。单行卡片显示四要素名称、分类标签、目标日期、倒计时天数。适配器里做两件事一是把数据列表加载进来二是把天数文案拼接好。public class EventAdapter extends RecyclerView.AdapterEventAdapter.ViewHolder { private ListEventModel list; private OnItemClickListener listener; public EventAdapter(ListEventModel list) { this.list list; } interface OnItemClickListener { void onItemClick(EventModel item); } public void setOnItemClickListener(OnItemClickListener listener) { this.listener listener; } NonNull Override public ViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_event, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(NonNull ViewHolder holder, int position) { EventModel item list.get(position); holder.nameText.setText(item.getName()); holder.categoryText.setText(item.getCategory()); holder.dateText.setText(item.getDate()); long[] result DateUtil.calculateDays(item.getDate()); if (result[0] 0) { holder.daysText.setText(还有 result[0] 天); } else { holder.daysText.setText(已过去 result[1] 天); } holder.itemView.setOnClickListener(v - { if (listener ! null) { listener.onItemClick(item); } }); } }onBindViewHolder里每次都要重新算天数听起来似乎有性能问题实际完全不用担心。RecyclerView 在划过一屏后只会复用离屏的 ViewHolder每屏十几个 item计算一次的时间在微秒级。真正的性能隐患其实是不小心在适配器里做了数据库查询——POSITION 变化时反复开数据库那才会卡 UI。所以适配器只拿内存里的 list 数据做渲染数据变更时通过notifyDataSetChanged()刷新这是最简单的方案。点击事件的写法也值得提一下。用接口回调而不是直接传 Intent 进适配器是为了让 Activity 统一处理跳转逻辑——详情页、编辑页、删除确认框都在这同一个回调里分发。这样适配器的职责更单一答辩时被提问这里为什么不用静态方法也有话说。4.2 DatePickerDialog 日期选择器与格式校验日期信息靠手工输入yyyy-MM-dd一旦格式错了后续所有倒计时计算都会出问题。写死EditText加正则校验是最省事的路但用户体验较差。用 DatePickerDialog 弹窗选日期配合模拟器上触摸屏的操作习惯也方便演示public void showDatePickDialog(View view) { Calendar calendar Calendar.getInstance(); int year calendar.get(Calendar.YEAR); int month calendar.get(Calendar.MONTH); int day calendar.get(Calendar.DAY_OF_MONTH); DatePickerDialog dialog new DatePickerDialog(this, (picker, y, m, d) - { String monthStr (m 1) 10 ? 0 (m 1) : String.valueOf(m 1); String dayStr d 10 ? 0 d : String.valueOf(d); binding.dateEdit.setText(y - monthStr - dayStr); }, year, month, day); dialog.show(); }这里最容易错的就是月份加一。Calendar的月份是 0 到 11DatePickerDialog 里拿到的 m 也是 0 到 11但展示给用户时必须加 1。另一个隐蔽的坑是字符串拼接时的手动补零——选中 1 月 5 日原始值是2024-1-5存入数据库后排序全乱掉。所以月份和日期都要补成两位数否则2024-1-5和2024-11-5混在一起时字典序直接错乱。4.3 通知栏提醒用 NotificationCompat 串起来纪念日的价值在于提醒。到期当天弹一条通知让用户不打开 APP 也知道今天是什么日子。Android 8.0 以后的通知必须指定 Channel不然通知不显示private void sendReminder(String eventName, String daysText) { NotificationManager manager (NotificationManager) getSystemService(NOTIFICATION_SERVICE); NotificationChannel channel new NotificationChannel( reminder_channel, 纪念日提醒, NotificationManager.IMPORTANCE_HIGH ); manager.createNotificationChannel(channel); NotificationCompat.Builder builder new NotificationCompat.Builder(this, reminder_channel) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(纪念日提醒) .setContentText(eventName daysText) .setAutoCancel(true); manager.notify((int) System.currentTimeMillis(), builder.build()); }IMPORTANCE_HIGH配合手机振动效果会让模拟器和真机都有强提醒感。setAutoCancel(true)让用户点击通知后自动消失。注意targetSdk在 33 以上时部分真机需要动态申请POST_NOTIFICATIONS权限但这只影响 Android 13 及以上的设备期末演示用的旧设备通常没问题可以先不写权限申请的代码真机若没弹通知再加。4.4 各页面间的 Intent 跳转与数据回传编辑页/新增页和主页之间有数据往来。用startActivityForResult已废弃用ActivityResultLauncher是新接口也是 2023 年答辩时老师比较在意的知识点。注册时先声明一个 launcherprivate ActivityResultLauncherIntent launcher; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); launcher registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result - refreshList() ); } private void openEditPage() { Intent intent new Intent(this, EditActivity.class); launcher.launch(intent); }刷新列表非常简单直接在回调里调用 DAO 的queryAll()再notifyDataSetChanged()即可。没必要用 LiveData 或 ViewModel——期末项目里一个屏幕一个 Activity 的结构回调已经足够干净上重量级的架构反而会让代码量翻倍答辩时也不好在短时间里讲清楚 LiveData 和 StateFlow 的区别。5. 进阶打磨清单排序策略、图标替换与恢复出厂适配5.1 排序策略按最近到期优先溢出的几种算法对比默认的ORDER BY date ASC有一个体验问题去年已过的纪念日永远在最前面而今年即将到来的纪念日被挤到后面。用户其实更关心最近将到的日子。这里介绍一个二分思路查询时先把日期和当前日期比较将在今天及之后的日期排前过去的排后SELECT *, CASE WHEN date date(now) THEN 0 ELSE 1 END AS is_past FROM events ORDER BY is_past ASC, date ASC这条 SQL 在rawQuery里直接执行效率比 Java 层排序高一个量级。考试若被问内存排序和数据库排序选哪种从数据量角度回答即可——百条级别的数据两者都无所谓但这个写法体现了对 SQL 表达能力的理解。往更深想一层如果想要几天后即将到来的未来事件置顶、未到月份按日期排、已过去的按日期倒序排就用abs()函数取日期差但可读性下降期末阶段并非必要。5.2 替换默认图标避免答辩减分ic_launcher默认是安卓机器人图标老师看到就知道没换。用 Android Studio 的 Image Asset 工具替换右键res目录选择New Image Asset前景图选一张简单的爱心或日历图形背景色换成暖色。这个工具支持 SVG 转 PNG注意调好 padding 防止图标缩边被裁切。通知栏小图标同理务必是纯白色透明底的 PNG彩色小图标在部分手机上会被系统强制染色。5.3 权限适配与恢复出厂演示场景期末答辩用模拟器时恢复默认设置后 APP 数据会被清空这是最尴尬的演示事故。解决办法是给首次使用做引导提示数据库为空时列表页显示一张引导图和一个大按钮提示用户添加第一个纪念日。另一招是数据手动导出到getExternalFilesDir()目录通过文件管理器分享给别人但导入解析代码量不小期末阶段放掉导入、只保留导出即可。private void exportData() { File dir new File(getExternalFilesDir(null), export); if (!dir.exists()) { dir.mkdirs(); } File file new File(dir, anniversary_backup_ System.currentTimeMillis() .txt); try { FileOutputStream fos new FileOutputStream(file); StringBuilder sb new StringBuilder(); for (EventModel e : dao.queryAll()) { sb.append(e.getName()).append(|) .append(e.getDate()).append(|) .append(e.getCategory()).append(\n); } fos.write(sb.toString().getBytes()); fos.close(); Toast.makeText(this, 备份到: file.getAbsolutePath(), Toast.LENGTH_LONG).show(); } catch (Exception ex) { Toast.makeText(this, 导出失败, Toast.LENGTH_SHORT).show(); } }导出的文本格式用管道符分隔比 CSV 少了转义各种引号的麻烦。getExternalFilesDir不需要权限改动应用卸载时会连此目录一起清除逻辑上是最干净的选择。答辩时如果问到为什么不用公共存储可以顺带回答 Android 10 分区存储对公共目录的读写限制这样既有实践又有理论深度。6. 提交前的半天冲刺回归验收与答辩兜底功能做完了最后半天专门用来跑回归测试。先按用户路径走一遍主流程添加纪念日到列表、点进去看倒计时、编辑日期看文案变化、删除后列表刷新、锁屏后等通知弹出。这条链路走完大问题都能暴露出来。一个最容易忽略的测试点是日期跨年。模拟器里把系统时间改到次年 1 月 1 日再打开列表看倒计时能查出SimpleDateFormat解析年份时的隐蔽错误。测试完记得把模拟器时间改回来不然答辩开着后年的日期所有倒计时数据全乱。边界值测试集中在 2 月 29 日和 12 月 31 日两个日期上。分别测试目标日期恰好是今天、昨天、明天三种情况。calculateDays里对0 天的处理建议显示为就是今天避免用户看到还有 0 天这种滑稽文案。代码层面把 Log.d 调试输出统一做一次清理尤其警惕打印 log 时直接把整条 SQL 打了出去。避免在 Android Studio 的 Logcat 里看到中文乱码run — Edit Configurations — Logcat里把 encoding 设为 UTF-8这是很实用的一个小技巧。答辩的兜底策略是给模拟器拍一张截图。万一现场模拟器启动慢或者网络磁盘出问题有截图兜底不至于全场沉默。另外把 APK导出路径放在一个容易找到的位置在演示完后顺手点开app/build/outputs/apk/debug/app-debug.apk装到自己手机感受下性能也算给整个项目画一个完整的句号。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →