Java写的安卓学生信息管理APP源码,带SQLite增删改查,Android Studio 7.5可直接编译运行
本文还有配套的精品资源,点击获取
简介:这个学生信息管理APP用Java开发,基于Android Studio 7.5环境,本地数据全靠SQLite存,不用联网也能用。功能包括添加学生(填学号、姓名、年级、班级、课程)、删除指定学生、修改任意字段、按条件查单个或多个学生、完整列表滚动展示。还额外做了管理员账号维护模块,能改密码、切换权限。项目结构很规整:app模块下有清晰的XML布局文件、Java Activity和Fragment逻辑代码、SQLiteOpenHelper封装好的数据库操作类;Gradle配置齐全,build.gradle、settings.gradle、gradlew都配好了,local.properties留了占位提示。demo.jpg是主界面截图,真机和模拟器都测过能跑起来,导入AS后点一下run就能看到效果,不依赖第三方SDK或网络接口。适合用来交毕业设计,也适合刚学Android的同学跟着敲一遍理解Activity跳转、ListView/RecyclerView绑定、SQL语句写法和事务处理。
1. 项目概述:为什么这个学生信息管理APP值得你花时间细看
我带过三届本科毕业设计,每年都有至少二十个同学卡在“第一个能跑起来的完整Android项目”上——不是写不出Hello World,而是写不出一个有真实数据流转、有界面交互闭环、有本地持久化逻辑的“小而全”的应用。这套用Java写的学生信息管理APP源码,就是我反复打磨后给新手准备的“通关钥匙”。它不炫技,不堆砌高阶框架,但把Android开发最核心的几根骨头都拆得清清楚楚:Activity生命周期怎么配合数据操作、ListView/RecyclerView如何与SQLite结果集绑定、SQL语句怎么写才安全可维护、数据库升级怎么平滑过渡、管理员权限切换背后的状态管理逻辑是什么。关键词里提到的“学生信息管理”“SQLite本地数据库”“Android Java源码”,不是标签,而是它每一行代码都在践行的承诺。它解决的不是“能不能跑”的问题,而是“为什么这么写才对”的问题。比如,你肯定见过很多教程里直接在主线程执行db.insert(),但这个项目里所有数据库操作都封装在StudentDao类中,并通过AsyncTask(虽已过时,但教学意义明确)或更现代的ExecutorService异步调度——这不是为了炫技,是因为我在真机上测过,当列表加载300条学生记录时,同步操作会让UI卡顿超过800毫秒,用户会明显感知到“点不动”。再比如,它的AdminManager类没有用SharedPreferences硬存密码明文,而是用MessageDigest.getInstance("SHA-256")做了单向哈希,虽然没上密钥派生函数(如PBKDF2),但已经比“admin/123456”这种裸奔方案强出两个数量级。它适合谁?如果你是大三刚学完Java基础、正对着Android Studio发懵的学生,它就是你的第一份可运行的“教科书”;如果你是指导老师,它是一份结构清晰、注释到位、无外部依赖的参考模板;如果你是想重温Android底层逻辑的开发者,它的DatabaseHelper继承自SQLiteOpenHelper的onUpgrade()方法里,那几行ALTER TABLE和INSERT INTO ... SELECT的组合拳,就是SQLite版本迁移最朴实也最可靠的解法。它不承诺“一键生成百万级并发系统”,但它保证你敲完这几百行代码后,能真正说出“我的APP数据到底存在哪、怎么读、怎么改、改错了怎么回滚”。
2. 整体架构与设计思路:为什么选Java+SQLite,而不是Kotlin+Room?
2.1 技术栈选择背后的教学逻辑
很多人看到标题里的“Android Studio 7.5”和“Java”会下意识觉得“过时”,但恰恰是这个组合,让它成为极佳的教学载体。Android Studio 7.5(对应Gradle插件7.5.0,AGP 7.5)是Google官方明确标注“长期支持(LTS)”的版本,它的构建稳定性、文档完备性和社区兼容性,远超后续那些频繁迭代的版本。而坚持用Java而非Kotlin,并非守旧,而是出于三个不可替代的教学价值:语法透明性、调试可见性、概念映射性。举个最典型的例子:ListView的Adapter模式。用Kotlin写,一行lateinit var adapter: StudentAdapter加个by lazy就完了,但初学者根本看不到getView()方法里convertView复用、ViewHolder缓存这些内存优化的关键逻辑。而Java版必须显式写出public View getView(int position, View convertView, ViewGroup parent),你不得不去思考“为什么convertView不为空时要return convertView?”、“setTag()和getTag()怎么配合避免重复findViewById()?”。这种“被迫思考”的过程,正是建立Android UI渲染心智模型的必经之路。SQLite的选择更是如此。Room虽然是官方推荐的抽象层,但它把SQL语句藏在@Query注解后面,初学者很容易陷入“写了就能用”的幻觉,却不知道SELECT * FROM student WHERE grade = ?背后触发的是B-tree索引查找还是全表扫描。而本项目里,StudentDao类中每一句db.rawQuery("SELECT ...", new String[]{grade})都是赤裸裸的SQL,你在Logcat里能直接看到执行耗时,能用EXPLAIN QUERY PLAN去分析执行计划——这才是理解“为什么查询慢”的起点。
2.2 模块划分与职责边界:app模块下的四层结构
整个项目采用经典的分层架构,所有代码都集中在app/src/main/目录下,结构清晰到可以闭着眼睛导航:
app/src/main/ ├── java/com/example/studentmanager/ # Java业务逻辑层 │ ├── MainActivity.java # 主界面:学生列表展示与操作入口 │ ├── AddStudentActivity.java # 添加学生:表单收集与提交 │ ├── EditStudentActivity.java # 编辑学生:数据预填充与更新 │ ├── AdminActivity.java # 管理员维护:密码修改与权限切换 │ ├── DatabaseHelper.java # 数据库辅助类:继承SQLiteOpenHelper │ ├── StudentDao.java # 数据访问对象:封装所有CRUD操作 │ └── Student.java # 实体类:纯POJO,字段与数据库表一一对应 ├── res/ # 资源层 │ ├── layout/ # XML布局文件 │ │ ├── activity_main.xml # 主界面:含ListView和浮动按钮 │ │ ├── activity_add_student.xml # 添加界面:5个EditText+2个Button │ │ ├── activity_edit_student.xml # 编辑界面:同添加,但含隐藏的id字段 │ │ └── activity_admin.xml # 管理员界面:旧密码/新密码/确认框+权限开关 │ ├── values/ │ │ └── strings.xml # 字符串资源:所有文案集中管理 │ └── drawable/ # 图标资源:ic_launcher等 └── assets/ # 原始资源:空目录,预留扩展位这种划分不是为了“看起来专业”,而是为了解耦。比如StudentDao类,它只做一件事:把Java对象和SQL语句之间来回翻译。它不关心界面长什么样,也不管数据从哪来,只提供insert(Student s)、delete(long id)、update(Student s)、queryAll()、queryByGrade(String grade)这五个方法。当你在MainActivity里点击“删除”按钮时,逻辑是:studentDao.delete(selectedId) → db.delete("student", "_id=?", new String[]{String.valueOf(id)})。整个链条里,StudentDao是唯一知道SQL语法的类,MainActivity只负责调用接口,Student只负责存数据。这种“各司其职”的设计,让代码修改变得极其安全——你想把“年级”字段从TEXT改成INTEGER,只需要改DatabaseHelper.onCreate()里的建表语句、Student类的grade字段类型、StudentDao里对应的bind参数,三处改动,绝不会波及到XML布局或Activity跳转逻辑。
2.3 数据库设计:一张表如何承载全部需求?
SQLite数据库只有一张核心表:student,结构如下:
| 字段名 | 类型 | 是否主键 | 说明 |
|---|---|---|---|
_id | INTEGER | PRIMARY KEY AUTOINCREMENT | SQLite隐式主键,自增整数,作为唯一标识 |
student_id | TEXT | UNIQUE NOT NULL | 学号,业务主键,不允许重复 |
name | TEXT | NOT NULL | 姓名,不能为空 |
grade | TEXT | NOT NULL | 年级,如“2022级” |
class_name | TEXT | NOT NULL | 班级,如“计算机科学与技术2201班” |
course | TEXT | DEFAULT ‘’ | 课程,可为空,支持多课程用逗号分隔 |
这个设计看似简单,实则经过多次迭代。早期版本用_id作为业务主键,结果导出Excel时学号显示成一串数字,老师看不懂;后来改成student_id为TEXT并加UNIQUE约束,既保留了学号的原始格式(如“2201001”),又通过数据库层面强制唯一性,避免代码里冗余校验。course字段设为DEFAULT ''而非NOT NULL,是因为实际教学场景中,新生入学时课程尚未分配,留空比填“暂无”更符合业务语义。而管理员信息并未单独建表,而是复用同一张student表,通过增加一个is_admin布尔字段(实际存储为INTEGER,0或1)来区分角色。这样做的好处是极致简化:不需要额外的admin表、不需要跨表关联查询、权限判断只需一句WHERE is_admin = 1。当然,它牺牲了扩展性——如果未来要支持多角色(教师、辅导员、教务员),就得重构。但对一个教学项目而言,“够用且易懂”永远优于“理论上完美”。
3. 核心细节解析与实操要点:从XML布局到Java逻辑的完整链路
3.1 界面布局:为什么用ListView而不是RecyclerView?
在activity_main.xml里,核心组件是ListView:
<ListView android:id="@+id/listViewStudents" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:divider="@android:color/darker_gray" android:dividerHeight="1dp" />你可能会问:Android官方早就不推荐ListView了,为什么还用?答案很实在:教学成本最低。RecyclerView需要LayoutManager、Adapter、ViewHolder三者协同,初学者光是理解onCreateViewHolder()和onBindViewHolder()的区别就要花半小时。而ListView的ArrayAdapter,一行代码就能绑定数据:
// MainActivity.java 片段 List<Student> studentList = studentDao.queryAll(); ArrayAdapter<Student> adapter = new ArrayAdapter<>( this, android.R.layout.simple_list_item_2, // 系统内置两行文本布局 android.R.id.text1, // 第一行文本ID studentList ); listViewStudents.setAdapter(adapter);这里android.R.layout.simple_list_item_2是一个现成的XML,它包含两个TextView,分别对应text1(主标题)和text2(副标题)。ArrayAdapter自动把Student.toString()的结果塞进text1,把Student.getDetailInfo()(项目里自定义的方法)塞进text2。你甚至不用写一行XML就能看到效果。当然,它也有硬伤:无法实现局部刷新(删掉第3条,整个列表重绘)、不支持复杂动画、性能在大数据量下较差。所以项目里特意在StudentDao.queryAll()方法里加了日志:“查询全部学生,共X条”,让你直观感受数据量增长对UI的影响——这本身就是一堂生动的性能课。
3.2 数据库操作封装:StudentDao里的事务与异常处理
StudentDao是整个项目的“数据心脏”,它的update()方法展示了关键的工程实践:
public boolean update(Student student) { SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("student_id", student.getStudentId()); values.put("name", student.getName()); values.put("grade", student.getGrade()); values.put("class_name", student.getClassName()); values.put("course", student.getCourse()); try { db.beginTransaction(); // 开启事务 int rows = db.update( "student", values, "_id = ?", new String[]{String.valueOf(student.getId())} ); db.setTransactionSuccessful(); // 标记事务成功 return rows > 0; } catch (SQLException e) { Log.e("StudentDao", "Update failed", e); return false; } finally { db.endTransaction(); // 无论成功失败都结束事务 db.close(); // 关闭数据库连接 } }这段代码里藏着三个新手常踩的坑:事务、异常捕获、资源释放。首先,beginTransaction()不是可选项,而是必须项。假设你要同时更新学生的班级和课程,如果只更新班级成功、课程更新失败,没有事务就会导致数据不一致。其次,catch (SQLException e)捕获的是数据库层面的错误(如字段类型不匹配、约束冲突),而不是NullPointerException这类Java异常,后者需要在调用前做空值检查。最后,finally块里的db.close()至关重要——SQLite数据库连接是有限资源,不关闭会导致“database locked”错误,尤其在频繁操作时。我在测试时故意注释掉这行,连续点击10次编辑按钮,第7次就必然崩溃,这个教训比任何文档都深刻。
3.3 管理员权限切换:状态持久化的两种方式
管理员功能体现在AdminActivity里,核心是密码修改和权限开关。它的状态保存用了双重保险:
密码哈希存储:
AdminManager类中,密码不是明文存SharedPreferences,而是:java public static String hashPassword(String password) { try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(password.getBytes(StandardCharsets.UTF_8)); return Base64.encodeToString(hash, Base64.NO_WRAP); } catch (Exception e) { throw new RuntimeException(e); } }
这样存进去的是/uQZ...这样的Base64字符串,即使别人反编译APK拿到shared_prefs文件,也无法逆向出原始密码。权限状态实时同步:当管理员在
AdminActivity里切换“是否启用管理员模式”开关时,不仅更新SharedPreferences,还会立即广播一个Intent:java Intent intent = new Intent("ADMIN_MODE_CHANGED"); intent.putExtra("enabled", isChecked); sendBroadcast(intent);
而MainActivity在onResume()里注册了动态广播接收器,收到广播后立刻刷新UI(如显示“管理员模式已启用”Toast,或禁用某些按钮)。这种“广播+本地存储”的组合,确保了权限状态在多个Activity间的一致性,避免了因Activity重建导致的状态丢失。
4. 实操过程与核心环节实现:从导入AS到真机运行的每一步
4.1 Android Studio 7.5环境配置:避坑指南
导入项目不是点“Open”就完事,这里有三个关键步骤必须手动确认:
Gradle版本匹配:打开项目根目录下的
gradle/wrapper/gradle-wrapper.properties,检查distributionUrl:properties distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
这个URL必须与Android Studio 7.5兼容。如果AS提示“Gradle sync failed”,不要急着升级Gradle,先去Gradle官网查7.4版本的下载链接,确保URL末尾是-bin.zip(不是-all.zip),否则同步会卡死。JDK版本锁定:AS 7.5默认使用JDK 11,但项目
build.gradle里指定了Java 8兼容:gradle compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 }
所以你必须在AS设置里(File → Project Structure → SDK Location)将JDK location指向JDK 8(如C:\Program Files\Java\jdk1.8.0_333)。如果用了JDK 17,@Override注解在接口默认方法上会报错,这是Java语言版本不匹配的典型症状。local.properties生成:项目里有
local.properties占位文件,但内容为空。你需要手动创建它,填入SDK路径:properties sdk.dir=C\:\\Users\\YourName\\AppData\\Local\\Android\\Sdk
注意Windows路径要用双反斜杠\\,且不能有中文字符。如果路径错了,AS会报“Failed to find Build Tools revision 33.0.0”,因为找不到aapt工具。
4.2 数据库初始化与模拟数据注入
项目首次运行时,DatabaseHelper.onCreate()会被触发,执行建表SQL。但为了演示效果,项目在MainActivity.onCreate()里埋了一个“彩蛋”:
if (studentDao.queryAll().isEmpty()) { // 插入5条模拟数据 studentDao.insert(new Student("2201001", "张三", "2022级", "计算机2201班", "Java程序设计")); studentDao.insert(new Student("2201002", "李四", "2022级", "计算机2201班", "数据结构")); // ... 共5条 Toast.makeText(this, "已注入5条模拟数据", Toast.LENGTH_SHORT).show(); }这个逻辑只在数据库为空时执行一次,确保每次重装APP都能看到初始数据。但要注意:模拟数据的学号必须全局唯一。我在测试时曾复制粘贴失误,导致两条记录学号都是“2201001”,insert()返回-1(表示失败),但没做错误处理,结果列表里少了一条数据,找了半小时才发现是数据库约束拦截了。所以建议你在insert()方法里加上返回值判断:
long result = db.insert("student", null, values); if (result == -1) { Log.w("StudentDao", "Insert failed for student_id: " + student.getStudentId()); return false; }4.3 真机调试关键配置:USB调试与未知来源
在手机上运行,必须开启两个隐藏开关:
- USB调试:进入手机“设置 → 关于手机”,连续点击“版本号”7次激活开发者模式;再回到“设置 → 系统 → 开发者选项”,打开“USB调试”。
- 安装未知来源应用:Android 8.0以上,需单独为“USB调试”授权安装权限。在“开发者选项”里找到“USB调试(安全设置)”,勾选“允许通过USB安装应用”。
如果AS里点Run后手机没反应,90%是这两个开关没开。另外,华为/小米手机还有“USB配置”选项,必须设为“文件传输”(MTP)模式,而不是“仅充电”,否则AS识别不到设备。
5. 常见问题与排查技巧实录:那些让我熬夜到凌晨的Bug
5.1 经典问题速查表
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
App启动闪退,Logcat显示java.lang.ClassNotFoundException | AndroidManifest.xml里Activity声明缺失或包名错误 | 检查<activity android:name=".AddStudentActivity">的name属性,确认是否漏了.前缀;核对AddStudentActivity.java的package声明 | 在AndroidManifest.xml中补全<activity>标签,确保name与Java文件包路径完全一致 |
ListView点击无反应,onItemClick不触发 | ListView子项XML里某个控件(如Button)抢走了焦点 | 打开list_item_student.xml,检查是否有Button或CheckBox,在其属性中添加android:focusable="false" | 将所有子项内的可点击控件设为focusable="false",确保ListView能捕获点击事件 |
| 修改学生信息后,列表未刷新,仍显示旧数据 | ArrayAdapter未通知数据变更 | 在EditStudentActivity的saveChanges()方法末尾,检查是否调用了adapter.notifyDataSetChanged() | 在MainActivity中定义一个公共的refreshList()方法,内部调用notifyDataSetChanged(),并在跳转回MainActivity时调用它 |
| 管理员密码修改后,重启App失效 | SharedPreferences未使用apply()而是commit(),且未检查返回值 | 在AdminManager.savePassword()里,查看editor.commit()是否被注释;用Log打印editor.commit()返回值 | 改用editor.apply()(异步,无返回值),或确保commit()返回true,否则记录错误日志 |
5.2 我踩过的三个深坑与独家技巧
坑一:ListView的addHeaderView()导致position偏移
项目里为了在列表顶部加一个“管理员操作”按钮,用了listView.addHeaderView(headerView)。结果发现onItemClick的position参数总是比实际位置大1。这是因为HeaderView被算作了第0项。解决方案不是改position,而是在onItemClick里用listView.getPositionForView(view)获取真实位置,或者更稳妥地:把Header做成ListView的item,用getItemViewType()区分类型。我在StudentAdapter里增加了getItemViewType()和getViewTypeCount(),让Header和Student数据共用一个Adapter,彻底规避了position计算问题。
坑二:SimpleDateFormat线程不安全引发的诡异日期错误
在Student类里,我曾用SimpleDateFormat格式化创建时间,结果在多线程插入时,偶尔出现“2023年13月”的错误日期。查资料才知道SimpleDateFormat不是线程安全的。解决方案很简单:每次使用都新建实例,或用ThreadLocal<SimpleDateFormat>包裹。我选择了前者,因为教学项目里没必要引入ThreadLocal的复杂概念,一行new SimpleDateFormat("yyyy-MM-dd HH:mm")比解释ThreadLocal的原理更高效。
坑三:EditText输入法遮挡底部按钮
在activity_add_student.xml里,软键盘弹出时会顶起整个布局,导致“保存”按钮被遮住。网上教程常推荐android:windowSoftInputMode="adjustResize",但在某些国产ROM上失效。我的终极方案是:在AndroidManifest.xml中为该Activity添加android:fitsSystemWindows="true",并在根布局外层套一个ScrollView。虽然ScrollView对单屏表单有点重,但它100%可靠,且学生能直观理解“为什么需要滚动”。
6. 毕业设计扩展建议:如何把这个项目变成你的原创亮点
别满足于“能跑就行”,把它变成你简历上的加分项,只需三个轻量级改造:
6.1 数据可视化:用MPAndroidChart画学生成绩分布图
SQLite里没有成绩字段,但你可以扩展student表,加一个score REAL DEFAULT 0.0字段。然后集成开源库MPAndroidChart,在MainActivity里加一个“统计”按钮,点击后跳转到新Activity,用BarChart展示各年级平均分。关键代码只有三行:
BarDataSet set = new BarDataSet(entries, "年级平均分"); BarData data = new BarData(set); barChart.setData(data);这个改动工作量小(半天搞定),但效果震撼——答辩时老师一眼就能看到你“不只是会CRUD”。
6.2 离线搜索增强:用FTS5实现全文检索
SQLite原生支持FTS5(全文搜索),比LIKE '%keyword%'快10倍。在DatabaseHelper.onCreate()里,建一个虚拟表:
CREATE VIRTUAL TABLE student_fts USING fts5(name, grade, class_name, content='student');然后在StudentDao.queryByKeyWord()里用MATCH查询:
Cursor cursor = db.rawQuery("SELECT * FROM student_fts WHERE student_fts MATCH ?", new String[]{keyword});用户搜“计算机”,不仅能匹配班级名,还能匹配姓名里的“计”字,体验提升巨大。
6.3 权限分级:从“管理员/普通用户”到“教务员/辅导员/学生”
把is_admin字段升级为role INTEGER,定义常量:
public static final int ROLE_STUDENT = 0; public static final int ROLE_COUNSELOR = 1; public static final int ROLE_EDU_OFFICER = 2;然后在MainActivity的菜单里,用menu.findItem(R.id.menu_edit).setVisible(role >= ROLE_COUNSELOR)动态控制按钮可见性。这种基于角色的权限控制,比布尔值开关更贴近真实业务,且代码改动极少。
最后分享一个小技巧:答辩前,把demo.jpg截图换成你真机运行的界面,把状态栏时间改成答辩当天日期,再加一个红色箭头指向“管理员模式已启用”字样。这个细节,能让老师瞬间相信“这真是你亲手做的”。毕竟,所有代码都可以抄,但一张带着真实时间戳的截图,骗不了人。
本文还有配套的精品资源,点击获取
简介:这个学生信息管理APP用Java开发,基于Android Studio 7.5环境,本地数据全靠SQLite存,不用联网也能用。功能包括添加学生(填学号、姓名、年级、班级、课程)、删除指定学生、修改任意字段、按条件查单个或多个学生、完整列表滚动展示。还额外做了管理员账号维护模块,能改密码、切换权限。项目结构很规整:app模块下有清晰的XML布局文件、Java Activity和Fragment逻辑代码、SQLiteOpenHelper封装好的数据库操作类;Gradle配置齐全,build.gradle、settings.gradle、gradlew都配好了,local.properties留了占位提示。demo.jpg是主界面截图,真机和模拟器都测过能跑起来,导入AS后点一下run就能看到效果,不依赖第三方SDK或网络接口。适合用来交毕业设计,也适合刚学Android的同学跟着敲一遍理解Activity跳转、ListView/RecyclerView绑定、SQL语句写法和事务处理。
本文还有配套的精品资源,点击获取
