☰
Android学生信息管理App课程设计:Room+RecyclerView实现增删改查
2026/10/13 11:28:46 网站建设 项目流程

简介:这份资源是面向安卓初学者与课程设计需求者的Android Studio学生信息管理App完整工程,基于官方IDE构建,帮助读者掌握从界面布局到数据存储、活动跳转与调试发布的完整开发链路。压缩包共1426个文件,约21.14MB,以XML布局、PNG图片资源、Java源码、class与dex编译产物、JSON配置及APK安装包为主,另含Gradle构建脚本与属性文件,覆盖工程源码、资源与打包产物各环节。目前已有3734人学习下载,适合作为课程设计参考或个人作品集素材。工程围绕学生信息的增删改查展开,涉及RecyclerView列表展示、SQLite与SharedPreferences存储、Activity生命周期与Intent传值、AlertDialog与菜单交互、运行时权限申请及自定义Adapter等核心知识点,并保留Logcat调试与真机测试痕迹。读者可据此理解安卓应用整体架构,对照源码排查布局与数据绑定问题,快速完成同类课程设计或在此基础上扩展功能。

1. 从一份课程设计压缩包说起:学生信息管理 App 到底要交什么

很多同学拿到「Android Studio 学生信息管理 App 安卓课程设计完整版」这类题目时,第一反应是去搜一份能跑的源码,解压、导入、点运行,看到列表能滚动就以为万事大吉。真正答辩或者验收的时候,老师问一句「数据存哪了」「增删改查怎么保证不崩」,当场就卡住。这个标题背后其实是一套很典型的最小可用系统:用 Android Studio 建工程,用 SQLite 或 Room 做本地持久化,用 RecyclerView 展示学生列表,再配几个 Activity 完成增删改查。它解决的不是高并发、不是分布式,而是「一个能独立跑通、逻辑闭环、代码可讲清楚」的课程设计交付物。适合的人群很明确:正在做安卓课程设计、需要一份结构清晰可复现方案的学生,以及想用一个小项目把 Android 基础组件串起来的初学者。下面我按实际动手顺序,把选型、建库、界面、排错和验收一条线讲透。

2. 工程结构与技术选型:为什么我建议用 Room 而不是裸 SQLite

2.1 三种常见存储方案的取舍

课程设计里做学生信息管理,存储方案基本就三种:SharedPreferences、裸 SQLite、Room。SharedPreferences 只能存键值对,存一个学生列表要序列化成 JSON,查询和更新全靠自己遍历,稍微多几条数据就难维护,只适合存「是否登录」这类开关。裸 SQLite 用 SQLiteOpenHelper 手写建表语句和 Cursor 解析,能跑,但每个实体都要重复写一遍增删改查,Cursor 的列索引写错一个数字就取到错误字段,调试起来很费时间。Room 是官方在 SQLite 之上封的一层 ORM,用注解定义实体和 DAO,编译期就能检查 SQL 语句里的表名和字段名,写错了直接编译不过,而不是等到运行时崩掉。

我一般会这样判断:如果课程设计只要求「能存能取」,裸 SQLite 也能交差;但只要老师会看代码结构,或者你后面想加搜索、排序、分页,Room 的收益就非常明显。Room 的依赖也不复杂,在模块级 build.gradle 里加几个 androidx.room 的库即可。需要提醒的是,Room 的版本要和 compileSdk 匹配,版本差太多会出现注解处理器找不到符号的问题,这是新手最容易翻车的地方之一。

2.2 用 Room 建库的最小可用配置

下面这段是模块级 build.gradle 里跟 Room 相关的配置,我把它单独拎出来,方便对照自己的工程。

// 模块级 build.gradle dependencies { // Room 运行时库,提供数据库、实体、DAO 的核心注解 implementation "androidx.room:room-runtime:2.5.2" // 注解处理器,编译期根据 @Entity/@Dao 生成实现类 annotationProcessor "androidx.room:room-compiler:2.5.2" // 如果工程是 Kotlin,把上面这行换成 kapt,并在插件里启用 kotlin-kapt }

逻辑说明:room-runtime 是运行时依赖,room-compiler 是编译期依赖,两者版本必须一致,否则生成的代码和运行时对不上。参数说明:2.5.2 只是我写这篇时常用的一个稳定版本,你完全可以用自己工程里已有的版本,关键是 runtime 和 compiler 保持同版本号。如果工程是 Kotlin,annotationProcessor 要换成 kapt,并在 plugins 里加 kotlin-kapt,否则注解处理器不会执行,运行时会报「Cannot find implementation for Database」这类错误。

2.3 实体、DAO、数据库三件套的写法

