ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android Studio学生信息管理App完整实现与避坑指南

Android Studio学生信息管理App完整实现与避坑指南

简介:这份资源是面向安卓初学者与课程设计需求者的完整学生信息管理App项目源码,基于Android Studio开发,适合作为课程设计参考、个人练手项目或作品集素材。压缩包共1426个文件,约21.14MB,涵盖xml布局、png图片、java源码、class与dex编译产物、json配置、apk安装包及gradle构建脚本等,完整保留了工程结构与构建产物。项目覆盖Android开发核心知识点:XML布局与RecyclerView列表展示、SQLite与SharedPreferences数据存储、Activity生命周期与Intent跳转、按钮点击与输入验证、AlertDialog对话框、菜单资源、运行时权限、自定义Adapter及Logcat调试等,并包含生成APK与发布流程的实践内容。已有3734人学习下载,读者可借此快速理解安卓应用架构,对照源码完成课程设计或二次开发,节省从零搭建工程的时间。

1. 从一份课程设计压缩包说起:Android Studio 学生信息管理 App 到底能跑通什么

如果你正在搜 Android Studio 学生信息管理 App 的完整实现,大概率是三种处境之一:课程设计 deadline 逼近、想找一个能跑通的参考工程、或者单纯想看看一个标准 CRUD 安卓项目该长什么样。Mystudent.rar 这个包解决的就是这件事——它是一套基于 Android Studio 的学生信息管理 App 完整工程,覆盖增删改查、列表展示、数据库持久化这些课程设计里绕不开的核心环节。适合谁?适合刚学完 Activity 和基础控件、需要一份能编译能运行能改的工程来对照的人;也适合已经能写页面但没完整搭过数据层的新手。它不解决高并发、不解决复杂架构,但能把「一个安卓 App 从建表到跑起来」这条链路完整走一遍。下面我按拆包、导入、跑通、改功能、排错的顺序,把这份资源里真正值得看的东西讲清楚。

2. 拆包与工程结构:先看清 Android Studio 项目骨架再动手

拿到一个 .rar 压缩包,最忌讳的就是直接双击导入然后报一堆红。先把包解开,用文件管理器看一眼目录结构,比在 IDE 里瞎点强得多。Mystudent 这类课程设计工程,通常是一个标准的 Gradle 项目,根目录下会有 settings.gradle、build.gradle、gradle.properties,以及一个 app 模块目录。你要做的第一件事不是打开 Android Studio,而是确认这个工程用的是哪个 Gradle 版本、哪个 Android Gradle Plugin 版本,因为这两项直接决定你能不能顺利同步。

2.1 解压后先看这五个文件

解压到一个纯英文路径下,路径里不要有中文和空格,这是血泪经验。然后按顺序看这几个文件:

  • settings.gradle:确认模块名,通常是include ':app'
  • 根目录build.gradle:看com.android.tools.build:gradle的版本号
  • gradle/wrapper/gradle-wrapper.properties:看distributionUrl里的 Gradle 版本
  • app/build.gradle:看compileSdk、minSdk、targetSdk以及依赖库
  • app/src/main/AndroidManifest.xml:看注册了哪些 Activity,哪个是入口

这五个文件看完,你对这个工程能不能在你机器上跑起来就有七成判断了。常见情况是:工程写于两三年前,compileSdk是 30 或 31,而你本地装的是最新版 Android Studio,默认 Gradle 版本偏高,一同步就报插件不兼容。

2.2 用命令行先验证 Gradle 能不能过

在导入 IDE 之前,我一般会先在终端里跑一次 Gradle 任务,把环境问题提前暴露出来。进入工程根目录,执行:

# Windows 下用 gradlew.bat,macOS/Linux 用 ./gradlew # 先只看版本,确认 wrapper 能正常下载对应 Gradle ./gradlew --version # 尝试编译 debug 包,不安装,只验证编译链路 ./gradlew assembleDebug --stacktrace

