简介:本资源是一份面向高校Android开发初学者与课程设计学生的完整安卓应用实战项目,聚焦学生信息管理系统的全流程实现,帮助学习者系统掌握Android Studio开发核心技能。压缩包共1426个文件,涵盖338个XML布局与资源文件、280个PNG图标素材、255个编译后class字节码、39个Java源码文件、12个APK安装包及多个gradle构建配置文件,完整呈现从UI设计、SQLite数据存储、Activity跳转、RecyclerView列表展示到权限适配与调试发布的全链路工程结构,总大小21.14MB。已有3738人学习下载,项目代码规范、模块清晰、注释充分,附带可直接运行的debug版APK与资源打包产物(如resources-debug.ap_、slice_x.apk等),便于快速部署验证与逆向理解构建流程,是Android入门实践与课程作业交付的理想参考范例。
1. 学生信息管理App:一个能真正在Android Studio里跑通、改得动、交得上的课程设计闭环
你是不是也经历过——老师布置“用Android Studio做一个学生信息管理系统”,结果网上搜到的代码要么缺数据库表结构,要么Activity跳转逻辑断在半路,要么连build.gradle里该配哪个版本的com.android.tools.build:gradle都模棱两可?这个Mystudent项目不是Demo截图,也不是半成品框架,而是一个从新建项目开始、完整走完“添加学生→查询列表→修改信息→删除记录→本地SQLite持久化”全链路的可运行工程。它不炫技,没用Jetpack Compose,纯XML+Java(或Kotlin)+原生SQLite,所有Activity之间用Intent传参、所有数据操作封装在DAO层、所有UI控件命名规范可读。适合大二大三刚学完《移动应用开发》课程的学生直接导入Android Studio 4.2+(兼容Gradle 7.0),改个包名就能当课设交作业;也适合想补Android基础的同学,把它当“教科书式样板”——看清楚一个真实App里,onCreate()里该初始化什么,CursorAdapter怎么绑定ListView,ContentValues怎么填字段,甚至adb logcat | grep Mystudent时该盯哪几行日志。这不是玩具项目,是踩过坑、修过bug、真在真机上点过“添加”按钮弹出Toast的落地工程。
2. 工程结构与技术栈解析:从project根目录到app/src/main的每一层都藏着关键线索
2.1 项目层级与模块划分:为什么这个结构能避免“导入就报红”
打开解压后的Mystudent文件夹,你会看到典型的Android Studio标准结构:
Mystudent/ ├── app/ # 核心模块,所有业务代码在此 │ ├── src/ │ │ ├── main/ │ │ │ ├── java/com/example/mystudent/ # Java源码(若为Kotlin版则为kotlin/) │ │ │ │ ├── MainActivity.java # 主界面:学生列表展示 │ │ │ │ ├── AddStudentActivity.java # 添加界面:表单输入 │ │ │ │ ├── EditStudentActivity.java # 编辑界面:回显+更新 │ │ │ │ ├── Student.java # 实体类:id, name, age, major等字段 │ │ │ │ ├── StudentDBHelper.java # SQLiteOpenHelper子类:建表、升级 │ │ │ │ └── StudentDAO.java # 数据访问对象:增删改查封装 │ │ │ ├── res/ # 资源文件 │ │ │ │ ├── layout/ # activity_main.xml, activity_add.xml等 │ │ │ │ ├── values/ # strings.xml定义所有文本 │ │ │ │ └── ... │ │ │ └── AndroidManifest.xml # 四大组件注册、权限声明 │ │ └── build.gradle # 模块级构建脚本(重点看dependencies) │ ├── build.gradle # 项目级构建脚本(定义gradle插件版本) ├── gradle/ └── settings.gradle提示:很多同学导入失败,根本原因不是代码问题,而是
app/build.gradle里compileSdkVersion和targetSdkVersion写成了33,但本地Android Studio只装了SDK 31。务必先打开File → Project Structure → SDK Location,确认已安装的SDK Platform版本,再反向匹配build.gradle中的数值。常见做法是统一设为31(对应Android 12L),既兼容旧设备,又避开33引入的严格后台限制导致的崩溃。
2.2 核心依赖与版本锁定:Gradle配置里的生存指南
app/build.gradle中关键依赖如下(以Java版为例):
android { compileSdkVersion 31 // 必须与本地SDK安装版本一致 buildToolsVersion "30.0.3" // 若报错,可删掉此行,AS会自动选最新可用版 defaultConfig { applicationId "com.example.mystudent" minSdkVersion 16 // 支持Android 4.1+,覆盖99%以上设备 targetSdkVersion 31 // 与compileSdkVersion保持一致 versionCode 1 versionName "1.0" } } dependencies { implementation 'androidx.appcompat:appcompat:1.4.2' // 兼容库,必须 implementation 'com.google.android.material:material:1.6.1' // Material Design组件 implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // 布局利器 // 注意:没有引入Room或LiveData,纯原生SQLite,降低学习门槛 }这里没有用androidx.room:room-runtime,说明它绕过了Jetpack架构组件的抽象层,直接操作SQLiteDatabase对象。好处是逻辑透明——你看得见每一条INSERT INTO student (...) VALUES (...)是怎么拼出来的;坏处是需要手动处理游标关闭、事务边界。这正是课程设计要考察的基本功:不是调API,而是理解数据如何落盘。
2.3 Manifest注册与Activity跳转链:四步完成一次学生添加
AndroidManifest.xml中三个Activity的注册是功能闭环的前提:
<activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".AddStudentActivity" android:exported="false" /> <activity android:name=".EditStudentActivity" android:exported="false" />注意:
android:exported="false"表示这些Activity不能被其他App启动,符合安全规范。而跳转逻辑全部在Java代码中完成,例如MainActivity中点击“添加”按钮:
// MainActivity.java addBtn.setOnClickListener(v -> { Intent intent = new Intent(MainActivity.this, AddStudentActivity.class); startActivity(intent); // 启动添加界面 });这种显式Intent调用,比隐式Intent更可控,也更适合初学者理解组件通信本质。
2.4 资源文件组织逻辑:strings.xml和layout如何支撑多语言与适配
res/values/strings.xml不只是存文字,更是解耦UI与逻辑的第一道防线:
<resources> <string name="app_name">MyStudent</string> <string name="title_activity_main">学生信息列表</string> <string name="btn_add">添加学生</string> <string name="hint_name">请输入姓名</string> <string name="hint_age">请输入年龄</string> <string name="hint_major">请输入专业</string> <string name="toast_add_success">添加成功!</string> <string name="dialog_delete_title">确认删除</string> <string name="dialog_delete_message">确定要删除该学生信息吗?</string> </resources>所有界面文字都通过@string/btn_add引用,而非硬编码。这意味着:
- 若需中英文切换,只需新增
res/values-en/strings.xml; - 若老师要求改成“学员信息管理”,改一处
strings.xml即可全局生效; Toast.makeText(..., R.string.toast_add_success, ...)比写死字符串更易维护。
同理,activity_main.xml使用ConstraintLayout而非LinearLayout,是因为它天然支持不同屏幕尺寸的约束关系——比如“删除按钮始终在列表项右侧”,这种声明式布局比Java代码动态计算坐标更稳定,也更符合现代Android开发习惯。
3. SQLite数据库实现详解:StudentDBHelper与StudentDAO的协作机制
3.1 StudentDBHelper:建表语句、版本升级与onUpgrade的血泪经验
StudentDBHelper.java继承自SQLiteOpenHelper,核心在于onCreate()和onUpgrade()两个方法:
public class StudentDBHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "student.db"; private static final int DATABASE_VERSION = 2; // 初始为1,改表结构时必须+1 public StudentDBHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } @Override public void onCreate(SQLiteDatabase db) { // 创建student表:id主键自增,name非空,age默认0,major可为空 String CREATE_TABLE = "CREATE TABLE student (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT NOT NULL," + "age INTEGER DEFAULT 0," + "major TEXT" + ")"; db.execSQL(CREATE_TABLE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 版本升级策略:简单场景直接删表重建(仅限开发阶段!) if (oldVersion < 2) { db.execSQL("DROP TABLE IF EXISTS student"); onCreate(db); // 重建新表结构 } } }注意:
onUpgrade()里DROP TABLE IF EXISTS是教学项目的权宜之计。真实项目中应使用ALTER TABLE ADD COLUMN等增量语句,否则用户数据全丢。但课程设计允许——因为重点是让学生理解“版本号变化触发升级流程”,而不是数据迁移工程。
3.2 StudentDAO:封装CRUD操作,让业务代码干净如初
StudentDAO.java是数据访问层,屏蔽了SQL细节。以“添加学生”为例:
public long addStudent(Student student) { SQLiteDatabase db = dbHelper.getWritableDatabase(); ContentValues values = new ContentValues(); values.put("name", student.getName()); values.put("age", student.getAge()); values.put("major", student.getMajor()); // insert()返回新记录的rowid,-1表示失败 long result = db.insert("student", null, values); db.close(); // 关键!必须关闭,否则下次getWritableDatabase()可能阻塞 return result; }ContentValues是Android提供的键值对容器,比手动拼接SQL字符串安全(防SQL注入)、简洁(不用写'引号和?占位符)。而db.close()这行常被忽略——若不关,多次调用getWritableDatabase()会导致IllegalStateException: attempt to re-open an already-closed object。我一般会在每个DAO方法末尾加Log.d("DAO", "insert completed, rowid=" + result);,方便调试时一眼看出是否写入成功。
3.3 MainActivity中ListView与SimpleCursorAdapter的绑定:从数据库到UI的最后100米
MainActivity不直接new Student对象列表,而是用Cursor加载数据,并通过SimpleCursorAdapter绑定到ListView:
private void loadStudentList() { StudentDAO dao = new StudentDAO(this); Cursor cursor = dao.getAllStudents(); // 返回查询student表的Cursor // 定义ListView每一项显示哪些字段(from)和对应控件ID(to) String[] from = {"name", "age", "major"}; int[] to = {R.id.tv_name, R.id.tv_age, R.id.tv_major}; SimpleCursorAdapter adapter = new SimpleCursorAdapter( this, R.layout.list_item_student, // 每一项的布局文件 cursor, from, to, 0 ); listView.setAdapter(adapter); }list_item_student.xml长这样:
<!-- res/layout/list_item_student.xml --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:padding="8dp"> <TextView android:id="@+id/tv_name" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:textSize="16sp" /> <TextView android:id="@+id/tv_age" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:textSize="14sp" /> <TextView android:id="@+id/tv_major" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="2" android:textSize="14sp" /> </LinearLayout>这种CursorAdapter方案比ArrayAdapter<Student>更省内存——因为Cursor是数据库游标,不把全部数据加载进内存,适合列表项较多时。但代价是必须手动管理Cursor生命周期:adapter销毁时需调用cursor.close(),否则内存泄漏。课程设计中常被省略,但你在onDestroy()里加上这一句会显得很专业:
@Override protected void onDestroy() { super.onDestroy(); if (cursor != null && !cursor.isClosed()) { cursor.close(); } }3.4 查询、修改、删除的完整链路:一个学生ID如何贯穿三个Activity
编辑功能最能体现数据流设计:
MainActivity中长按列表项,获取该行Cursor的_id(注意:SQLite自动创建的主键列名是_id,不是id!这是CursorAdapter强制要求);- 通过
Intent将_id传给EditStudentActivity:Intent intent = new Intent(MainActivity.this, EditStudentActivity.class); intent.putExtra("student_id", cursor.getInt(cursor.getColumnIndexOrThrow("_id"))); startActivity(intent); EditStudentActivity中用该ID查出原始数据,填充到EditText;- 点击“保存”时,调用
StudentDAO.updateStudent(),SQL语句形如:UPDATE student SET name=?, age=?, major=? WHERE _id=?
这个_id就是连接UI与数据库的唯一纽带。很多同学卡在“编辑后内容没变”,八成是因为WHERE条件写成了WHERE id=?(少了个下划线),而_id才是Cursor能识别的列名。这是个经典玄学坑,debug时用Log.d("DEBUG", "cursor column names: " + Arrays.toString(cursor.getColumnNames()));打印列名,立刻破案。
4. 运行与调试全流程:从Android Studio导入到真机验证的每一步实操
4.1 导入工程的正确姿势:别让Gradle同步毁掉第一印象
- 解压Mystudent.rar到无中文、无空格路径,例如
D:\AndroidProjects\Mystudent; - 打开Android Studio →Open an existing project→ 选择解压后的
Mystudent文件夹(不是.rar文件!); - 首次导入会触发Gradle同步,此时观察右下角提示:
- 若提示“Gradle project sync failed”,点开
Build → Sync Project with Gradle Files重试; - 若提示“Could not find method compile()”,说明Gradle版本太老,需升级:打开
gradle/wrapper/gradle-wrapper.properties,将distributionUrl改为https\://services.gradle.org/distributions/gradle-7.2-bin.zip(对应AS 4.2+);
- 若提示“Gradle project sync failed”,点开
- 同步成功后,检查
app/src/main/AndroidManifest.xml中package属性是否为com.example.mystudent,若与你本地包名冲突(如你之前建过同名项目),需全局替换:右键com.example.mystudent包名 →Refactor → Rename→ 勾选Search in comments and strings,确保所有引用同步更新。
提示:同步完成后,
app/build/intermediates/merged_manifests/debug/AndroidManifest.xml会生成合并后的清单文件,可用来确认Activity是否注册成功。若AddStudentActivity没出现在这里,说明Manifest写错了。
4.2 连接真机与模拟器的双保险策略
- 真机调试:开启手机开发者选项 → 打开USB调试 → 用原装数据线连接电脑 → Android Studio中Device下拉菜单应显示设备型号。若显示
????????,需安装对应手机厂商的USB驱动(华为用HiSuite,小米用Mi PC Suite); - 模拟器推荐:AVD Manager中创建Pixel 3 API 30(Android 11)的x86_64镜像,启动后若卡在Google Logo,勾选
Cold Boot Now;若提示HAXM not installed,需在SDK Tools中勾选Intel x86 Emulator Accelerator (HAXM installer)并单独安装。
无论哪种方式,首次运行前务必检查:
app/src/main/res/values/strings.xml中app_name是否可见(排除资源编译失败);app/src/main/java/.../MainActivity.java中setContentView(R.layout.activity_main)的activity_main.xml是否存在且无语法错误。
4.3 关键日志定位法:用logcat快速揪出Crash根源
运行后App闪退?别急着重装,打开Android Studio底部Logcat窗口,设置过滤:
- 过滤器名称:
Mystudent(自定义); - Log Level:
Error; - Package Name:
com.example.mystudent; - 搜索关键词:
FATAL EXCEPTION、NullPointerException、SQLiteException。
典型报错及对策:
| 现象 | 原因 | 解决 |
|---|---|---|
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'void android.widget.Button.setOnClickListener(...)' on a null object reference | findViewById(R.id.btn_add)返回null,说明activity_main.xml中没有id为btn_add的Button,或ID写错(如写成R.id.add_btn) | 检查XML中android:id="@+id/btn_add"是否拼写一致,且在setContentView()之后调用findViewById |
android.database.sqlite.SQLiteException: no such table: student (code 1 SQLITE_ERROR) | StudentDBHelper.onCreate()未执行,常见于dbHelper = new StudentDBHelper(this)写在onCreate()之前,导致构造函数中getWritableDatabase()提前触发 | 确保StudentDBHelper dbHelper;声明在类成员变量,dbHelper = new StudentDBHelper(this);写在onCreate()方法内第一行 |
android.content.ActivityNotFoundException: Unable to find explicit activity class {...AddStudentActivity} | AndroidManifest.xml中漏注册AddStudentActivity,或android:name写成.addStudentActivity(首字母未大写) | 检查Manifest中<activity android:name=".AddStudentActivity">是否完整,注意大小写 |
4.4 功能验证Checklist:五步确认系统闭环
运行App后,按顺序验证以下动作,每步成功即打钩:
- ✅ 点击“添加学生” → 跳转至添加界面,输入姓名“张三”、年龄“20”、专业“计算机”,点击“保存” → 弹出Toast“添加成功!”;
- ✅ 返回主界面 → ListView中出现“张三 20 计算机”一行;
- ✅ 长按该行 → 弹出“删除”对话框,点击“确定” → 该行消失;
- ✅ 再次点击“添加”,输入“李四”、“21”、“软件工程” → 成功添加;
- ✅ 重启App → “李四”的信息仍在列表中(证明SQLite持久化生效)。
若第5步失败(重启后列表为空),说明数据库未真正写入。此时用ADB命令导出数据库验证:
# 在终端执行(需已连接设备) adb shell "run-as com.example.mystudent cp /data/data/com.example.mystudent/databases/student.db /sdcard/" adb pull /sdcard/student.db ./ # 下载到电脑 # 用DB Browser for SQLite打开student.db,查看student表是否有数据5. 常见问题排查:五个真实翻车现场与救命解法
5.1 现象:点击“添加”按钮毫无反应,logcat无任何输出
原因:btn_add.setOnClickListener(...)写在setContentView(R.layout.activity_main)之前,导致findViewById()找不到控件,返回null,setOnClickListener调用失败但不报错。
解决:严格遵循“先setContentView,再findViewById,最后setOnClickListener”顺序。在MainActivity.onCreate()中,把Button addBtn = findViewById(R.id.btn_add);这行移到setContentView(...)之后。
5.2 现象:添加学生后列表不刷新,需手动杀进程再进才显示
原因:loadStudentList()只在onCreate()中调用一次,添加新数据后未重新加载。AddStudentActivity保存成功后直接finish(),但MainActivity的onResume()里没调用loadStudentList()。
解决:在MainActivity中重写onResume():
@Override protected void onResume() { super.onResume(); loadStudentList(); // 每次回到主界面都刷新列表 }或者更优解:在AddStudentActivity保存成功后,发送广播或调用startActivityForResult(已废弃)的替代方案——用ActivityResultLauncher,但课程设计中直接onResume刷新最简单。
5.3 现象:编辑学生时,界面上显示的姓名、年龄全是0或空字符串
原因:EditStudentActivity中查询SQL写错,例如SELECT * FROM student WHERE id=?,但Cursor的_id列名是_id,getColumnIndex("id")返回-1,cursor.getString(-1)返回null。
解决:查询时明确指定列名,并用getColumnIndexOrThrow("_id"):
String query = "SELECT _id, name, age, major FROM student WHERE _id=?"; Cursor cursor = db.rawQuery(query, new String[]{String.valueOf(id)}); if (cursor.moveToFirst()) { String name = cursor.getString(cursor.getColumnIndexOrThrow("name")); int age = cursor.getInt(cursor.getColumnIndexOrThrow("age")); String major = cursor.getString(cursor.getColumnIndexOrThrow("major")); // 填充EditText... } cursor.close();5.4 现象:删除学生后,再次添加时新记录的ID不连续(如删了id=2,新ID变成4)
原因:SQLite的AUTOINCREMENT保证主键唯一且递增,但不保证连续。删除记录后,ID不会回收,这是数据库正常行为,不是Bug。
解决:无需解决。若老师质疑,可解释:“AUTOINCREMENT用于避免主键冲突,连续ID需额外维护序号表,违背关系型数据库设计原则。”课程设计中,只要功能正确,ID是否连续不扣分。
5.5 现象:在Android 12(API 31)及以上设备点击删除对话框“确定”后,App闪退
原因:AlertDialog.Builder在onCreateDialog()中创建,但Context传入的是Activity.this,而Android 12对Dialog的Context校验更严格,若Activity已finish(),show()会抛BadTokenException。
解决:确保AlertDialog在Activity存活时创建。将删除逻辑写在MainActivity中,而非Adapter的onClick里:
// 在MainActivity中定义 private void showDeleteDialog(final long studentId) { new AlertDialog.Builder(this) .setTitle(getString(R.string.dialog_delete_title)) .setMessage(getString(R.string.dialog_delete_message)) .setPositiveButton("确定", (dialog, which) -> { StudentDAO dao = new StudentDAO(this); dao.deleteStudent(studentId); loadStudentList(); // 刷新列表 }) .setNegativeButton("取消", null) .show(); }然后在Adapter的onClick中调用((MainActivity)context).showDeleteDialog(id)。
6. 二次开发改造指南:从交作业到真能用的三个跃迁技巧
6.1 技巧一:增加搜索功能——三步接入Filterable接口,让列表秒变可检索
课程设计只要求增删改查,但加个搜索框能让项目脱颖而出。核心是让SimpleCursorAdapter支持过滤,但SimpleCursorAdapter本身不实现Filterable,所以需自定义Adapter:
// 新建 StudentCursorAdapter.java public class StudentCursorAdapter extends CursorAdapter implements Filterable { private Context context; private StudentDAO dao; public StudentCursorAdapter(Context context, Cursor c, int flags) { super(context, c, flags); this.context = context; this.dao = new StudentDAO(context); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { return LayoutInflater.from(context).inflate(R.layout.list_item_student, parent, false); } @Override public void bindView(View view, Context context, Cursor cursor) { TextView name = view.findViewById(R.id.tv_name); TextView age = view.findViewById(R.id.tv_age); TextView major = view.findViewById(R.id.tv_major); name.setText(cursor.getString(cursor.getColumnIndexOrThrow("name"))); age.setText(String.valueOf(cursor.getInt(cursor.getColumnIndexOrThrow("age")))); major.setText(cursor.getString(cursor.getColumnIndexOrThrow("major"))); } @Override public Cursor runQueryOnBackgroundThread(CharSequence constraint) { if (constraint == null || constraint.length() == 0) { return dao.getAllStudents(); // 无搜索词,返回全量 } else { return dao.searchStudents(constraint.toString()); // 自定义搜索方法 } } }StudentDAO.java中新增searchStudents():
public Cursor searchStudents(String keyword) { SQLiteDatabase db = dbHelper.getReadableDatabase(); String query = "SELECT _id, name, age, major FROM student " + "WHERE name LIKE ? OR major LIKE ?"; String[] args = {"%" + keyword + "%", "%" + keyword + "%"}; return db.rawQuery(query, args); }最后在MainActivity中替换Adapter:
// 原来用SimpleCursorAdapter的地方,改为: StudentCursorAdapter adapter = new StudentCursorAdapter(this, cursor, 0); listView.setAdapter(adapter); // 添加搜索框(在activity_main.xml顶部加EditText) EditText searchBox = findViewById(R.id.et_search); searchBox.addTextChangedListener(new TextWatcher() { @Override public void onTextChanged(CharSequence s, int start, int before, int count) { adapter.getFilter().filter(s); // 触发runQueryOnBackgroundThread } // 其他方法留空 });从那以后我每次做列表页,都强制在
onCreate()里加一行Log.d("Adapter", "Adapter created with cursor: " + cursor.getCount());,确保Cursor不为空。搜索功能上线后,再加一句Log.d("Search", "Searching for: " + keyword);,调试时一眼看清搜索词是否传进来了。希望帮到你。
本文还有配套的精品资源,点击获取