Room 的核心就三个东西:@Entity 描述表结构,@Dao 描述操作,@Database 把两者串起来。下面是一个学生实体的最小定义。

// Student.java @Entity(tableName = "student") public class Student { @PrimaryKey(autoGenerate = true) public int id; // 主键,自增,插入时不用赋值 public String name; // 姓名 public String studentNo; // 学号,业务上应唯一 public String className; // 班级 public int age; // 年龄 public Student(String name, String studentNo, String className, int age) { this.name = name; this.studentNo = studentNo; this.className = className; this.age = age; } }

逻辑说明:@Entity 的 tableName 指定表名,不写就默认用类名。@PrimaryKey(autoGenerate = true) 让数据库自动分配 id,插入时不需要手动设置。参数说明:studentNo 在业务上应该唯一,但 Room 默认不校验,需要加 @Index(value = "studentNo", unique = true) 才能在建表时创建唯一索引,否则可以插入两个相同学号的学生,这是后面「数据重复」坑的根源。

DAO 层负责把 SQL 操作映射成方法,下面这段覆盖了增删改查和按姓名模糊搜索。

// StudentDao.java @Dao public interface StudentDao { @Insert long insert(Student student); // 返回新插入行的 id @Update void update(Student student); // 按主键更新 @Delete void delete(Student student); // 按主键删除 @Query("SELECT * FROM student ORDER BY id DESC") List<Student> getAll(); // 按 id 倒序,最新添加的在最上面 @Query("SELECT * FROM student WHERE name LIKE '%' || :keyword || '%'") List<Student> searchByName(String keyword); // 模糊搜索 }

逻辑说明:@Insert、@Update、@Delete 是 Room 提供的便捷注解,会根据实体主键自动生成 SQL。@Query 里写的是标准 SQL,Room 在编译期会校验表名和字段名。参数说明:searchByName 里的 :keyword 是占位符,用 || 做字符串拼接是 SQLite 的写法,等价于 LIKE '%keyword%'。注意 getAll 的排序,如果按 id 升序,新加的学生会跑到列表最下面,用户会以为没添加成功,这是很常见的体验问题。

数据库类把实体和 DAO 组装起来,并声明版本号。

// AppDatabase.java @Database(entities = {Student.class}, version = 1, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { public abstract StudentDao studentDao(); private static volatile AppDatabase INSTANCE; public static AppDatabase getInstance(Context context) { if (INSTANCE == null) { synchronized (AppDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder( context.getApplicationContext(), AppDatabase.class, "student_db" // 数据库文件名 ).build(); } } } return INSTANCE; } }

逻辑说明:@Database 的 entities 列出所有实体,version 是数据库版本,改表结构时要递增并写 Migration,否则运行时会抛异常。exportSchema = false 只是不导出 schema 文件,课程设计里可以关掉。参数说明:databaseBuilder 的第三个参数是数据库文件名,改名字等于换了一个新库,旧数据不会迁移。双检锁保证单例,避免多个数据库实例同时打开同一个文件导致锁冲突。

3. 界面与交互:RecyclerView 列表和增删改查怎么串起来

3.1 列表页的布局与适配器

学生列表用 RecyclerView 是标准做法,比 ListView 更灵活,也更容易讲清楚 ViewHolder 复用机制。主界面布局就是一个 RecyclerView 加一个悬浮按钮。

<!-- activity_main.xml --> <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recycler_view" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="8dp"/> <com.google.android.material.floatingactionbutton.FloatingActionButton android:id="@+id/fab_add" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="bottom|end" android:layout_margin="16dp" android:contentDescription="添加学生" android:src="@android:drawable/ic_input_add"/> </androidx.coordinatorlayout.widget.CoordinatorLayout>

逻辑说明:CoordinatorLayout 让悬浮按钮能浮在列表上方,FloatingActionButton 是 Material 组件,需要引入 material 依赖。参数说明:layout_gravity 控制按钮位置,contentDescription 是给无障碍功能用的,不写会有警告。RecyclerView 的 padding 让列表内容不贴边,视觉上更舒服。

适配器负责把 Student 对象绑定到列表项视图上。

// StudentAdapter.java public class StudentAdapter extends RecyclerView.Adapter<StudentAdapter.VH> { private List<Student> 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; } public void setData(List<Student> list) { this.data = list; notifyDataSetChanged(); // 数据整体刷新 } static class VH extends RecyclerView.ViewHolder { TextView tvName, tvNo, tvClass; VH(View itemView) { super(itemView); tvName = itemView.findViewById(R.id.tv_name); tvNo = itemView.findViewById(R.id.tv_no); tvClass = itemView.findViewById(R.id.tv_class); } } @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.name); holder.tvNo.setText(s.studentNo); holder.tvClass.setText(s.className); 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(); } }

