首页 / 资讯中心 / 文章详情

Android Studio安卓课设:MyStudent学生信息管理App完整版

Android Studio安卓课设:MyStudent学生信息管理App完整版 ★ FEATURED ARTICLE
简介这份资源是面向安卓初学者与高校课程设计学生的 Android Studio 学生信息管理 App 完整工程基于官方 IDE 与 Gradle 构建围绕增删改查这一典型业务串起界面布局、数据存储与页面跳转等核心技能点。工程内可见 XML 布局、Java 源码、drawable 与 mipmap 图片、rawproto 与 json 配置、class 与 dex 字节码以及多个 apk 产物完整覆盖从布局设计、RecyclerView 列表展示、SQLite 与 SharedPreferences 存储到 Activity 生命周期、Intent 传值、对话框、菜单、运行时权限和自定义 Adapter 的实现链路也保留了 Logcat 调试与打包发布所需的构建文件。压缩包共 1426 个文件以 338 个 xml、280 个 png、39 个 java 及若干 class、dex、apk 为主整体约 21.14MB目录层级贴合真实项目结构便于按模块检索与对照阅读。已有 3740 人学习适合需要参照成熟工程完成课设、梳理安卓基础架构或积累作品集素材的读者。1. 一份课程设计压缩包先看清它能替你省掉哪些活期末周最典型的场景安卓课程设计要交一个能登录、能增删改查、最好还带搜索的学生信息管理 App而机房上机时间只剩两个下午。Mystudent 这个基于 Android Studio 的安卓课程设计完整版干的就是把最耗时间的那条链路先铺平登录注册、学生列表、新增编辑、条件查询、SQLite 本地存储全都接好了解压导入就是一个能跑起来的业务闭环。它适合三类人——赶课程设计交付节点的学生、想找一个干净 Android 工程练手的转行者、以及需要给学生演示「一个完整 App 该长什么样」的助教。整套东西不依赖任何后端服务数据全部落在手机本地 SQLite 里断网照样演示答辩现场最怕的网络玄学直接绕开。2. 工程骨架与技术选型为什么这套组合在课程设计里最稳先把选型摆在桌面上。这套工程是标准 Java 风格的原生 Android 项目没有引入 Kotlin、没有 Compose、没有 Retrofit 和 Room数据层直接用SQLiteOpenHelper手写 DAO列表用RecyclerView加自定义Adapter界面以Activity XML 布局为主。听起来「老」但对课程设计这个场景恰恰是最优解老师看得懂、答辩问得住、你自己改得动出问题的时候能在 StackOverflow 风格的问答里精准搜到答案而不是被困在某个新框架的版本兼容里。反过来说如果你打算把它当简历项目这套骨架也不是不能升级——后面第 6 章会讲怎么在不动业务逻辑的前提下把数据层换成 Room、把界面换成 Fragment让它从「课设」长成「能聊的工程」。但在跑通之前别急着改架构先让它在你机器上编译出来。2.1 目录结构照着这张清单找文件五分钟摸清全貌解压后是个标准的 Android Studio 工程根目录有settings.gradle、build.gradle、gradle.properties主模块叫app。把app/src/main展开代码基本落在下面这几个位置路径职责改代码时的优先级java/com/example/mystudent/LoginActivity.java登录页校验账号密码并写入登录态高改登录逻辑必看java/com/example/mystudent/MainActivity.java首页/导航入口中java/com/example/mystudent/StudentListActivity.java学生列表、搜索、删除入口高核心页面java/com/example/mystudent/StudentEditActivity.java新增与编辑复用同一个页面高java/com/example/mystudent/db/DBHelper.java建库建表、版本升级高加字段必改java/com/example/mystudent/dao/StudentDao.java所有 SQL 集中在这一层高java/com/example/mystudent/dao/UserDao.java账号表读写中java/com/example/mystudent/bean/Student.java实体类字段与表列一一对应高java/com/example/mystudent/adapter/StudentAdapter.java列表项渲染与点击回调中res/layout/*.xml各页面布局中改样式必看res/values/strings.xml文案集中管理低这种分法谈不上多优雅但它有一个非常实在的好处SQL 只有一处。很多同学写课设习惯在 Activity 里直接db.rawQuery写着写着就变成五个页面五套 SQL改一个字段名要全局搜索半小时。这里把所有数据库操作收进dao包Activity 只负责取数据和刷 UI改表结构的时候你只需要动DBHelper和对应的 Dao改动面是可控的。再看bean包。Student实体里的字段名跟数据库列名保持一致sno、name、gender、age、major、phone好处是Cursor读出来直接new Student(...)不用做一层字段映射。代价是实体类跟表结构强耦合将来换 Room 的时候得补注解——但这属于后面的事先把功能跑通。2.2 Gradle 与依赖几行配置决定了你能不能一次同步成功课程设计工程最常见的翻车点不是代码是 Gradle 同步。老工程用的 AGP 版本和新版 Android Studio 对不上一导入就是满屏红字。这套工程的主模块配置大致长这样拿到之后先确认这几个值和你的 IDE 版本是否匹配// app/build.gradle android { namespace com.example.mystudent compileSdk 33 // 编译用的 SDK建议跟本机已装版本一致 defaultConfig { applicationId com.example.mystudent // 应用唯一标识改包名要同步改这里 minSdk 21 // 最低支持 Android 5.0机房老机器也能装 targetSdk 33 versionCode 1 versionName 1.0 } compileOptions { sourceCompatibility JavaVersion.VERSION_11 // 与 JDK 对齐避免编译报语法错 targetCompatibility JavaVersion.VERSION_11 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 // 提供 TextInputLayout、CardView implementation androidx.recyclerview:recyclerview:1.3.1 // 列表控件 implementation androidx.constraintlayout:constraintlayout:2.1.4 }先看compileSdk和sourceCompatibility。前者决定你能调用哪些系统 API后者决定编译器按哪个 Java 版本解析语法两个都跟本机环境错位的话报错信息往往非常迷惑比如提示找不到某个 lambda 相关的方法。minSdk 21是个保守选择机房里的老测试机通常能装如果你确定只在 Android 8 以上演示可以抬到 24能省掉一批兼容判断。applicationId这一行要特别留意。很多同学改包名只改了namespace或者只改了目录名结果装到手机上两个图标并存或者覆盖安装时报INSTALL_FAILED_UPDATE_INCOMPATIBLE。正确做法是用 Android Studio 的Refactor → Rename改包它会同步改namespace、applicationId、AndroidManifest里的package和 Java 文件的package声明改完再手动搜一遍残留的旧包名。提示gradle.properties里的org.gradle.jvmargs建议给到-Xmx2048m低配笔记本同步大工程时容易 OOM加这一行能省掉一次重启。2.3 数据库设计两张表撑起整个业务数据层是这个项目的骨架。一张user表管登录一张student表管业务数据够用且不容易乱。DBHelper继承SQLiteOpenHelper核心在onCreate建表和onUpgrade升级两处// db/DBHelper.java public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME mystudent.db; private static final int DB_VERSION 1; // 加字段时 1onUpgrade 才会触发 public static final String TABLE_STUDENT student; public static final String TABLE_USER user; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { // 学号设为 UNIQUE从数据层面挡住重复录入 db.execSQL(CREATE TABLE TABLE_STUDENT ( id INTEGER PRIMARY KEY AUTOINCREMENT, sno TEXT NOT NULL UNIQUE, name TEXT NOT NULL, gender TEXT, age INTEGER, major TEXT, phone TEXT, create_time TEXT)); db.execSQL(CREATE TABLE TABLE_USER ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL)); // 预置一个演示账号避免首次启动无法登录 db.execSQL(INSERT INTO TABLE_USER (username, password) VALUES (admin, 123456)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 课设阶段直接重建最简单真上生产要写增量迁移 if (oldVersion newVersion) { db.execSQL(DROP TABLE IF EXISTS TABLE_STUDENT); db.execSQL(DROP TABLE IF EXISTS TABLE_USER); onCreate(db); } } }onCreate只在数据库文件第一次创建时调用所以预置的admin/123456只写一次之后你手动往表里加数据重装 App 才会重新触发。sno TEXT NOT NULL UNIQUE这个约束很关键——它把「学号重复」这种业务校验下沉到了数据库层即使 UI 层漏判插入也会失败并抛SQLiteConstraintException你能第一时间发现逻辑漏洞。onUpgrade里直接DROP TABLE是课设阶段最省事的写法代价是升级即清库。答辩演示前如果你想加一个「班级」字段记得先把已有测试数据导出或者截图留档否则升一次版本数据全没血泪经验。真要做增量迁移标准做法是在onUpgrade里按oldVersion逐版本ALTER TABLE ADD COLUMN写成switch分段执行。2.4 Manifest 与页面约定四个 Activity 的跳转关系AndroidManifest.xml里把这四个 Activity 都注册了LoginActivity打上LAUNCHER作为入口。页面之间的跳转约定很简单登录成功 → 列表页列表页点悬浮按钮 → 编辑页新增模式列表页点某一项 → 编辑页编辑模式带id编辑页保存后setResult(RESULT_OK)返回并刷新列表。!-- AndroidManifest.xml 片段 -- application android:allowBackuptrue android:labelstring/app_name android:themestyle/Theme.MyStudent !-- 入口页登录 -- activity android:name.LoginActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity activity android:name.MainActivity android:exportedfalse / activity android:name.StudentListActivity android:exportedfalse / activity android:name.StudentEditActivity android:exportedfalse / /applicationandroid:exported这个属性从 Android 12 起是必填项漏写会直接编译失败报错信息是「Apps targeting Android 12 and higher are required to specify an explicit value for android:exported」。带intent-filter的入口页必须写true其余内部页面写false更安全也能避免被外部应用拉起。跳转时用Intent传id而不是传整个对象这一点值得强调。传对象要么实现Serializable性能差、易出错要么实现Parcelable样板代码多。传主键、回到编辑页再用id查一次库代价是一次毫秒级查询换来的是数据永远是最新的——列表页改了别的地方编辑页拿到的不会是一份过期快照。3. 核心链路拆解一次「新增学生」在代码里究竟走了哪几步功能跑通的关键不在页面多漂亮而在这条链路是否闭合点击保存 → 校验 → 写库 → 回传结果 → 列表刷新。任何一环断了表现出来都是「点了没反应」或者「列表不更新」。这一章按调用顺序把四段核心代码拆开看每段都能直接抄进你自己的工程。3.1 登录与会话SharedPreferences 记住登录态登录页做两件事校验账号密码、把登录状态存下来。校验走UserDao状态存SharedPreferences。别小看这一步很多课设的问题是「按返回键退出后再进来又要重新登录」体验分直接掉一档。// LoginActivity.java 登录逻辑 private void doLogin() { String username etUsername.getText().toString().trim(); String password etPassword.getText().toString().trim(); // 空值校验放在最前面避免把空串丢进 SQL if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) { Toast.makeText(this, 账号或密码不能为空, Toast.LENGTH_SHORT).show(); return; } UserDao userDao new UserDao(this); boolean ok userDao.checkLogin(username, password); if (!ok) { Toast.makeText(this, 账号或密码错误, Toast.LENGTH_SHORT).show(); return; } // 写入登录态key 用常量避免多处硬编码写错 getSharedPreferences(app_config, MODE_PRIVATE) .edit() .putBoolean(is_login, true) .putString(current_user, username) .apply(); // apply 异步落盘比 commit 更适合 UI 线程 startActivity(new Intent(this, StudentListActivity.class)); finish(); // 关键销毁登录页防止返回键退回登录界面 }checkLogin内部用Cursor遍历查询结果这里有个细节值得说不要用db.rawQuery拼接字符串。像select * from user where username username 这种写法遇到带单引号的输入会直接抛语法异常虽然课设场景没人攻击你但这个习惯一旦养成写别的项目迟早翻车。参数化查询db.rawQuery(sql, new String[]{username, password})只是多写一行收益是长期的。apply()和commit()的区别也顺手记一下apply()把修改先写内存、异步刷盘不阻塞 UIcommit()同步返回布尔值。登录态这种丢了也不致命的配置apply()完全够用。finish()那行更不能省否则用户按返回键会回到登录页看起来像「退出了但没完全退出」。3.2 列表渲染RecyclerView Adapter 的绑定套路列表页是整个 App 出现频率最高的界面。数据来源是StudentDao.queryAll()渲染靠StudentAdapter。这套组合的坑集中在「数据变了但界面没变」本质是 Adapter 里的 list 引用和外部持有的不是同一个对象。// adapter/StudentAdapter.java public class StudentAdapter extends RecyclerView.AdapterStudentAdapter.VH { private ListStudent data new ArrayList(); private OnItemClickListener listener; public interface OnItemClickListener { void onItemClick(Student student); // 点击进入编辑 void onItemLongClick(Student student); // 长按弹删除确认 } public void setOnItemClickListener(OnItemClickListener l) { this.listener l; } // 用新数据整体替换并刷新避免外部直接操作内部 list public void setData(ListStudent newData) { this.data.clear(); this.data.addAll(newData); notifyDataSetChanged(); } NonNull Override public VH onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View v LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_student, parent, false); return new VH(v); } Override public void onBindViewHolder(NonNull VH holder, int position) { Student s data.get(position); holder.tvName.setText(s.getName()); holder.tvSno.setText(学号 s.getSno()); holder.tvMajor.setText(s.getMajor() null ? 未填写 : s.getMajor()); // 点击事件绑在 itemView 上不要绑在单个 TextView 上 holder.itemView.setOnClickListener(v - { if (listener ! null) listener.onItemClick(s); }); holder.itemView.setOnLongClickListener(v - { if (listener ! null) listener.onItemLongClick(s); return true; // 返回 true 表示消费事件否则会同时触发点击 }); } Override public int getItemCount() { return data.size(); } static class VH extends RecyclerView.ViewHolder { TextView tvName, tvSno, tvMajor; VH(NonNull View itemView) { super(itemView); tvName itemView.findViewById(R.id.tv_name); tvSno itemView.findViewById(R.id.tv_sno); tvMajor itemView.findViewById(R.id.tv_major); } } }setData这个方法看着啰嗦实际是在防一个高频 bug如果外部直接adapter.data newList然后notifyDataSetChanged()看上去能跑但只要有人持有了旧的data引用去add界面就会和预期不一致。统一走setData刷新逻辑只有一个出口出问题好定位。onBindViewHolder里有个容易忽略的点setOnLongClickListener要返回true。返回false的话长按事件没被消费会继续冒泡触发点击结果就是「长按弹删除框的同时跳进了编辑页」。列表页拿到 Adapter 回调后做两件事点击时Intent带上id跳编辑页长按时弹AlertDialog确认后调StudentDao.delete(id)再重新queryAll()刷进 Adapter。3.3 新增与编辑复用同一个页面StudentEditActivity用Intent里有没有id来区分模式这是最省事的写法比写两个几乎一样的页面少一半维护量。// StudentEditActivity.java 保存逻辑 private void save() { String sno etSno.getText().toString().trim(); String name etName.getText().toString().trim(); String ageStr etAge.getText().toString().trim(); // 必填校验 if (TextUtils.isEmpty(sno) || TextUtils.isEmpty(name)) { Toast.makeText(this, 学号和姓名不能为空, Toast.LENGTH_SHORT).show(); return; } int age 0; if (!TextUtils.isEmpty(ageStr)) { try { age Integer.parseInt(ageStr); } catch (NumberFormatException e) { // 输入了非数字直接拦下不要让异常冒到主线程 Toast.makeText(this, 年龄请填写数字, Toast.LENGTH_SHORT).show(); return; } } Student s new Student(); s.setSno(sno); s.setName(name); s.setGender(rgGender.getCheckedRadioButtonId() R.id.rb_male ? 男 : 女); s.setAge(age); s.setMajor(etMajor.getText().toString().trim()); s.setPhone(etPhone.getText().toString().trim()); StudentDao dao new StudentDao(this); boolean success; if (studentId 0) { // 编辑模式Intent 带回了主键 s.setId(studentId); success dao.update(s) 0; } else { // 新增模式 long newId dao.insert(s); success newId 0; } if (success) { setResult(RESULT_OK); // 通知列表页刷新 finish(); } else { Toast.makeText(this, 保存失败学号可能已存在, Toast.LENGTH_SHORT).show(); } }三个参数值得单独说。studentId 0是模式开关onCreate里用getIntent().getLongExtra(id, -1)取值默认-1表示新增。setResult(RESULT_OK)配合列表页的onActivityResult或者registerForActivityResult才能触发刷新漏掉这行就是经典的「保存成功了但列表没变退出重进才出现」。dao.insert返回的是新行主键-1表示插入失败——学号重复时会走到这里所以 Toast 文案里带一句「学号可能已存在」用户才知道该改哪儿。3.4 删除、搜索与数据刷新的一致性Dao 层把四种操作写全Activity 就只剩调用。update和delete返回受影响行数用这个数字判断是否真的命中比单纯看有没有抛异常更可靠。// dao/StudentDao.java public long insert(Student s) { ContentValues cv new ContentValues(); cv.put(sno, s.getSno()); cv.put(name, s.getName()); cv.put(gender, s.getGender()); cv.put(age, s.getAge()); cv.put(major, s.getMajor()); cv.put(phone, s.getPhone()); cv.put(create_time, new SimpleDateFormat(yyyy-MM-dd HH:mm, Locale.CHINA) .format(new Date())); // 学号冲突时直接失败把判断权交回业务层 return getWritableDatabase().insert(TABLE_STUDENT, null, cv); } public ListStudent queryByKeyword(String keyword) { ListStudent list new ArrayList(); // 姓名或学号模糊匹配参数化查询防止单引号导致语法错误 String sql SELECT * FROM TABLE_STUDENT WHERE name LIKE ? OR sno LIKE ? ORDER BY id DESC; String like % keyword %; Cursor c getReadableDatabase().rawQuery(sql, new String[]{like, like}); while (c.moveToNext()) { Student s new Student(); s.setId(c.getLong(c.getColumnIndexOrThrow(id))); s.setSno(c.getString(c.getColumnIndexOrThrow(sno))); s.setName(c.getString(c.getColumnIndexOrThrow(name))); s.setGender(c.getString(c.getColumnIndexOrThrow(gender))); s.setAge(c.getInt(c.getColumnIndexOrThrow(age))); s.setMajor(c.getString(c.getColumnIndexOrThrow(major))); list.add(s); } c.close(); // 必须关否则游标泄漏数据量大了会出问题 return list; } public int delete(long id) { return getWritableDatabase().delete(TABLE_STUDENT, id?, new String[]{String.valueOf(id)}); }getColumnIndexOrThrow比getColumnIndex好用的地方在于列名写错会立刻抛异常而不是静默返回-1然后getString(-1)崩在一个看起来毫无关系的地方。调试期用带OrThrow的版本能省下大量找错时间。c.close()那行是硬要求。Cursor 不关数据量小的时候看不出问题几百条之后就可能出现「查询越来越慢」或者SQLiteDatabase相关警告。规范写法是try-with-resources或者try/finally里关但在课设代码里显式一行close()也够用前提是别在return之前提前退出。搜索框的联动建议加个 300ms 防抖TextWatcher每敲一个字就查一次库在小数据量下无感但这是个值得养成的好习惯。4. 从导入到跑通一份可以直接照着敲的启动清单代码看懂是一回事让它在你机器上跑起来是另一回事。这一章按顺序走一遍每一步都给出「怎么判断这一步成功了」出问题能立刻定位到哪一环。4.1 导入工程与首次同步解压后不要直接双击.rar里的工程先解压到不含中文和空格的路径比如D:\code\mystudent。路径带中文会导致 Gradle 某些插件解析失败报错信息往往跟路径毫无关系排查起来极其费劲。Android Studio 里选Open指向解压后的根目录含settings.gradle那一层等它自动同步。第一次同步会下载 AGP 和依赖视网络情况 315 分钟。判断成功的标志是底部Build窗口出现BUILD SUCCESSFUL左侧Project面板切到Android视图能看到app模块和java、res目录树。如果卡在Downloading gradle-x.x-bin.zip说明 Gradle 发行版没下下来。这种情况优先检查 IDE 自带的 Gradle 版本或者用本机已缓存的版本File → Project Structure → Project里把 Gradle 版本改成你~/.gradle/wrapper/dists目录下已有的那个。别在这上面硬等换个版本往往 5 分钟解决。4.2 模拟器与真机SDK 版本对齐跑之前确认三件事compileSdk对应的 SDK Platform 已安装、minSdk 21的设备可用、模拟器镜像是 x86_64 或 arm64跟主机 CPU 匹配。用命令行检查设备连接情况# 查看已连接设备列表为空说明真机没识别到检查 USB 调试和驱动 adb devices # 查看设备 Android 版本确认不低于 minSdk 21 adb shell getprop ro.build.version.release # 编译 debug 包不经过 IDE能快速判断是代码问题还是 IDE 问题 ./gradlew assembleDebug # 安装到已连接设备覆盖安装保留数据 adb install -r app/build/outputs/apk/debug/app-debug.apk # 安装失败又想彻底重来时先卸载会清空本地数据库 adb uninstall com.example.mystudent./gradlew assembleDebug这一步很有价值。IDE 里的报错经常被各种插件包裹得看不清命令行跑一次失败原因会原原本本打在终端里。Windows 上把./gradlew换成gradlew.bat。真机连接不上的话按这个顺序查数据线是不是只能充电的劣质线 → 手机开发者选项里「USB 调试」有没有开 → 连接时手机弹的授权框有没有点「允许」→adb kill-server adb start-server重启服务。这几步能解决九成的「设备列表为空」。4.3 首次启动的数据初始化App 装好后第一次打开DBHelper.onCreate会建库并插入演示账号。用admin / 123456登录进去列表是空的——这是正常的工程没有预置学生数据。想快速看到效果有两个办法。一是在界面上手动加两三条验证增删改查闭环二是往数据库里灌一批测试数据方便测分页和搜索。用adb shell直接操作 SQLite 最直接# 进入应用的数据库目录debuggable 包才能访问 adb shell run-as com.example.mystudent ls databases # 用 sqlite3 插入几条测试数据注意 sqlite3 在部分系统镜像里不存在 # 缺失时改用 Android Studio 的 Database Inspector adb shell run-as com.example.mystudent sqlite3 databases/mystudent.db \ \INSERT INTO student(sno,name,gender,age,major,phone,create_time) \ VALUES(20240101,张三,男,20,计算机科学与技术,13800000001,2024-01-01 09:00);\灌 20 条左右就能看出列表滚动是否流畅、搜索是否生效。Android Studio 的Database InspectorView → Tool Windows → App Inspection更友好能直接看表、改数据、跑查询还不用敲命令推荐用它做调试期的主要工具。4.4 打包 APK 与交付前自检课程设计通常要交一个能装到老师手机上的 APK。Build → Generate Signed Bundle / APK走签名流程或者更简单的Build → Build Bundle(s) / APK(s) → Build APK(s)产物在app/build/outputs/apk/debug/。debug 包也能装只是签名是调试证书部分手机尤其是开了应用完整性校验的会拦一下。交付前按这张表过一遍能挡掉大部分「到我这儿怎么打不开」的尴尬检查项怎么看不合格的后果包名是否已改成自己的applicationId与AndroidManifest与别人工程冲突覆盖安装失败应用名与图标strings.xml的app_name、mipmap目录装上去叫 MyStudent扣印象分最低版本minSdk与目标机型老师的老手机装不上首次启动能否登录卸载重装后试一次数据库残留导致演示时登不进无网络时功能是否正常开飞行模式走一遍答辩现场断网就演示不了数据是否已清空删掉测试数据再打包一堆「张三李四」显得没做完最后一条尤其重要。演示用的数据和调试用的数据要分开交上去的包里塞满自己乱敲的测试记录第一印象就没了。5. 避坑与排查五个让我返工过的真实问题5.1 现象、原因与解决办法现象一Gradle 同步报Could not find com.android.tools.build:gradle:x.x.x。原因是工程里写的 AGP 版本在仓库里拉不到常见于老工程配了新 IDE或者settings.gradle里缺了google()仓库。解决办法是先看settings.gradle的pluginManagement.repositories和dependencyResolutionManagement.repositories确认google()和mavenCentral()都在然后去 AGP 官方版本列表里挑一个和你 IDE 版本相邻的改build.gradle项目级里的classpath。改完记得点Sync Now别直接 RunRun 用的是上一次同步的产物会误导你以为没改成功。现象二新增学生成功后列表页没变化杀掉 App 重进才看到。九成是三个原因之一编辑页忘了setResult(RESULT_OK)、列表页没在onActivityResult里重新查库、或者查完库没调adapter.setData()只改了局部变量。排查方式很土但有效——在列表页的onResume里打印一条Log.d(ListPage, onResume, count dao.queryAll().size())如果这里数字是新的但界面还是旧的问题就在 Adapter 刷新如果这里数字就是旧的问题在回传链路。一次定位别凭感觉改。现象三插入时报UNIQUE constraint failed: student.snoApp 直接崩。insert遇到唯一约束冲突会抛SQLiteConstraintException如果没捕获就崩在 UI 线程。两种处理方式一是插入前先queryBySno判重把校验提前到业务层体验更好二是用insertWithOnConflict(TABLE_STUDENT, null, cv, SQLiteDatabase.CONFLICT_IGNORE)冲突时返回-1而不抛异常。课设场景建议走第一种因为要给用户明确的「学号已存在」提示静默忽略反而让人困惑。现象四真机安装报INSTALL_FAILED_UPDATE_INCOMPATIBLE或INSTALL_FAILED_VERSION_DOWNGRADE。前者是签名不一致——手机上已经装了一个同包名但用不同证书签名的版本比如之前装过同学改过的包。后者是versionCode比已装版本低。两个都用adb uninstall 包名解决代价是本地数据库被清空。所以调试期频繁改包名、频繁装不同人版本的机器上养成「改包名后先卸载旧包」的习惯能少踩很多次。现象五RecyclerView一滑动就卡或者列表项内容错乱明明点了张三跳过去是李四。卡顿通常是onBindViewHolder里做了耗时操作比如每次都查一次数据库取专业名正确做法是查询时用JOIN或者提前把字段读进Student对象。内容错乱则是 ViewHolder 复用没处理好if/else分支里只设置了部分控件另一部分沿用了上一个 item 的值。解决办法是每个分支都把该控件显式设一遍别指望默认值尤其是Visibility和Background。5.2 排查工具箱logcat 与 Database Inspector出问题时先别改代码先看现场。logcat 按包名过滤能把无关日志全挡掉# 只看本应用的日志-s 表示静默其他 tag adb logcat -s ListPage:D StudentDao:D AndroidRuntime:E # 清空日志缓冲然后复现问题日志更干净 adb logcat -c adb logcat -s AndroidRuntime:EAndroidRuntime:E这一条尤其关键所有未捕获异常的堆栈都在这个 tag 下。崩溃之后第一条要看的永远是它堆栈第一行的Caused by直接指向出问题的类和方法比在 IDE 里翻半天快得多。数据库层面的问题用Database Inspector看。它能看到实时表数据、执行 SQL、还能在 App 运行时改值验证逻辑。有个实际好用的技巧怀疑某个查询写错时直接在 Inspector 里跑一遍同样的 SQL如果结果和 App 显示的不一致问题就在 Java 侧的Cursor读取而不在 SQL 本身——这一步能砍掉一半的排查范围。注意Database Inspector只对debuggable的包生效。打 release 包之后就连不上了调试期别急着切 release。6. 二次开发把课设改造成能讲二十分钟的项目跑通之后真正的价值在于它是个可以往上长东西的底座。答辩时老师最爱问的一句是「如果让你再加一个功能你会怎么做」这时候能答出「改哪几个文件、加哪张表、动哪一层」和答「我再研究研究」是两种完全不同的印象分。最推荐的第一个改造是加字段。假设要加「班级」和「邮箱」完整步骤是Student.java加两个属性 getter/setter →DBHelper把DB_VERSION改成 2onCreate的建表语句加两列onUpgrade里补ALTER TABLE student ADD COLUMN class_name TEXT→StudentDao的insert、update、queryByKeyword都补上对应的ContentValues.put和Cursor读取 → 编辑页布局加两个TextInputLayout→StudentAdapter的 item 布局加一行显示。五处改动一次跑通你就完整走了一遍「数据结构变更如何影响全链路」这个经验比写十个页面都值钱。// 增量迁移的正确姿势不要 DROP逐版本 ADD COLUMN Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(ALTER TABLE student ADD COLUMN class_name TEXT); db.execSQL(ALTER TABLE student ADD COLUMN email TEXT); } // 将来 v3 再加一段 if (oldVersion 3) { ... }历史数据全程保留 }第二条路是换数据层。把SQLiteOpenHelper 手写 Dao换成Room业务代码几乎不用动因为 Activity 只依赖 Dao 接口。Room 的收益是编译期校验 SQL、自动生成Cursor映射、支持LiveData观察数据变化——列表页从此不用手动setData数据一变界面自己刷新。改造量集中在bean加Entity注解、Dao改成Dao接口、DBHelper换成RoomDatabase子类两三个小时能搞定但足够你在简历上写一句「熟悉 Room 与数据持久化最佳实践」。第三条是加导出功能实用且实现短。把queryAll()的结果拼成 CSV 写到getExternalFilesDir()下再配合FileProvider分享出去整个功能不到 80 行。面试聊到「你怎么处理文件权限和 Android 7.0 之后的 URI 变更」这就是现成的素材。还有两个成本更低但效果明显的小改造列表加SwipeRefreshLayout下拉刷新顺手理解一次异步回调、搜索框加 300ms 防抖、列表项加左滑删除ItemTouchHelper。每个都是几十行但拼起来就是一个「有交互设计感」的 App而不是一个只能点按钮的作业。我自己的习惯是每改完一个功能先卸载重装、再用飞行模式走一遍全流程再提交代码。因为课设项目最容易出问题的永远不是新功能本身而是升级数据库时把老数据搞没了或者某个页面忘了处理空数据直接空指针。这个动作固定下来之后我交出去的包就再没出现过「在我这儿是好的」这种对话。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站