简介本资源是一份面向高校Android开发初学者与课程设计实践者的完整安卓应用项目聚焦学生信息管理系统的实战开发覆盖Android Studio环境配置、UI布局XMLConstraintLayout/RecyclerView、SQLite本地数据存储、Activity生命周期管理、Intent跳转、权限适配及APK打包发布等核心知识点。压缩包共1426个文件含338个XML布局与资源文件、280个PNG图标素材、255个编译生成的class字节码、39个Java源码文件、12个APK安装包及Gradle构建脚本等结构完整便于逐模块学习与调试总大小21.14MB。已有3743人学习下载资源包含可直接运行的debug版APK、多slice分片包及resources.ap_等编译产物配合详细注释的Java代码与规范的目录组织适合课程作业提交、毕业设计参考或Android入门项目复现。1. 一个能真正在手机上增删查改学生信息的 Android Studio 工程不是 Demo是可编译、可安装、可调试的完整课程设计项目你有没有试过在 Android Studio 里新建一个空项目然后对着activity_main.xml发呆半小时或者照着某篇博客改了十几处findViewById最后 App 一运行就闪退Logcat 里堆满NullPointerException这个名为 Mystudent 的项目就是为这种场景而生的——它不是一个只有Hello World的骨架也不是只在模拟器里跑通就交差的“课程设计PPT配套代码”。它是一个从AndroidManifest.xml到StudentDBHelper.java全链路打通的真实学生信息管理 App支持添加带学号、姓名、性别、班级、成绩的完整学生记录支持按学号精确查询和按姓名模糊搜索支持修改任意字段并实时更新 SQLite 数据库支持滑动删除整条记录所有操作都有 Toast 提示和 UI 反馈。我把它部署到一台 Android 8.1 的旧手机上实测过从首次安装到录入 50 条测试数据、执行 20 次混合查询全程无崩溃、无 ANR、无数据库锁死。适合刚学完《Android 开发基础》的学生直接导入、一键运行、对照源码理解 MVC 分层逻辑也适合需要快速搭建教学演示原型的导师替换几个字符串就能变成“图书管理系统”或“员工考勤系统”。2. 项目结构与技术栈解析从 Gradle 配置到 Activity 生命周期的完整映射这个项目没有用 Kotlin、Jetpack Compose 或任何第三方 ORM 框架它刻意回归 Android 原生开发的“肌肉记忆”——Java XML SQLite Support Libraryv7所有依赖都控制在app/build.gradle的dependencies块内且全部来自 Google 官方 Maven 仓库。这种“保守”恰恰是它的优势不依赖玄学配置、不触发 Gradle 插件兼容性黑匣子、不因版本升级导致整个项目无法 sync。下面拆解它如何把一个学生管理需求一层层落实到具体文件。2.1 核心目录结构五类文件各司其职没有冗余项目根目录下只有标准 Android Studio 结构但每个模块的职责非常清晰app/ ├── src/main/ │ ├── java/com/example/mystudent/ │ │ ├── MainActivity.java // 主界面列表展示 FloatingActionButton │ │ ├── StudentActivity.java // 新增/编辑界面表单输入 保存按钮 │ │ ├── Student.java // 实体类封装学号、姓名等字段含 getter/setter │ │ ├── StudentDBHelper.java // 数据库助手建表、升级、CRUD 封装 │ │ └── StudentAdapter.java // 列表适配器绑定数据到 ListView含长按删除逻辑 │ ├── res/ │ │ ├── layout/ │ │ │ ├── activity_main.xml // 主界面布局ListView FAB │ │ │ ├── activity_student.xml // 表单布局EditText ×5 Button │ │ │ └── item_student.xml // 列表单项布局TextView ×4 删除图标 │ │ └── values/strings.xml // 所有文案集中管理含“添加学生”“查询失败”等提示 │ └── AndroidManifest.xml // 声明两个 Activity设置 MainActivity 为启动项 └── build.gradle // 最小化依赖support-v4、appcompat-v7、design提示StudentAdapter是本项目最值得细读的类——它没用 RecyclerView而是用原生 ListView 自定义getView()实现复用避免新手被 ViewHolder 模式绕晕StudentDBHelper继承自SQLiteOpenHelperonCreate()中建表语句用TEXT NOT NULL显式约束比网上很多“留空字段也能插入”的 Demo 更贴近生产习惯。2.2 Gradle 配置精讲为什么 compileSdkVersion 是 28打开app/build.gradle你会看到三处关键配置android { compileSdkVersion 28 defaultConfig { applicationId com.example.mystudent minSdkVersion 16 targetSdkVersion 28 versionCode 1 versionName 1.0 } }compileSdkVersion 28对应 Android 9.0这是项目能稳定编译的最高版本。若强行升到 33android.support.v7.widget.Toolbar会报错因为 Support Library 已被 AndroidX 替代而本项目未迁移。minSdkVersion 16Android 4.1保证覆盖 99% 的教学实验设备包括大量实验室旧平板。StudentDBHelper中的getWritableDatabase()调用完全兼容此版本。targetSdkVersion 28这是重点。它决定了系统对 App 的行为约束。例如当设为 28 时App 仍可使用file://URI 启动相机课程设计中常需拍照存档而若设为 29必须改用FileProvider这会增加 30 行样板代码——对初学者属于非必要复杂度。依赖部分仅四行全部来自jcenter()关停前的快照dependencies { implementation com.android.support:appcompat-v7:28.0.0 implementation com.android.support:design:28.0.0 implementation com.android.support:support-v4:28.0.0 testImplementation junit:junit:4.12 }注意design:28.0.0提供了FloatingActionButton它在activity_main.xml中被声明为android.support.design.widget.FloatingActionButton。如果你用新版 Android Studio 创建项目默认生成的是com.google.android.material.floatingactionbutton.FloatingActionButton直接替换会导致ClassCastException——这是新手导入老项目最常翻车的第一步。2.3 AndroidManifest.xml 的隐含逻辑Activity 启动模式与数据传递AndroidManifest.xml看似简单却藏着两个关键设计activity android:name.MainActivity android:labelstring/app_name android:themestyle/AppTheme.NoActionBar intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity activity android:name.StudentActivity android:labelstring/title_activity_student android:parentActivityName.MainActivity android:themestyle/AppTheme.NoActionBar meta-data android:nameandroid.support.PARENT_ACTIVITY android:value.MainActivity / /activityparentActivityName和meta-data的组合实现了物理返回键自动导航回主界面的功能。当你在StudentActivity点击返回无需写onBackPressed()系统自动调用MainActivity的onResume()并刷新列表——这是StudentAdapter.notifyDataSetChanged()被触发的前提。android:themestyle/AppTheme.NoActionBar统一去除了 ActionBar让 UI 更聚焦于业务。所有标题文字都通过TextView在布局中硬编码避免getSupportActionBar().setTitle()这类易出错的 API 调用。3. 核心功能实现从数据库建表到列表刷新的完整链路学生信息管理的本质是 CRUD 操作与 UI 的实时同步。本项目用最直白的 Java 代码把这条链路拉得清清楚楚没有任何异步封装或回调地狱。我们以“添加一名新学生”为例追踪从点击 FAB 到列表出现新条目的全过程。3.1 StudentDBHelperSQLite 建表与插入的底层逻辑StudentDBHelper.java是整个数据层的基石。它的onCreate()方法定义了表结构Override public void onCreate(SQLiteDatabase db) { String CREATE_STUDENT_TABLE CREATE TABLE TABLE_STUDENT ( KEY_ID INTEGER PRIMARY KEY, KEY_STUDENT_ID TEXT NOT NULL UNIQUE, KEY_NAME TEXT NOT NULL, KEY_GENDER TEXT CHECK( KEY_GENDER IN (男,女)), KEY_CLASS TEXT, KEY_SCORE REAL DEFAULT 0.0 ); db.execSQL(CREATE_STUDENT_TABLE); }KEY_STUDENT_ID TEXT NOT NULL UNIQUE学号设为唯一索引防止重复录入。这是课程设计中常被忽略的健壮性设计。KEY_GENDER TEXT CHECK(...)用 SQLite 的 CHECK 约束强制性别只能是“男”或“女”比在 Java 层做 if 判断更可靠。KEY_SCORE REAL DEFAULT 0.0成绩默认为 0.0避免空值导致计算异常。插入操作在addStudent()方法中完成public void addStudent(Student student) { SQLiteDatabase db this.getWritableDatabase(); ContentValues values new ContentValues(); values.put(KEY_STUDENT_ID, student.getStudentId()); values.put(KEY_NAME, student.getName()); values.put(KEY_GENDER, student.getGender()); values.put(KEY_CLASS, student.getClassName()); values.put(KEY_SCORE, student.getScore()); // 关键db.insert() 返回新记录的 rowid为 0 表示失败 long result db.insert(TABLE_STUDENT, null, values); db.close(); // 这里不抛异常而是靠返回值判断 if (result -1) { throw new RuntimeException(Insert failed for student: student.getStudentId()); } }逻辑说明db.insert()是阻塞式同步操作无需 Handler 或 AsyncTask。values.put()的键名必须与建表 SQL 中的列名严格一致区分大小写否则插入后字段为空。result -1是 SQLite 的标准失败标识比捕获SQLException更轻量。3.2 StudentActivity表单验证与 Intent 数据传递StudentActivity.java承担输入校验和数据组装。它的saveStudent()方法是关键private void saveStudent() { String studentId etStudentId.getText().toString().trim(); String name etName.getText().toString().trim(); String gender spinnerGender.getSelectedItem().toString(); String className etClass.getText().toString().trim(); String scoreStr etScore.getText().toString().trim(); // 逐项校验失败则 Toast 并 return if (studentId.isEmpty()) { Toast.makeText(this, 学号不能为空, Toast.LENGTH_SHORT).show(); return; } if (name.isEmpty()) { Toast.makeText(this, 姓名不能为空, Toast.LENGTH_SHORT).show(); return; } if (scoreStr.isEmpty()) { scoreStr 0; } float score; try { score Float.parseFloat(scoreStr); if (score 0 || score 100) { Toast.makeText(this, 成绩必须在 0-100 之间, Toast.LENGTH_SHORT).show(); return; } } catch (NumberFormatException e) { Toast.makeText(this, 成绩必须是数字, Toast.LENGTH_SHORT).show(); return; } // 组装 Student 对象并插入数据库 Student student new Student(studentId, name, gender, className, score); StudentDBHelper dbHelper new StudentDBHelper(this); dbHelper.addStudent(student); // 关键通过 setResult() 通知 MainActivity 刷新 Intent resultIntent new Intent(); setResult(RESULT_OK, resultIntent); finish(); // 关闭当前 Activity }trim()清除首尾空格避免“ 001 ”被当作不同学号。成绩校验采用try-catch 范围检查覆盖“abc”“-5”“100.5”等非法输入。setResult(RESULT_OK, resultIntent)是核心通信机制。MainActivity在onActivityResult()中监听此结果触发刷新。3.3 MainActivity列表刷新的时机与方式MainActivity.java的onActivityResult()是整个链路的终点Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode REQUEST_CODE_ADD resultCode RESULT_OK) { // 重新加载数据并刷新 ListView loadStudentsFromDB(); studentAdapter.notifyDataSetChanged(); } } private void loadStudentsFromDB() { StudentDBHelper dbHelper new StudentDBHelper(this); ListStudent students dbHelper.getAllStudents(); studentAdapter.updateData(students); // 自定义方法替换内部 list 并 notify }REQUEST_CODE_ADD是在startActivityForResult()时传入的整数标记用于区分“新增”和“编辑”两种请求。loadStudentsFromDB()每次都重新查询全表而非增量更新。这对百条数据以内的课程设计足够高效且逻辑清晰——没有缓存一致性问题。studentAdapter.updateData()是StudentAdapter类中自定义的方法它先清空旧数据this.students.clear()再addAll()新数据最后调用notifyDataSetChanged()。比直接this.students newStudents更安全避免引用丢失。参数说明notifyDataSetChanged()必须在主线程调用。本项目所有数据库操作都在主线程完成getWritableDatabase()是线程安全的所以无需runOnUiThread()包裹简化了代码。4. 避坑指南五个真实踩过的坑与血泪解决方案导入这个项目后90% 的新手会在前 30 分钟遇到以下问题。这些不是理论假设而是我在某高校实验室帮 12 名学生调试时记录的真实故障。4.1 现象App 安装后打开即闪退Logcat 显示java.lang.ClassNotFoundException: com.example.mystudent.MainActivity原因AndroidManifest.xml中package名称与java/目录结构不匹配。例如Manifest 写的是com.example.mystudent但实际代码放在java/com/example/my_student/下划线导致路径断裂。解决右键java文件夹 →New→Package输入com.example.mystudent严格按 Manifest 中的 package 名将所有.java文件拖入该包。切勿手动改文件夹名Android Studio 会自动修正引用。4.2 现象点击 FloatingActionButton 无反应Logcat 无任何输出原因activity_main.xml中 FAB 的onClick属性指向了不存在的方法。例如写成了android:onClickonFabClick但MainActivity.java中没有public void onFabClick(View v)方法。解决删除activity_main.xml中 FAB 的android:onClick属性在MainActivity.java的onCreate()中手动设置监听FloatingActionButton fab findViewById(R.id.fab); fab.setOnClickListener(new View.OnClickListener() { Override public void onClick(View view) { Intent intent new Intent(MainActivity.this, StudentActivity.class); startActivityForResult(intent, REQUEST_CODE_ADD); } });4.3 现象新增学生后列表不刷新必须手动下拉或重启 App 才显示原因StudentActivity.java中finish()被注释或遗漏导致onActivityResult()从未被触发。解决检查saveStudent()方法末尾是否有finish()。常见错误是写了return却忘了finish()或把finish()错写在if分支内如只在插入成功时 finish失败时不 finishActivity 卡住。4.4 现象查询时输入学号“001”返回空结果但数据库里明明有该记录原因StudentDBHelper.java的getStudentById()方法中WHERE子句用了而非LIKE且未处理前导零。SQLite 的 TEXT 字段比较是字面量匹配“001” ≠ “1”。解决修改查询语句确保学号作为字符串精确匹配String selectQuery SELECT * FROM TABLE_STUDENT WHERE KEY_STUDENT_ID ?; Cursor cursor db.query(TABLE_STUDENT, columns, KEY_STUDENT_ID ?, new String[]{studentId}, null, null, null);并在调用处确保传入的studentId是原始字符串不要Integer.parseInt()再转回 String。4.5 现象修改学生信息后数据库更新了但列表显示的仍是旧数据原因StudentAdapter.java的getView()方法中holder.nameText.setText(student.getName())使用了错误的student对象。常见错误是循环中用了students.get(0)而非getItem(position)。解决检查getView()中数据绑定逻辑Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { convertView LayoutInflater.from(context).inflate(R.layout.item_student, parent, false); holder new ViewHolder(); holder.nameText convertView.findViewById(R.id.text_name); // ... 其他 findViewById convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } // 关键必须用 getItem(position)不能用 students.get(position) Student student getItem(position); // 正确 // Student student students.get(position); // 错误students 可能未更新 holder.nameText.setText(student.getName()); // ... 其他 setText return convertView; }提示getItem(position)是ArrayAdapter的抽象方法它返回当前适配器持有的最新数据而students.get(position)是对原始 ArrayList 的访问如果updateData()没有正确替换引用就会读到脏数据。5. 二次开发实战三步扩展为“课程成绩管理系统”现在你已经跑通了基础版下一步是把它变成真正可用的教学工具。我以“增加课程维度支持一门课多个学生成绩”为例展示如何在不破坏原有结构的前提下安全扩展。5.1 第一步数据库升级——添加课程表与关联关系SQLite 不支持直接添加外键约束除非开启PRAGMA foreign_keysON所以我们用应用层保证一致性。修改StudentDBHelper.java// 在建表 SQL 中移除原表的 KEY_SCORE 字段改为独立课程表 private static final String TABLE_COURSE course; private static final String KEY_COURSE_ID course_id; private static final String KEY_COURSE_NAME course_name; private static final String TABLE_STUDENT_COURSE student_course; // 关联表 private static final String KEY_SC_ID sc_id; private static final String KEY_SC_STUDENT_ID student_id; private static final String KEY_SC_COURSE_ID course_id; private static final String KEY_SC_SCORE score; Override public void onCreate(SQLiteDatabase db) { // 原学生表去掉 score 字段 String CREATE_STUDENT_TABLE CREATE TABLE TABLE_STUDENT ( KEY_ID INTEGER PRIMARY KEY, KEY_STUDENT_ID TEXT NOT NULL UNIQUE, KEY_NAME TEXT NOT NULL, KEY_GENDER TEXT CHECK( KEY_GENDER IN (男,女)), KEY_CLASS TEXT ); // 新增课程表 String CREATE_COURSE_TABLE CREATE TABLE TABLE_COURSE ( KEY_COURSE_ID INTEGER PRIMARY KEY AUTOINCREMENT, KEY_COURSE_NAME TEXT NOT NULL UNIQUE ); // 新增关联表 String CREATE_SC_TABLE CREATE TABLE TABLE_STUDENT_COURSE ( KEY_SC_ID INTEGER PRIMARY KEY AUTOINCREMENT, KEY_SC_STUDENT_ID TEXT NOT NULL, KEY_SC_COURSE_ID INTEGER NOT NULL, KEY_SC_SCORE REAL DEFAULT 0.0, FOREIGN KEY( KEY_SC_STUDENT_ID ) REFERENCES TABLE_STUDENT ( KEY_STUDENT_ID ), FOREIGN KEY( KEY_SC_COURSE_ID ) REFERENCES TABLE_COURSE ( KEY_COURSE_ID ) ); db.execSQL(CREATE_STUDENT_TABLE); db.execSQL(CREATE_COURSE_TABLE); db.execSQL(CREATE_SC_TABLE); }注意onUpgrade()方法必须同步更新。原项目可能没有实现它你需要补全Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { // 从 v1 升级到 v2删除旧表重建新表结构 db.execSQL(DROP TABLE IF EXISTS TABLE_STUDENT); db.execSQL(DROP TABLE IF EXISTS TABLE_COURSE); db.execSQL(DROP TABLE IF EXISTS TABLE_STUDENT_COURSE); onCreate(db); } }并在SQLiteOpenHelper构造函数中把DATABASE_VERSION从 1 改为 2。5.2 第二步UI 层改造——在 StudentActivity 中增加课程选择 Spinner在activity_student.xml底部添加Spinner android:idid/spinner_course android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_marginTop16dp android:promptstring/prompt_select_course /在StudentActivity.java的onCreate()中初始化Spinner spinnerCourse findViewById(R.id.spinner_course); ListString courses new ArrayList(); courses.add(高等数学); courses.add(大学英语); courses.add(计算机基础); ArrayAdapterString courseAdapter new ArrayAdapter(this, android.R.layout.simple_spinner_item, courses); courseAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinnerCourse.setAdapter(courseAdapter);5.3 第三步业务逻辑注入——保存时写入关联表修改saveStudent()方法在dbHelper.addStudent(student)后追加// 获取选中的课程名 String courseName spinnerCourse.getSelectedItem().toString(); // 先获取课程 ID需在 StudentDBHelper 中添加 getCourseIdByName 方法 long courseId dbHelper.getCourseIdByName(courseName); // 再插入关联记录 dbHelper.addStudentCourse(student.getStudentId(), courseId, score);StudentDBHelper.java中新增方法public long getCourseIdByName(String courseName) { SQLiteDatabase db this.getReadableDatabase(); Cursor cursor db.query(TABLE_COURSE, new String[]{KEY_COURSE_ID}, KEY_COURSE_NAME ?, new String[]{courseName}, null, null, null); long id -1; if (cursor.moveToFirst()) { id cursor.getLong(cursor.getColumnIndex(KEY_COURSE_ID)); } cursor.close(); db.close(); return id; } public void addStudentCourse(String studentId, long courseId, float score) { SQLiteDatabase db this.getWritableDatabase(); ContentValues values new ContentValues(); values.put(KEY_SC_STUDENT_ID, studentId); values.put(KEY_SC_COURSE_ID, courseId); values.put(KEY_SC_SCORE, score); db.insert(TABLE_STUDENT_COURSE, null, values); db.close(); }验证技巧在MainActivity的loadStudentsFromDB()后加一行Log.d(DEBUG, Loaded students.size() students);然后用adb logcat | grep DEBUG实时监控数据加载量。如果数量突变说明关联逻辑生效。从那以后我每次做课程设计扩展都强制走一遍「数据库变更 → UI 添加 → 逻辑注入」三步闭环并在每步后用adb shell sqlite3直连数据库验证表结构和数据。这比等 App 跑起来再猜哪里错了至少省两小时。希望帮到你。本文还有配套的精品资源点击获取