逻辑说明:ViewHolder 缓存视图引用,避免每次绑定都 findViewById。setData 里用 notifyDataSetChanged 整体刷新,简单但会重绘所有可见项,数据量大时会有卡顿。参数说明:onLongClickListener 返回 true 表示长按事件被消费,不会再触发点击事件,否则长按会同时触发点击,导致既进编辑页又弹删除框。这是交互上很容易忽略的细节。

3.2 增删改查在主界面里的调度

主界面负责把数据库操作和界面刷新串起来,下面这段是核心逻辑。

// MainActivity.java public class MainActivity extends AppCompatActivity { private StudentAdapter adapter; private StudentDao dao; private List<Student> currentList = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); dao = AppDatabase.getInstance(this).studentDao(); RecyclerView rv = findViewById(R.id.recycler_view); rv.setLayoutManager(new LinearLayoutManager(this)); adapter = new StudentAdapter(); rv.setAdapter(adapter); adapter.setOnItemClickListener(new StudentAdapter.OnItemClickListener() { @Override public void onItemClick(Student student) { // 跳转到编辑页,把 id 传过去 Intent it = new Intent(MainActivity.this, EditActivity.class); it.putExtra("student_id", student.id); startActivity(it); } @Override public void onItemLongClick(Student student) { new AlertDialog.Builder(MainActivity.this) .setTitle("删除确认") .setMessage("确定删除 " + student.name + " 吗?") .setPositiveButton("删除", (d, w) -> { new Thread(() -> { dao.delete(student); runOnUiThread(MainActivity.this::loadData); }).start(); }) .setNegativeButton("取消", null) .show(); } }); findViewById(R.id.fab_add).setOnClickListener(v -> startActivity(new Intent(this, EditActivity.class))); } @Override protected void onResume() { super.onResume(); loadData(); // 从编辑页返回时刷新列表 } private void loadData() { new Thread(() -> { List<Student> list = dao.getAll(); runOnUiThread(() -> { currentList = list; adapter.setData(list); }); }).start(); } }

逻辑说明:数据库操作放在子线程,避免阻塞主线程导致 ANR。runOnUiThread 把结果切回主线程更新 UI。onResume 里刷新列表,保证从编辑页返回后能看到最新数据。参数说明:putExtra 传的是 student.id,编辑页根据 id 从数据库查出完整对象,而不是直接传整个 Student 对象,因为对象序列化在跨进程或配置变更时容易出问题。删除用 AlertDialog 二次确认,避免误触。

3.3 编辑页的插入与更新复用

编辑页同时承担新增和修改两个职责,靠 intent 里有没有 student_id 来区分。

// EditActivity.java public class EditActivity extends AppCompatActivity { private EditText etName, etNo, etClass, etAge; private StudentDao dao; private Student editing; // 为空表示新增 @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_edit); dao = AppDatabase.getInstance(this).studentDao(); etName = findViewById(R.id.et_name); etNo = findViewById(R.id.et_no); etClass = findViewById(R.id.et_class); etAge = findViewById(R.id.et_age); int id = getIntent().getIntExtra("student_id", -1); if (id != -1) { new Thread(() -> { editing = dao.getById(id); // 需要自行在 DAO 里加这个方法 runOnUiThread(() -> { if (editing != null) { etName.setText(editing.name); etNo.setText(editing.studentNo); etClass.setText(editing.className); etAge.setText(String.valueOf(editing.age)); } }); }).start(); } findViewById(R.id.btn_save).setOnClickListener(v -> save()); } private void save() { String name = etName.getText().toString().trim(); String no = etNo.getText().toString().trim(); String cls = etClass.getText().toString().trim(); String ageStr = etAge.getText().toString().trim(); if (name.isEmpty() || no.isEmpty()) { Toast.makeText(this, "姓名和学号不能为空", Toast.LENGTH_SHORT).show(); return; } int age = 0; try { age = Integer.parseInt(ageStr); } catch (NumberFormatException e) { Toast.makeText(this, "年龄必须是数字", Toast.LENGTH_SHORT).show(); return; } final int finalAge = age; new Thread(() -> { if (editing == null) { dao.insert(new Student(name, no, cls, finalAge)); } else { editing.name = name; editing.studentNo = no; editing.className = cls; editing.age = finalAge; dao.update(editing); } runOnUiThread(() -> { Toast.makeText(this, "保存成功", Toast.LENGTH_SHORT).show(); finish(); // 返回主界面,onResume 会刷新 }); }).start(); } }

