ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android课设实战:校园快递领取系统开发全流程

Android课设实战:校园快递领取系统开发全流程 简介面向 Android 期末课设的校园快递领取系统完整项目主要服务缺少项目经验的新手帮助其理解从一个需求到一套可运行 App 的实现路径。压缩包共 484 个文件、约 2.3MB包含 126 个 Java 源码、199 个 XML 布局与配置、131 张 PNG 图片以及 Gradle 构建脚本和第三方 jar 依赖可直接导入 Android Studio 查看与运行其中 XML 负责界面布局与配置Java 对应业务逻辑PNG 为界面及图标素材。已有 5284 人浏览学习。项目覆盖管理员与用户双端管理员可完成登录、快递入库、出库、按手机号搜索、发布取件公告和个人信息维护用户可注册、登录、按手机号查看快递并修改密码。模块划分清晰界面资源齐全适合作为课程设计或毕业设计的基础模板新手可重点学习 Android 界面搭建、事件响应和本地数据存储等写法节省从零搭建工程的时间。1. 校园快递领取系统拆解课设题目在考你什么“校园快递领取系统”这类题目几乎是每个 Android Studio 课设季都会出现的熟面孔。乍一看要你做快递管理实际上评委验收时看的只有一件事一件快递从入库生成取件码到收件人凭码取走这条状态链路在你的 App 里能不能完整跑通。功能边界清晰、单表单表查询、不需要网络也不需要申请一堆权限这是新手第一次独立用 Android 写完整 App 最稳的选题。下面按环境准备、数据层、界面交互、验收查漏四个顺序展开代码可以直接粘到 Android Studio 里跑。2. 环境准备从下载 Android Studio 到跑通第一个空项目2.1 JDK 与 SDK 版本怎么选先别急着写代码环境配不对会让后面的每一步都在报错。目前 Android Studio 稳定版自带 JBRJetBrains Runtime装完 IDE 后系统里不需要单独再配 JDK 环境变量。课设里最常见的版本坑是老师给的项目模板基于旧版 AGP你在自己电脑上装了最新版 Android Studio结果 Gradle 同步直接报 “Unsupported class file major version” 或者 “Could not determine java version”。选版本只要记住一个对应关系AGP 8.x 配 JDK 17AGP 7.x 通常配 JDK 11。具体到项目可以在根目录的gradle/wrapper/gradle-wrapper.properties里看到 Gradle 版本再去查对应 AGP 的兼容表拿不准时看 Android Studio 下载页面标注的最低 JDK 要求。另外很多新手会先搜“android studio 怎么设置中文”装个语言包确实很快但我建议保留英文界面因为编译报错的关键词用英文去搜命中率比中文高得多遇到问题时能少走弯路。历史版本下载入口在 Android Studio 官网的归档页面按自己电脑的 CPU 架构选对应的包不要在 ARM 版 Mac 上硬装 x86_64 版。Android SDK 官网下载给出的是 SDK 工具的入口日常开发直接用 Android Studio 里的 SDK Manager 勾选即可。新手只勾一个 Android API 34 或 35 的 Platform加一个 System Image 给模拟器用磁盘占用最小。API 级别选太高模拟器起不来或者启动奇慢比“SDK 无法勾选”更影响进度。2.2 创建空 Activity 项目并处理首次构建慢照着 androidstudio 安装教程装好 IDE 只是第一步New Project 才是真正开始。打开 Android Studio 选 New Project模板选 Empty Views Activity。默认的 Compose 模板对课设来说学习曲线太陡传统 XML 布局更直观也更容易在答辩时讲清楚。项目名可以用 ExpressPickupSystem语言选 JavaMin SDK 选 API 24Target SDK 保持默认。创建完成后第一个坑是 Gradle 同步慢。国内网络环境下google() 和 mavenCentral() 仓库经常连不上常见做法是把仓库地址换到阿里云镜像位置在项目根目录的settings.gradle.kts老版本项目可能写在build.gradle的 allprojects 块里pluginManagement { repositories { maven { url uri(https://maven.aliyun.com/repository/google) } maven { url uri(https://maven.aliyun.com/repository/central) } google() mavenCentral() gradlePluginPortal() } }仓库列表里先写阿里云镜像再保留官方仓库这样官方源恢复后不影响后续同步。同步成功的标志是 Build 面板没有红色报错右侧 Gradle 进度条走完窗口顶栏出现绿色锤子图标。首次构建少则几分钟多则十几分钟都正常不要中途强杀进程否则 Gradle 锁文件损坏后面每次构建都要再等一遍。2.3 新手进入编译期后最常碰到的 3 类报错第一类是 SDK 组件状态异常。SDK Manager 里某些版本的 Platform 显示已安装但无法取消或者整个列表灰掉先检查 Android SDK 的目录路径是否被移动过路径没问题就点 Edit重新选中要装的包再 Finish让 IDE 重新校验一遍不需要卸载重装 Android Studio。装完模拟器后报 HAXM 相关错误优先使用 Android Emulator hypervisor driver或者直接保留 Windows Hypervisor Platform比单独装 HAXM 省事。第二类是资源文件相关的编译失败典型提示是 “tag number over 30 is not supported”。出现这类报错时布局或 drawable 里单个 XML 文件的标签层级过深、数量过多。常见来源是复制别人的复杂布局后没有整理解决方式是拆分include复用公共部分把嵌套的 LinearLayout 改平标签数量压下来基本不会再触发。第三类是点击按钮后瞬间崩溃错误信息里能看到 “Cannot perform operation” 或者 “Attempt to invoke virtual method” 一类字样。这种多半不是点击逻辑写错而是点击发生时 SQLiteDatabase 已经关闭或者列表数据更新后没有通知 Adapter 刷新。建议把数据库的打开、关闭收敛到一个 DAO 里不要在 Activity 的 onDestroy 里无脑关闭全局连接。能编译、能安装到模拟器再开始写业务逻辑这个顺序对新手最友好。3. 数据层先行快递实体、SQLite 建表与取件码生成3.1 实体类字段设计与建表 SQL把驿站取件流程翻译成数据模型最简只需要一张主表。字段不建议再多课设核心是 CRUD字段越多录入界面越难排布。推荐的最小字段集如下每个字段在后续的搜索和统计里都有用字段类型说明idINTEGER PRIMARY KEY AUTOINCREMENT自增主键tracking_noTEXT NOT NULL快递运单号recipient_nameTEXT NOT NULL收件人姓名recipient_phoneTEXT NOT NULL收件人电话pickup_codeTEXT NOT NULL6 位数字取件码statusINTEGER DEFAULT 00 未取件1 已取件arrival_timeTEXT NOT NULL入库时间ISO8601 格式pickup_timeTEXT取件时间取件后写入status 用 INTEGER 而不是字符串是为了让 SQL 聚合统计更干净也避免同一个状态被写成“已取件”“取出”“已拿走”三种文本。时间字段用 TEXT 存yyyy-MM-dd HH:mm:ss展示和排序都直观不需要额外引入序列化逻辑。实体类按字段写一个 Java Bean提供一个从 Cursor 读取数据的方法。getter/setter 用 Android Studio 的 AltInsert 一键生成避免手写出错public class ExpressPackage { private long id; private String trackingNo; private String recipientName; private String recipientPhone; private String pickupCode; private int status; private String arrivalTime; // 入库时间 private String pickupTime; // 取件时间未取件为 null public static ExpressPackage fromCursor(Cursor c) { ExpressPackage p new ExpressPackage(); p.id c.getLong(c.getColumnIndexOrThrow(id)); p.trackingNo c.getString(c.getColumnIndexOrThrow(tracking_no)); p.recipientName c.getString(c.getColumnIndexOrThrow(recipient_name)); p.recipientPhone c.getString(c.getColumnIndexOrThrow(recipient_phone)); p.pickupCode c.getString(c.getColumnIndexOrThrow(pickup_code)); p.status c.getInt(c.getColumnIndexOrThrow(status)); p.arrivalTime c.getString(c.getColumnIndexOrThrow(arrival_time)); p.pickupTime c.getString(c.getColumnIndexOrThrow(pickup_time)); return p; } // getter/setter 用 AltInsert 生成 }建表语句写在自定义的DbHelper里。类名和表名在课设里不必过度设计能一眼看出用途就够了public class DbHelper extends SQLiteOpenHelper { public static final String DB_NAME campus_express.db; public static final int DB_VERSION 1; public DbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE IF NOT EXISTS express_package ( id INTEGER PRIMARY KEY AUTOINCREMENT, tracking_no TEXT NOT NULL, recipient_name TEXT NOT NULL, recipient_phone TEXT NOT NULL, pickup_code TEXT NOT NULL, status INTEGER DEFAULT 0, arrival_time TEXT NOT NULL, pickup_time TEXT)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP TABLE IF EXISTS express_package); onCreate(db); } }onUpgrade 里直接 DROP 表再重建是课设阶段的偷懒写法能保证修改表结构后程序不崩。真实项目不能这么干会丢用户数据正确做法是 ALTER TABLE ADD COLUMN。这里用 DROP 是为了让你在频繁改字段时不卡在迁移逻辑上。3.2 用 DAO 封装快递表的最小增删改查新手容易把db.execSQL散落在 Activity 里课设数据量小短时间看不出问题。但取件操作要同时改状态、写取件时间、刷新列表逻辑一多就缠在一起。常见做法是做一个ExpressDao把快递表的操作全部收口public class ExpressDao { private final DbHelper helper; public ExpressDao(Context context) { helper new DbHelper(context.getApplicationContext()); } public long insert(String trackingNo, String name, String phone) { SQLiteDatabase db helper.getWritableDatabase(); ContentValues cv new ContentValues(); cv.put(tracking_no, trackingNo); cv.put(recipient_name, name); cv.put(recipient_phone, phone); cv.put(pickup_code, PickupCodeGenerator.generate(db)); cv.put(status, 0); cv.put(arrival_time, now()); long id db.insert(express_package, null, cv); db.close(); return id; } public int markPickedUp(long id) { SQLiteDatabase db helper.getWritableDatabase(); ContentValues cv new ContentValues(); cv.put(status, 1); cv.put(pickup_time, now()); return db.update(express_package, cv, id ?, new String[]{String.valueOf(id)}); } public ListExpressPackage queryAll() { SQLiteDatabase db helper.getReadableDatabase(); ListExpressPackage list new ArrayList(); Cursor c db.rawQuery( SELECT * FROM express_package ORDER BY status ASC, arrival_time DESC, null); while (c.moveToNext()) { list.add(ExpressPackage.fromCursor(c)); } c.close(); db.close(); return list; } private static String now() { return new SimpleDateFormat(yyyy-MM-dd HH:mm:ss, Locale.US) .format(new Date()); } }三个细节值得注意。第一DAO 构造函数里传入applicationContext不是 Activity 实例避免页面退出后 DAO 还持有 View 引用造成内存泄漏。第二insert 里取件码由PickupCodeGenerator生成DAO 不关心取件码的具体规则职责单一。第三每次操作后手动db.close()在低内存设备上能减少文件描述符占用。markPickedUp的返回值是受影响行数返回 1 表示更新成功返回 0 表示 id 不存在。刷新列表前可以判断这个值如果为 0 就 Toast 提示“快递不存在”比直接更新完再看界面反馈要严谨。注意不要在 Activity 的 onDestroy 里统一调用helper.close()。DAO 每次操作自己 open/close连接生命周期才可控某一处全局关闭之后后续按钮点击都会触发 “Cannot perform operation” 一类异常。3.3 取件码生成的校验逻辑避免重复码取件码最简单是随机 6 位数字难在入库时生成、取件时按码匹配如果两个快递同一个码收件人取错件的风险就出来了。生成时要查一次表确认没有重复再落库。比较稳的写法是加上重试上限public class PickupCodeGenerator { private static final Random RANDOM new Random(); private static final int MAX_RETRY 50; public static String generate(SQLiteDatabase db) { String code; int attempt 0; do { code String.format(Locale.US, %06d, RANDOM.nextInt(1000000)); attempt; } while (isExists(db, code) attempt MAX_RETRY); return code; } private static boolean isExists(SQLiteDatabase db, String code) { Cursor c db.rawQuery( SELECT 1 FROM express_package WHERE pickup_code ? LIMIT 1, new String[]{code}); boolean exists c.moveToFirst(); c.close(); return exists; } }这里用SELECT 1 … LIMIT 1而不是SELECT COUNT(*)命中一条就返回数据库不需要统计全部满足条件的行数。重试上限的意义是兜底6 位数字只有一百万种组合当表内数据到几万件时碰撞概率明显上升重试 50 次还没有空闲码说明容量已到极限这时应该扩到 7 位或字母数字混合。课设场景里两三百条数据重复概率非常低但校验逻辑写出来答辩时是一个明确的加分点。不要用“当前时间戳取后六位”当取件码时间戳既有重复又容易被猜而且写入时间和取件码之间的关联会让数据看起来不自然。4. 列表页与取件交互RecyclerView Adapter 完成状态流转4.1 布局拆分主列表、item 布局、取件确认对话框主界面从上到下分两块顶部快递录入区底部快递列表。录入区放运单号和收件人两个输入框加一个“入库”按钮列表区用 RecyclerView不要再用 ListView。RecyclerView 在答辩时能说出的点是 ViewHolder 复用减少了 findViewById 的次数这是课设里少有的能讲出设计意图的地方。主布局的关键部分LinearLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical EditText android:idid/et_tracking_no android:hint运单号 / EditText android:idid/et_recipient android:hint收件人姓名 / Button android:idid/btn_add android:text快递入库 / ProgressBar android:idid/loading_pb android:layout_widthwrap_content android:layout_heightwrap_content android:layout_gravitycenter_horizontal android:visibilitygone / androidx.recyclerview.widget.RecyclerView android:idid/rv_packages android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 / /LinearLayoutProgressBar 在首次加载列表时显示数据回来后置为 gone。SQLite 的同步查询通常快到来不及看到进度条但保留这个控件数据量大时不会让人觉得应用卡死。RecyclerView 用layout_weight1撑满剩余高度不要把列表塞进 ScrollView那会导致滑动冲突和内存浪费。item 布局左侧显示运单号和收件人信息右侧一个状态文字和一个“取件”按钮。状态文字用颜色区分未取件用蓝色已取件用灰色。按钮在已取件时 disable从交互层面避免同一件快递被反复操作。4.2 用 Adapter 承接点击事件并刷新列表状态Adapter 的核心职责是把数据渲染到列表把“取件”按钮点击转发给 Activity。它不直接访问 SQLite否则数据层和 UI 层耦合后面改查询逻辑时容易牵连界面。接口回调是课设里常见也好解释的做法public class ExpressAdapter extends RecyclerView.AdapterExpressAdapter.VH { private final ListExpressPackage data new ArrayList(); private OnPickupClickListener pickupListener; public interface OnPickupClickListener { void onPickupClick(ExpressPackage item); } public void setData(ListExpressPackage newData) { data.clear(); data.addAll(newData); notifyDataSetChanged(); } Override public void onBindViewHolder(VH holder, int position) { ExpressPackage item data.get(position); holder.tvTrackingNo.setText(item.getTrackingNo()); holder.tvRecipient.setText(item.getRecipientName() item.getRecipientPhone()); if (item.getStatus() 1) { holder.tvStatus.setText(已取件); holder.tvStatus.setTextColor(0xFF9E9E9E); holder.btnPickup.setEnabled(false); } else { holder.tvStatus.setText(未取件); holder.tvStatus.setTextColor(0xFF2196F3); holder.btnPickup.setEnabled(true); holder.btnPickup.setOnClickListener(v - pickupListener.onPickupClick(item)); } } static class VH extends RecyclerView.ViewHolder { TextView tvTrackingNo, tvRecipient, tvStatus; Button btnPickup; VH(View itemView) { super(itemView); tvTrackingNo itemView.findViewById(R.id.tv_tracking_no); tvRecipient itemView.findViewById(R.id.tv_recipient); tvStatus itemView.findViewById(R.id.tv_status); btnPickup itemView.findViewById(R.id.btn_pickup); } } }取件按钮只绑定在“未取件”分支里已取件的 item 按钮直接禁用从根源上避免重复点击。setData方法用整体替换的方式更新列表实现最简单代价是全部重绘。课设两三百条数据完全能接受以后数据量大了再换 ListAdapter DiffUtil。按钮回调里的 item 是当前点击位置对应的实体不是界面上的临时文本这样点击后不管数据库怎么改用的都是同一份数据引用。4.3 点击取件之后更新数据库并同步列表Activity 里初始化 RecyclerView 时需要按下面这个顺序串起来。LayoutManager 不要忘记RecyclerView 没有 LayoutManager 不会显示任何数据expressDao new ExpressDao(this); expressAdapter new ExpressAdapter(); rvPackages.setLayoutManager(new LinearLayoutManager(this)); rvPackages.setAdapter(expressAdapter); expressAdapter.setOnPickupClickListener(item - { new AlertDialog.Builder(this) .setTitle(取件确认) .setMessage(确认 item.getRecipientName() 取走快递 item.getTrackingNo() 吗) .setPositiveButton(确认, (dialog, which) - { expressDao.markPickedUp(item.getId()); loadData(); }) .setNegativeButton(取消, null) .show(); }); loadData();loadData()是列表刷新的统一入口private void loadData() { loadingPb.setVisibility(View.VISIBLE); ListExpressPackage list expressDao.queryAll(); expressAdapter.setData(list); loadingPb.setVisibility(View.GONE); }数据库状态改变后立即重新查询保证 UI 和 SQLite 里的 status 完全一致。不要在弹窗确认后手动去改某个 View 的文本界面状态和数据库短暂不一致下次启动又会变回去。入库按钮也走同样思路读取两个 EditText非空校验通过后调expressDao.insert再loadData。如果快递数量上千条同步查询加全量刷新在点击瞬间会有卡顿。课设阶段不需要提前优化但答辩时可以说“当前查询在主线程数据量大后会迁移到协程或 Room 的 Flow 订阅。”这句话比背概念更能让评委认可。5. 验收前补上统计与搜索3 条 SQL 为课设加分5.1 用聚合函数统计未取件数量主界面顶部加一行“未取件xx 件”只需要一条聚合 SQL。不要先把全表数据拉到 Java 层再用循环数直接在 SQLite 里聚合SELECT COUNT(*) FROM express_package WHERE status 0;用rawQuery执行后游标moveToFirst()再getInt(0)取出结果。注意 COUNT 返回的是 INTEGER用 getString 再 Integer.parseInt 是绕远路。这个统计值也可以反过来校验取件码容量如果未取件数量接近一万而取件码还是 6 位数字说明该扩位了。5.2 按收件人/取件码做模糊查询的写法验收经常会让演示“搜索某人的快递”。输入关键词后点击搜索列表过滤到匹配项。注意用参数绑定不要用字符串拼接拼出 WHERE 条件public ListExpressPackage search(String keyword) { SQLiteDatabase db helper.getReadableDatabase(); String like % keyword %; Cursor c db.rawQuery( SELECT * FROM express_package WHERE recipient_name LIKE ? OR pickup_code LIKE ? ORDER BY arrival_time DESC, new String[]{like, like}); ListExpressPackage list new ArrayList(); while (c.moveToNext()) { list.add(ExpressPackage.fromCursor(c)); } c.close(); db.close(); return list; }参数绑定用?占位第二个参数是字符串数组每个元素对应一个占位符。这能避免关键词里的单引号破坏 SQL 结构也防止最简单那类注入。LIKE 在数据量大时不走索引但几千行记录的全表扫描对手机来说不是负担。中文姓名和数字取件码基本不涉及大小写问题课设里可以不用再处理 COLLATE NOCASE。5.3 答辩前自测的 5 个检查点杀掉 App 进程再重新打开快递数据和状态还在证明数据写入 SQLite 而不是只存在于内存列表。同一件快递取件后按钮变为不可点击重启后仍是已取件。未输入运单号或姓名时点入库程序给出 Toast 提示不崩溃。输入一个不存在的取件码搜索列表为空界面不报错。使用 Build Build Bundle(s) / APK(s) Build APK(s) 打一个 debug 包装到真机上走一遍完整流程。数据库到底写对没有不用靠猜。Android Studio 自带 App Inspection 工具路径是 View Tool Windows App Inspection连接模拟器或真机后选择 Database Inspector能看到campus_express.db的表结构和实际数据。手工改一条记录的状态再回到 App 刷新列表比边看日志边点按钮高效得多。用 Database Inspector 核对一遍表数据后再打包这一版课设就基本没有可问倒你的软肋了。本文还有配套的精品资源点击获取
返回列表