逻辑说明:--version这一步会触发 Gradle Wrapper 去下载gradle-wrapper.properties里指定的 Gradle 发行版。如果这一步就卡住或报错,说明网络或版本有问题,此时进 IDE 也是白搭。assembleDebug是真正走一遍编译,--stacktrace让你在失败时能看到完整调用栈,而不是只看到一句「Build failed」。

参数说明:如果你本地没有配置JAVA_HOME,Gradle 会报找不到 Java。Android Studio 自带 JDK,但命令行不一定认。常见做法是把JAVA_HOME指向 Android Studio 内置的 jbr 目录,或者单独装一个 JDK 17。这一步过了,再进 IDE 导入,成功率会高很多。

2.3 导入 Android Studio 的正确姿势

不要用「Open」直接打开 app 目录,要用「Open」打开工程根目录,也就是包含settings.gradle的那一层。导入后如果提示 Gradle Sync 失败,先别急着改代码,去File > Project Structure里核对 SDK 路径和 JDK 版本。课程设计工程最常见的翻车点就是 JDK 版本对不上:老工程用 JDK 8 编译,新 IDE 默认 JDK 17,一同步就报Unsupported class file major version。解决办法是在File > Settings > Build > Build Tools > Gradle里把 Gradle JDK 改成 11 或 17,逐个试,哪个能过用哪个。

3. 数据层实现:SQLite 建表、DAO 封装与增删改查落地

学生信息管理 App 的核心不是界面,是数据怎么存、怎么取。这类课程设计工程,数据层通常有两种写法:一种是直接用SQLiteOpenHelper手写建表和 CRUD,另一种是引入 Room 做 ORM 映射。Mystudent 这类包,为了体现「完整」,多半是手写 SQLite 为主,因为这样代码量大、看起来工作饱满,也方便答辩时讲原理。下面按手写 SQLite 的路子拆,Room 的差异我会在末尾提一句。

3.1 建表语句与字段设计

学生信息表一般包含学号、姓名、性别、年龄、专业、班级这几个字段。学号做主键,因为学号天然唯一。建表 SQL 通常长这样:

// 在 SQLiteOpenHelper 的 onCreate 中执行 public static final String CREATE_STUDENT = "create table Student(" + "id integer primary key autoincrement, " + "studentNo text unique not null, " + "name text not null, " + "gender text, " + "age integer, " + "major text, " + "className text)";

逻辑说明:id是自增主键,方便内部引用;studentNo加了unique约束,防止重复录入学号,这是业务上的硬要求。name设为not null,因为没名字的学生记录没有意义。其余字段允许为空,因为录入时可能信息不全。

参数说明:字段类型上,SQLite 是动态类型,integer和text只是亲和类型,实际存什么都能存,但为了代码可读性和查询效率,还是按语义写。autoincrement只在integer primary key上生效,别乱加。

3.2 DAO 封装:把 SQL 关进一个类里

直接在 Activity 里写 SQL 是新手最容易犯的错,改起来痛苦。常见做法是抽一个StudentDao类,把所有数据库操作收口:

public class StudentDao { private final SQLiteOpenHelper helper; public StudentDao(Context context) { helper = new StudentDbHelper(context); } // 插入 public long insert(Student s) { SQLiteDatabase db = helper.getWritableDatabase(); ContentValues cv = new ContentValues(); cv.put("studentNo", s.getStudentNo()); cv.put("name", s.getName()); cv.put("gender", s.getGender()); cv.put("age", s.getAge()); cv.put("major", s.getMajor()); cv.put("className", s.getClassName()); long rowId = db.insert("Student", null, cv); db.close(); return rowId; // -1 表示插入失败 } // 删除 public int deleteByNo(String studentNo) { SQLiteDatabase db = helper.getWritableDatabase(); int rows = db.delete("Student", "studentNo = ?", new String[]{studentNo}); db.close(); return rows; // 返回受影响行数 } // 查询全部 public List<Student> queryAll() { List<Student> list = new ArrayList<>(); SQLiteDatabase db = helper.getReadableDatabase(); Cursor cursor = db.query("Student", null, null, null, null, null, "studentNo asc"); while (cursor.moveToNext()) { Student s = new Student(); s.setStudentNo(cursor.getString(cursor.getColumnIndexOrThrow("studentNo"))); s.setName(cursor.getString(cursor.getColumnIndexOrThrow("name"))); s.setGender(cursor.getString(cursor.getColumnIndexOrThrow("gender"))); s.setAge(cursor.getInt(cursor.getColumnIndexOrThrow("age"))); s.setMajor(cursor.getString(cursor.getColumnIndexOrThrow("major"))); s.setClassName(cursor.getString(cursor.getColumnIndexOrThrow("className"))); list.add(s); } cursor.close(); db.close(); return list; } }

逻辑说明:insert用ContentValues而不是拼 SQL 字符串,避免 SQL 注入和引号转义问题。delete的第二个参数用?占位符,第三个参数传实际值,这是 Android SQLite API 的标准防注入写法。query方法里orderBy传"studentNo asc",让列表按学号排序,符合学生信息管理的习惯。

参数说明:db.insert的第二个参数是nullColumnHack,当ContentValues为空时用它指定一个允许为 null 的列,一般传null即可。cursor.getColumnIndexOrThrow比getColumnIndex更安全,列名写错会直接抛异常,而不是静默返回 -1 导致数据错位。

3.3 在 Activity 里调用 DAO 并刷新列表

数据层写好后,Activity 里只负责调方法和更新 UI。典型写法:

// 查询并绑定到 ListView private void refreshList() { List<Student> students = dao.queryAll(); SimpleAdapter adapter = new SimpleAdapter( this, convertToListMap(students), R.layout.item_student, new String[]{"studentNo", "name", "major"}, new int[]{R.id.tv_no, R.id.tv_name, R.id.tv_major} ); listView.setAdapter(adapter); } // 把 Student 对象转成 SimpleAdapter 需要的 Map 列表 private List<Map<String, Object>> convertToListMap(List<Student> students) { List<Map<String, Object>> data = new ArrayList<>(); for (Student s : students) { Map<String, Object> map = new HashMap<>(); map.put("studentNo", s.getStudentNo()); map.put("name", s.getName()); map.put("major", s.getMajor()); data.add(map); } return data; }

逻辑说明:每次增删改之后都调一次refreshList(),保证界面和数据一致。SimpleAdapter是课程设计里最常用的适配器,因为它不用自己写 ViewHolder,代码量少,适合演示。

参数说明:SimpleAdapter的from数组和to数组必须一一对应,顺序不能错,否则会出现「学号显示成姓名」这种玄学 bug。R.layout.item_student是列表项布局,里面要有对应的 TextView。

4. 界面与交互:Activity 跳转、列表刷新与表单校验

界面层是答辩时最直观的部分,也是新手最容易写出「能跑但很丑」的地方。这一章讲三个关键点:Activity 之间怎么传数据、列表怎么刷新、表单怎么做基本校验。这三点做好了,整个 App 的交互闭环就成立了。

4.1 Activity 跳转与数据传递

从列表页跳到添加页,或者从列表项跳到详情页,都需要传数据。Android 里标准做法是 Intent 加 extras:

// 列表页:点击某一项,跳到详情页并传学号 listView.setOnItemClickListener((parent, view, position, id) -> { Map<String, Object> item = (Map<String, Object>) parent.getItemAtPosition(position); String studentNo = (String) item.get("studentNo"); Intent intent = new Intent(MainActivity.this, DetailActivity.class); intent.putExtra("studentNo", studentNo); startActivity(intent); }); // 详情页:取出学号,再查数据库拿完整信息 String studentNo = getIntent().getStringExtra("studentNo"); Student s = dao.queryByNo(studentNo);

逻辑说明:列表项里只放了学号、姓名、专业三个字段,但详情页需要完整信息,所以传学号过去再查一次数据库,而不是把整个对象序列化传过去。这样做的好处是数据始终以数据库为准,不会出现「列表改了详情没改」的不一致。

参数说明:putExtra的 key 用字符串常量,建议在详情页里定义public static final String EXTRA_STUDENT_NO = "studentNo",避免两边写错。getStringExtra在 key 不存在时返回 null,所以详情页里要做空判断,否则一取就崩。

4.2 列表刷新的三种时机

列表刷新不是随便调的,要在正确的时机调,否则会出现「删了还在、加了不显示」的问题。常见做法是:

  • onCreate里调一次,首次加载
  • onResume里调一次,从添加页或编辑页返回时刷新
  • 删除操作的回调里调一次,删除后立即刷新
@Override protected void onResume() { super.onResume(); refreshList(); // 从其他页面返回时重新查库 }

逻辑说明:onResume在 Activity 每次回到前台时都会执行,包括从其他 Activity 返回。把刷新放这里,比在onActivityResult里处理更省事,适合课程设计这种简单场景。

参数说明:如果列表数据量大,onResume里每次都全量查库会有性能问题,但学生信息管理这种几十条数据的场景,完全不用考虑。真到了几千条,再考虑分页或缓存。

4.3 表单校验:别让空学号进数据库

添加和编辑页面必须有校验,否则用户不填学号直接点保存,数据库会抛约束异常,App 直接崩。校验逻辑:

private boolean validateForm() { String no = etNo.getText().toString().trim(); String name = etName.getText().toString().trim(); if (TextUtils.isEmpty(no)) { etNo.setError("学号不能为空"); return false; } if (TextUtils.isEmpty(name)) { etName.setError("姓名不能为空"); return false; } if (!no.matches("\\d{6,12}")) { etNo.setError("学号应为6到12位数字"); return false; } return true; }

逻辑说明:先判空,再用正则校验格式。setError会在输入框右侧显示一个红色感叹号,点一下能看到提示文字,是 Android 原生支持的校验反馈方式,不用自己弹 Toast。

参数说明:正则\\d{6,12}表示 6 到 12 位数字,具体位数按学校规则改。trim()去掉首尾空格,防止用户复制粘贴时带入空格导致校验通过但存进去带空格。

5. 避坑与排查:导入、编译、运行中最容易翻车的五个点

这一章是我拆这类课程设计包时踩过的坑,按「现象 → 原因 → 解决」写,你遇到问题时可以直接对号入座。

5.1 现象:Gradle Sync 卡在 Download 不动

原因:gradle-wrapper.properties里指定的 Gradle 发行版需要从境外服务器下载,网络不通就一直卡着。

解决:把distributionUrl改成国内镜像地址,或者用本地已经下载过的 Gradle 版本。改完后File > Invalidate Caches / Restart再同步。如果还是不行,检查 Android Studio 的 HTTP Proxy 设置,确认没有配错代理。

5.2 现象:编译报Unsupported class file major version 61

原因:工程用的 Gradle 版本太老,不支持 JDK 17 编译出来的 class 文件。61 对应 JDK 17,老 Gradle 只认到 55(JDK 11)甚至更低。

解决:两条路。一是把 Gradle JDK 降到 11,在Settings > Build Tools > Gradle > Gradle JDK里改。二是升级 Gradle 版本,改gradle-wrapper.properties里的distributionUrl和根build.gradle里的 AGP 版本,两者要匹配。课程设计场景下,降 JDK 更快。

5.3 现象:App 装上后一打开就闪退

原因:多半是AndroidManifest.xml里没注册 Activity,或者注册了但exported属性没写。Android 12 以后,带 intent-filter 的 Activity 必须显式声明android:exported。

解决:打开 Manifest,检查每个 Activity 是否都有<activity android:name=".XxxActivity" />,入口 Activity 要有android:exported="true"。改完重新编译安装。

5.4 现象:数据库插入成功但列表不显示

原因:插入后没有调refreshList(),或者refreshList()里查的是旧的 Cursor。也有可能是SimpleAdapter的from和to数组对不上。

解决:确认插入操作的回调里调了刷新方法。检查from数组里的 key 和convertToListMap里map.put的 key 是否完全一致,大小写都不能差。to数组里的控件 id 要和item_student.xml里的 id 一致。

5.5 现象:删除后重新进入又出现了

原因:删除时用的条件不对,比如用name删而不是用studentNo删,重名时删错行;或者删除后没有真正提交事务。

解决:删除条件必须用唯一键,学号是首选。db.delete返回的受影响行数要判断,如果返回 0 说明没删到,要检查条件值是否传对。另外确认db.close()之前没有异常抛出导致事务未提交。

6. 二次开发与验证:把课程设计改成能讲清楚的项目

跑通只是第一步,课程设计要答辩,你得能讲清楚每一层在干什么。这一章讲怎么在现有工程上做二次开发,以及怎么验证你的改动是对的。

6.1 加一个搜索功能,验证数据层是否真的通了

搜索是检验 DAO 封装质量的好方法。在StudentDao里加一个模糊查询:

public List<Student> searchByName(String keyword) { List<Student> list = new ArrayList<>(); SQLiteDatabase db = helper.getReadableDatabase(); Cursor cursor = db.query("Student", null, "name like ?", new String[]{"%" + keyword + "%"}, null, null, "studentNo asc"); while (cursor.moveToNext()) { // 同 queryAll 的映射逻辑 } cursor.close(); db.close(); return list; }

逻辑说明:like ?配合%keyword%实现模糊匹配。参数用?占位,实际值在第二个参数数组里传,防止注入。

参数说明:%是 SQL 通配符,匹配任意字符。如果关键词里本身有%或_,需要转义,但课程设计场景下可以忽略。

6.2 用日志验证每一步的数据流

在关键位置打 Log,是排查数据问题最直接的手段:

// 插入后 long id = dao.insert(student); Log.d("StudentDao", "insert result id=" + id); // 查询后 List<Student> list = dao.queryAll(); Log.d("StudentDao", "queryAll size=" + list.size());

逻辑说明:Log.d的 tag 用类名,方便在 Logcat 里过滤。插入返回 -1 说明失败,查询返回 0 说明表是空的,一眼就能定位问题在哪一层。

参数说明:Logcat 里可以按 tag 过滤,也可以按包名过滤。如果 release 包看不到日志,检查build.gradle里minifyEnabled是否开了混淆,混淆后 tag 会变。

6.3 验证清单:答辩前自己走一遍

验证项操作预期结果
首次启动安装后打开列表为空,不闪退
添加填完整信息保存返回列表能看到新记录
添加校验学号留空保存输入框报错,不保存
重复学号用已存在的学号保存提示失败或数据库拒绝
编辑修改姓名保存列表显示新姓名
删除删除一条记录列表立即少一条
搜索输入姓名关键字只显示匹配记录
退出重进杀掉进程再打开数据仍在,说明持久化成功

这张表走一遍,基本能覆盖课程设计答辩时老师会问的所有操作。哪一项没过,就回到对应章节查。

6.4 从手写 SQLite 迁到 Room 的思路

如果想让项目看起来更「现代」,可以把数据层换成 Room。核心改动是三处:实体类加@Entity注解,DAO 接口加@Dao和@Query注解,数据库类继承RoomDatabase。查询方法从返回 Cursor 改成返回List<Student>,Room 自动做映射。迁移过程中最容易翻车的是实体类字段名和列名不一致,需要在@ColumnInfo里显式指定。改完后编译期就能发现 SQL 错误,比手写 SQLite 运行时才崩要友好。

从那以后我每次拿到这类课程设计包,都强制自己先跑一遍assembleDebug,再按验证清单走一遍,确认数据流是通的,才动手改功能。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进