逻辑说明:editing 为空走插入,不为空走更新,复用同一套表单。保存前做非空和数字校验,避免把脏数据写进库。参数说明:getIntExtra 的默认值 -1 表示没有传 id,即新增模式。finalAge 是因为 age 在 lambda 里被引用,必须是 final 或等效 final。finish 之后主界面的 onResume 会触发 loadData,列表自动刷新,不需要手动回调。

4. 避坑与排查:课程设计里最容易翻车的五个点

4.1 主线程操作数据库导致 ANR

现象:点击保存或删除后界面卡住几秒,严重时弹出「应用无响应」。原因:Room 默认不允许在主线程执行查询,如果强行用 allowMainThreadQueries 绕过,数据量大时就会阻塞 UI。解决:所有 dao 调用放进子线程或使用 ExecutorService,查询结果用 runOnUiThread 或 Handler 切回主线程。我一般会封装一个简单的 AppExecutors 工具类,把 IO 线程和主线程分开,避免到处写 new Thread。

4.2 数据库版本升级没写 Migration

现象:改了 Student 实体加了一个字段,重新运行直接崩溃,日志里是「Migration didn't properly handle」。原因:@Database 的 version 没递增,或者递增了但没提供 Migration,Room 找不到从旧版本到新版本的迁移路径。解决:课程设计阶段如果数据不重要,可以用 fallbackToDestructiveMigration() 让升级时重建表,但会丢数据;正式一点就写 Migration,用 ALTER TABLE 加字段。血泪经验是,答辩前别随便改实体结构,改了就一定要同步处理版本。

4.3 学号重复插入没有拦截

现象:同一个学号能添加多次,列表里出现两条一样学号的学生。原因:studentNo 没有加唯一索引,DAO 的 insert 也不做查重。解决:在 @Entity 上加 @Index(value = "studentNo", unique = true),插入重复时 Room 会抛 SQLiteConstraintException,在 save 里 catch 这个异常并提示「学号已存在」。注意加了唯一索引后,旧数据里如果已经有重复学号,建索引会失败,需要先清理数据。

4.4 RecyclerView 数据刷新后位置错乱

现象:删除中间某一条后,列表项内容对不上,或者点击某一行进入的是另一个学生的编辑页。原因:adapter 里的 data 列表和数据库返回的列表不是同一个引用,或者用了 notifyItemRemoved 但 position 传错。解决:每次操作后重新从数据库查全量数据,用 setData 整体替换,虽然粗暴但不会错位。如果要做局部刷新,position 必须用 holder.getAdapterPosition() 而不是构造时传入的 position,因为列表变化后旧 position 会失效。

4.5 编辑页返回后列表不更新

现象:新增或修改后返回主界面,列表还是旧的,要手动杀掉应用重进才看到新数据。原因:主界面只在 onCreate 里加载了一次数据,没有在 onResume 里刷新。解决:把 loadData 放到 onResume,或者在 startActivityForResult 的回调里刷新。现在更推荐用 ActivityResultLauncher,但课程设计里 onResume 刷新最简单可靠。注意 onResume 在每次界面可见时都会调用,包括从后台切回来,所以 loadData 要保证幂等,不要在里面做插入操作。

5. 验收前怎么自测:一份能讲清楚逻辑的检查清单

课程设计最后要面对的是演示和提问,代码能跑只是及格线,能讲清楚为什么这么写才是加分项。我一般会按下面这个顺序自测,每一步都对应一个可能被问到的问题。

检查项操作预期结果对应知识点
冷启动杀掉应用后重新打开列表能正常显示已有数据Room 持久化生效
新增点悬浮按钮填完整信息保存返回后列表顶部出现新学生insert + onResume 刷新
校验姓名留空或年龄填字母提示错误,不写入数据库表单校验
修改点某一行改名字保存列表对应项更新,id 不变update 按主键
删除长按某一行确认删除该项消失,其他项不受影响delete + 整体刷新
搜索输入姓名关键字只显示匹配的学生LIKE 查询
旋转屏幕列表页旋转数据不丢失,不崩溃数据库与生命周期

自测时重点看两个地方:一是 Logcat 里有没有 SQLite 相关异常,二是旋转屏幕后数据是否还在。如果旋转后列表空了,说明数据只存在内存里没落库,或者 Activity 重建时没重新查询。另外,答辩时老师很可能会让你现场加一个字段,比如「加一个性别」,这时候你要能说清楚改哪几个文件:实体加字段、数据库版本加一、Migration 加 ALTER TABLE、编辑页加输入框、适配器加绑定。能当场改出来,比背十页文档都有说服力。

最后说一个我自己的习惯:每次改完实体或 DAO,先把应用卸载重装一次,排除旧数据库残留的干扰,再跑一遍完整流程。课程设计的数据量小,卸载重装的成本几乎为零,但能省掉很多「明明改了却没生效」的玄学问题。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询