☰
Android学生信息管理App:Room+RecyclerView完整工程实践
2026/11/4 5:56:41 网站建设 项目流程

简介:本资源是一份面向高校Android开发初学者与课程设计实践者的完整安卓应用项目,聚焦学生信息管理系统的实战开发,覆盖Android Studio环境配置、UI布局(XML+ConstraintLayout/RecyclerView)、SQLite本地数据存储、Activity生命周期管理、Intent跳转、权限适配及APK打包发布等核心知识点。压缩包共1426个文件,含338个XML布局与资源文件、280个PNG图标素材、255个编译生成的class字节码、39个Java源码文件、12个APK安装包及Gradle构建相关文件,整体21.14MB,结构完整,可直接导入Android Studio运行调试。已有3743人学习下载,项目包含从创建工程、界面设计、增删改查功能实现到真机测试的全流程代码与资源,适配Android 6.0+运行时权限机制,并提供调试用Logcat日志参考与基础排错说明,是掌握安卓应用开发基础架构与工程规范的理想实践范例。

1. 学生信息管理App不是Demo,是能直接交课设、改毕业设计的完整工程

上周帮某高校实验室带实训的A同学调试时,他拿着一份“Android Studio学生信息管理App”源码跑来问我:“老师,这项目真能当课程设计交吗?”——我当场连上真机,三分钟导入数据、新增学号为2023001的张三、修改其专业为“智能科学与技术”、再按班级筛选导出Excel,全程没改一行代码。他愣了两秒说:“原来课设还能这么交?” 这份Mystudent.rar不是网上常见的“新建Activity+TextView硬编码5个学生”的教学玩具,而是一个覆盖SQLite本地持久化、RecyclerView分页列表、多条件模糊查询、JSON数据导入导出、Material Design规范UI、Activity生命周期健壮处理的完整安卓应用工程。它面向的是需要真实交付、接受答辩、甚至后续扩展为实习项目的学生开发者:你不需要从零造轮子,但必须理解每一层怎么协作;你改几个字段就能适配自己学校的学籍表结构,删掉导出功能也能跑得稳如老狗。如果你正卡在“写了Activity不会连数据库”“能增删但一旋转屏幕就丢数据”“老师问‘为什么用Room不用原生SQL’答不上来”,那这份资源就是为你拆包准备的。


2. 工程结构与技术栈:从build.gradle到res目录的逐层解剖

2.1 项目层级与模块划分:一个标准的Android Studio单Module工程

打开解压后的Mystudent/目录,你会看到典型的 Android Studio 单 Module 结构:

Mystudent/ ├── app/ # 主应用Module(无独立gradle,依赖根目录配置) ├── gradle/ ├── build.gradle # 项目级构建脚本(声明仓库、插件版本) ├── settings.gradle # 模块注册(仅包含':app') └── README.md # 极简说明(含数据库表结构速查)

重点在app/目录下:

app/ ├── src/main/ │ ├── java/com/example/mystudent/ # Java源码根包 │ │ ├── MainActivity.java # 主界面(含TabLayout+ViewPager2) │ │ ├── StudentListActivity.java # 学生列表(核心CRUD入口) │ │ ├── StudentDetailActivity.java# 学生详情/编辑页 │ │ ├── database/ # 数据库相关类 │ │ │ ├── StudentDatabase.java # Room Database抽象类 │ │ │ ├── StudentDao.java # Room DAO接口 │ │ │ └── StudentEntity.java # Room Entity实体类(对应students表) │ │ ├── adapter/ # RecyclerView适配器 │ │ │ └── StudentAdapter.java │ │ └── utils/ # 工具类 │ │ ├── ExcelExporter.java # Apache POI导出逻辑 │ │ └── JsonHelper.java # JSON序列化/反序列化 │ ├── res/ # 资源文件(关键!) │ │ ├── layout/ # 布局XML(activity_main.xml等) │ │ ├── values/ # 字符串、颜色、尺寸定义 │ │ └── menu/ # 选项菜单(menu_main.xml) │ └── AndroidManifest.xml # 组件注册与权限声明 └── build.gradle # Module级构建脚本(依赖、编译SDK版本)

提示:这不是Kotlin项目,全部使用Java 8语法编写,兼容Android Studio Giraffe及更高版本。minSdkVersion设为21(Android 5.0),意味着可运行于99%以上活跃安卓设备,无需担心低版本兼容性玄学问题。

2.2 核心依赖解析:为什么选这些库?它们各自扛什么活?

app/build.gradle中的关键依赖如下(已去除非必要注释):

dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.10.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' // Room数据库(替代原生SQLiteOpenHelper) implementation 'androidx.room:room-runtime:2.6.1' implementation 'androidx.room:room-ktx:2.6.1' // Kotlin扩展(本项目未用,但保留以防扩展) annotationProcessor 'androidx.room:room-compiler:2.6.1' // RecyclerView分页与动画 implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.lifecycle:lifecycle-viewmodel:2.7.0' // JSON处理(轻量级,不引入Gson/Jackson全量包) implementation 'com.alibaba:fastjson:1.2.83' // Excel导出(Apache POI精简版,仅支持.xlsx) implementation 'org.apache.poi:poi:5.2.4' implementation 'org.apache.poi:poi-ooxml:5.2.4' // 测试依赖(可删,但建议保留用于验证) testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' }

为什么这样选?

  • Room 而非 SQLiteOpenHelper:StudentDatabase.java中通过@Database注解声明数据库,StudentDao.java用@Insert/@Query写SQL,编译期校验语句合法性,避免运行时SQLiteException黑匣子。比如@Query("SELECT * FROM students WHERE name LIKE '%' || :keyword || '%'"),Room会自动生成执行逻辑,你不用手写Cursor解析。
  • Fastjson 而非 Gson:体积更小(APK增量约150KB),对中文字段名支持更稳定。JsonHelper.java里JSON.parseObject(jsonStr, StudentEntity.class)一行完成反序列化,比 Gson 的new Gson().fromJson()少写模板参数。
  • Apache POI 5.x 而非 jxl:jxl 只支持.xls(Excel 2003),而本项目导出的是.xlsx(Office 2007+),POI 5.2.4 是当前兼容 Android 的稳定版本,ExcelExporter.java中XSSFWorkbook创建工作簿、XSSFSheet写入行,逻辑清晰。

2.3 数据库设计:一张表撑起所有业务,字段命名直击教务系统痛点

StudentEntity.java定义了唯一的数据表students,共11个字段,全部映射教务常见属性:

@Entity(tableName = "students") public class StudentEntity { @PrimaryKey(autoGenerate = true) public long id; // Room主键,自增,非业务学号 @ColumnInfo(name = "student_id") public String studentId; // 业务主键:学号(如20230001) @ColumnInfo(name = "name") public String name; // 姓名 @ColumnInfo(name = "gender") public String gender; // 性别("男"/"女",非枚举,方便扩展) @ColumnInfo(name = "major") public String major; // 专业(如"计算机科学与技术") @ColumnInfo(name = "grade") public String grade; // 年级(如"2023级") @ColumnInfo(name = "class_name") public String className; // 班级(如"计科2301班") @ColumnInfo(name = "phone") public String phone; // 手机号(字符串存,避免长数字溢出) @ColumnInfo(name = "email") public String email; // 邮箱 @ColumnInfo(name = "enrollment_date") public String enrollmentDate; // 入学日期(格式:yyyy-MM-dd) @ColumnInfo(name = "status") public int status; // 状态(0=在读,1=休学,2=毕业,3=退学) }

注意:student_id是业务主键(学号),id是Room内部主键。这样设计的好处是——删除学生时用id(安全,不暴露业务键),查询/导出时用student_id(符合教务习惯)。status用整型而非字符串,既节省存储,又便于SQLWHERE status IN (0,1)多状态筛选。

2.4 UI架构:TabLayout + ViewPager2 实现三栏导航,不是Fragment堆砌

MainActivity.java是整个App的壳,它用TabLayout和ViewPager2管理三个一级页面:

  • 首页(HomeFragment):欢迎语 + 快捷操作按钮(新增、批量导入)
  • 列表页(StudentListFragment):RecyclerView展示学生,顶部有搜索框和筛选下拉(按专业、年级、状态)
  • 统计页(StatsFragment):柱状图(用MPAndroidChart库绘制,已预集成)显示各专业人数、各年级分布

关键代码在MainActivity.java的setupViewPager()方法中:

private void setupViewPager() { ViewPager2 viewPager = findViewById(R.id.view_pager); TabLayout tabLayout = findViewById(R.id.tab_layout); // 使用 FragmentStateAdapter(推荐,内存友好) viewPager.setAdapter(new FragmentStateAdapter(this) { @NonNull @Override public Fragment createFragment(int position) { switch (position) { case 0: return new HomeFragment(); case 1: return new StudentListFragment(); case 2: return new StatsFragment(); default: return new HomeFragment(); } } @Override public int getItemCount() { return 3; } }); // 关联TabLayout与ViewPager2 new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { switch (position) { case 0: tab.setText("首页"); case 1: tab.setText("学生列表"); case 2: tab.setText("统计分析"); } }).attach(); }

为什么不用Activity跳转?
因为学生列表页(StudentListActivity)是独立Activity,而首页/列表/统计是Fragment——这里存在一个设计分层:StudentListActivity是功能入口页(带ActionBar菜单、支持从通知启动),而StudentListFragment是嵌入式展示页(作为Tab的一部分)。这种混合架构让项目既能满足课程设计“有多个页面”的要求,又保持了代码复用(列表逻辑写一次,两处调用)。


3. 核心功能实现:从增删改查到Excel导出的代码级拆解

3.1 Room数据库初始化与DAO调用:告别SQLiteOpenHelper的手动建表

数据库初始化发生在StudentDatabase.java中,这是Room的标准写法:

@Database(entities = {StudentEntity.class}, version = 1, exportSchema = false) public abstract class StudentDatabase extends RoomDatabase { private static volatile StudentDatabase INSTANCE; public abstract StudentDao studentDao(); public static StudentDatabase getDatabase(final Context context) { if (INSTANCE == null) { synchronized (StudentDatabase.class) { if (INSTANCE == null) { INSTANCE = Room.databaseBuilder( context.getApplicationContext(), StudentDatabase.class, "student_database" // 数据库文件名 ).allowMainThreadQueries() // 仅开发期开启!上线必须删 .fallbackToDestructiveMigration() // 版本升级时清空重建(课设够用) .build(); } } } return INSTANCE; } }

关键点说明:

  • allowMainThreadQueries():开发阶段便利开关,但必须在发布前注释掉!否则主线程查数据库会ANR。课设答辩演示时开着没问题,但你要知道它在哪、为什么开。
  • fallbackToDestructiveMigration():当version从1升到2时,Room自动删库重建。课设期间你大概率只改字段不改逻辑,这个开关省去写Migration的麻烦。但若你后续要保留数据,就必须换成addMigrations(...)并手写Migration类。
  • getDatabase(context)是单例模式,确保整个App只有一份数据库实例,避免多线程冲突。

DAO接口StudentDao.java定义了所有数据操作:

@Dao public interface StudentDao { @Insert(onConflict = OnConflictStrategy.REPLACE) long insert(StudentEntity student); @Update void update(StudentEntity student); @Delete void delete(StudentEntity student); @Query("SELECT * FROM students WHERE student_id = :id") StudentEntity findById(String id); @Query("SELECT * FROM students WHERE name LIKE '%' || :keyword || '%' OR student_id LIKE '%' || :keyword || '%'") List<StudentEntity> searchByNameOrId(String keyword); @Query("SELECT * FROM students WHERE major = :major AND status = 0 ORDER BY student_id ASC") List<StudentEntity> getStudentsByMajor(String major); @Query("SELECT COUNT(*) FROM students WHERE status = 0") int getActiveCount(); @Query("DELETE FROM students") void deleteAll(); }

注意onConflict = OnConflictStrategy.REPLACE:当插入重复student_id时,Room自动更新旧记录而非报错。这符合教务场景——学号唯一,录入时发现已有,就该覆盖信息。

3.2 RecyclerView列表渲染:ViewHolder绑定与点击事件的健壮写法

StudentAdapter.java继承RecyclerView.Adapter<StudentAdapter.ViewHolder>,关键在onBindViewHolder:

@Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { StudentEntity student = students.get(position); holder.tvName.setText(student.name); holder.tvId.setText("学号:" + student.studentId); holder.tvMajor.setText(student.major); holder.tvClass.setText(student.className); // 性别用图标代替文字,更省空间 if ("男".equals(student.gender)) { holder.ivGender.setImageResource(R.drawable.ic_male); } else { holder.ivGender.setImageResource(R.drawable.ic_female); } // 状态用颜色区分(在values/colors.xml中预定义) switch (student.status) { case 0: holder.tvStatus.setText("在读"); holder.tvStatus.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.status_active)); break; case 1: holder.tvStatus.setText("休学"); holder.tvStatus.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.status_suspended)); break; default: holder.tvStatus.setText("其他"); holder.tvStatus.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.status_other)); } // 点击条目跳转详情页(带数据传递) holder.itemView.setOnClickListener(v -> { Intent intent = new Intent(holder.itemView.getContext(), StudentDetailActivity.class); // 用Parcelable传递对象(StudentEntity实现Parcelable) intent.putExtra("student", student); holder.itemView.getContext().startActivity(intent); }); }

为什么用Parcelable而非Serializable?
StudentEntity实现了Parcelable接口(代码略,标准模板),序列化效率比Serializable高10倍以上,尤其在列表项多时,避免滑动卡顿。intent.putExtra("student", student)这一行背后是Parcel的高效打包。

3.3 多条件筛选与模糊搜索:SQL拼接的安全实践

筛选逻辑在StudentListFragment.java的filterStudents()方法中:

private void filterStudents() { String major = spinnerMajor.getSelectedItem().toString(); String grade = spinnerGrade.getSelectedItem().toString(); String statusStr = spinnerStatus.getSelectedItem().toString(); String keyword = etSearch.getText().toString().trim(); List<StudentEntity> filtered = new ArrayList<>(); // 先查全量(Room不支持动态WHERE,所以用内存过滤) List<StudentEntity> all = studentDao.getAll(); // getAll()是自定义@Query("SELECT * FROM students") for (StudentEntity s : all) { boolean match = true; if (!"全部专业".equals(major) && !s.major.equals(major)) match = false; if (!"全部年级".equals(grade) && !s.grade.equals(grade)) match = false; // 状态转换:字符串→int int targetStatus = 0; if ("在读".equals(statusStr)) targetStatus = 0; else if ("休学".equals(statusStr)) targetStatus = 1; else if ("毕业".equals(statusStr)) targetStatus = 2; else if ("退学".equals(statusStr)) targetStatus = 3; if (targetStatus != 0 && s.status != targetStatus) match = false; // 搜索关键词(姓名或学号) if (!keyword.isEmpty()) { if (!s.name.contains(keyword) && !s.studentId.contains(keyword)) { match = false; } } if (match) filtered.add(s); } adapter.updateData(filtered); // 自定义方法,notifyDataSetChanged() }

提示:这里用内存过滤而非SQL动态拼接,是因为Room不支持@Query中的动态表名/列名。虽然数据量少时无感,但若你后续接入网络API,应改为服务端筛选,客户端只做最终呈现。

3.4 JSON数据导入导出:Fastjson一行搞定,但要注意空值陷阱

JsonHelper.java提供两个静态方法:

// 导出为JSON字符串(用于备份或API传输) public static String toJson(List<StudentEntity> students) { // Fastjson默认不序列化null字段,避免JSON臃肿 return JSON.toJSONString(students, SerializerFeature.WriteMapNullValue); } // 从JSON字符串恢复List public static List<StudentEntity> fromJson(String json) { if (TextUtils.isEmpty(json)) return new ArrayList<>(); try { return JSON.parseArray(json, StudentEntity.class); } catch (JSONException e) { Log.e("JsonHelper", "JSON解析失败", e); return new ArrayList<>(); } }

血泪经验:WriteMapNullValue参数不能少!
StudentEntity中某些字段(如email)可能为空,若不加此参数,Fastjson生成的JSON会缺失该字段,导致反序列化时email=null,而Room插入时@ColumnInfo默认允许NULL,但某些设备上SQLite会报NOT NULL constraint failed。加上后,JSON中显式写出"email": null,反序列化时字段值就是null,Room能正确处理。

3.5 Excel导出:Apache POI写入.xlsx,避开Android OOM雷区

ExcelExporter.java的核心导出方法:

public static boolean exportToExcel(Context context, List<StudentEntity> students, String fileName) { try { // 关键:用SXSSFWorkbook替代XSSFWorkbook,流式写入防内存溢出 SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 每100行刷入磁盘 XSSFSheet sheet = workbook.createSheet("学生信息"); // 表头 String[] headers = {"学号", "姓名", "性别", "专业", "年级", "班级", "手机号", "邮箱", "入学日期", "状态"}; XSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行 for (int i = 0; i < students.size(); i++) { StudentEntity s = students.get(i); XSSFRow row = sheet.createRow(i + 1); row.createCell(0).setCellValue(s.studentId); row.createCell(1).setCellValue(s.name); row.createCell(2).setCellValue(s.gender); row.createCell(3).setCellValue(s.major); row.createCell(4).setCellValue(s.grade); row.createCell(5).setCellValue(s.className); row.createCell(6).setCellValue(s.phone); row.createCell(7).setCellValue(s.email); row.createCell(8).setCellValue(s.enrollmentDate); row.createCell(9).setCellValue(getStatusText(s.status)); // 状态转文字 } // 写入文件(Android 10+需用Scoped Storage) File file = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), fileName); FileOutputStream fos = new FileOutputStream(file); workbook.write(fos); fos.close(); workbook.dispose(); // 必须调用,释放临时文件 // 触发媒体扫描,让文件管理器可见 MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null); return true; } catch (Exception e) { Log.e("ExcelExporter", "导出失败", e); return false; } }

避坑点:

  • SXSSFWorkbook(100):内存行数限制为100,超出部分自动刷入磁盘临时文件,防止大数据量(>1000条)时OOM。XSSFWorkbook会把整个Excel加载进内存,安卓上极易崩溃。
  • workbook.dispose():必须调用!否则临时文件不清理,多次导出后SD卡被占满。
  • context.getExternalFilesDir(...):写入App私有目录,无需申请WRITE_EXTERNAL_STORAGE权限(Android 10+强制),规避权限申请的兼容性翻车。

4. 运行与调试:从Android Studio配置到真机部署的全流程

4.1 环境配置:最低要求与常见报错修复

最低环境要求:

  • Android Studio Giraffe | 2022.3.1 或更高版本(推荐 Hedgehog 2023.1.1)
  • JDK 17(AS安装时自带,勿手动换JDK 8/11)
  • Gradle Plugin 8.0+(build.gradle中com.android.tools.build:gradle:8.1.2)
  • 编译SDK:33(Android 13),目标SDK:33

常见报错与修复:

  1. Failed to resolve androidx.room:room-compiler:2.6.1
    → 检查build.gradle顶部是否漏了google()仓库:

    repositories { google() // 必须有! mavenCentral() }
  2. Cannot resolve symbol 'R'
    → 90%是XML布局文件有语法错误(如android:text="@string/xxx"但strings.xml里没定义xxx)。点开res/layout/下任意XML,看AS是否有红色波浪线,修完后Build > Clean Project。

  3. Execution failed for task ':app:processDebugResources'
    → 资源命名违规:res/drawable/ic_male.png文件名含大写字母或下划线。改为ic_male(全小写+下划线),并检查所有@drawable/xxx引用同步修改。

4.2 真机调试:USB调试开启与ADB连接验证

步骤:

  1. 手机设置 → 关于手机 → 连续点击“版本号”7次,开启开发者选项
  2. 返回设置 → 系统 → 开发者选项 → 打开“USB调试”
  3. 用原装USB线连接电脑,在AS中点击 ▶️ Run
  4. 若弹出“Select Deployment Target”,选择你的设备(显示型号+Android版本)
  5. 验证ADB是否通:终端执行adb devices,应返回类似:
    List of devices attached 1234567890ABCDEF device

注意:若显示unauthorized,手机会弹出“允许USB调试吗?”对话框,勾选“始终允许”,点确定。

4.3 数据库查看:用Device File Explorer窥探SQLite文件

Room数据库文件路径为:
/data/data/com.example.mystudent/databases/student_database

查看步骤:

  1. AS右上角点击View > Tool Windows > Device File Explorer
  2. 依次展开:data > data > com.example.mystudent > databases
  3. 右键student_database→Save As...保存到电脑
  4. 用 DB Browser for SQLite 打开,可直观看到students表结构与数据

提示:/data/data/目录需Root权限才能直接访问,但AS的Device File Explorer在调试模式下可绕过(仅限debug包)。若看不到,检查App是否以debug模式运行(Build Variants中选debug)。

4.4 日志追踪:用Logcat过滤关键事件

在AS底部Logcat窗口中,输入过滤器:

  • tag:StudentDao→ 查看所有数据库操作日志(StudentDao.java中Log.d("StudentDao", "..."))
  • tag:ExcelExporter→ 查看导出成功/失败日志
  • pid:<your_app_pid>→ 只看当前App日志(PID在Logcat左上角显示)

关键日志埋点位置:

  • StudentDao.insert()开头:Log.d("StudentDao", "插入学生:" + student.studentId)
  • ExcelExporter.exportToExcel()结尾:Log.i("ExcelExporter", "导出成功,共" + students.size() + "条")

5. 避坑指南:5个真实踩过的坑与解决方案

5.1 现象:新增学生后,列表不刷新,必须杀进程重进才显示

原因:StudentListActivity中调用studentDao.insert()后,未及时通知RecyclerView.Adapter数据变更。代码中漏掉了adapter.notifyItemInserted()或adapter.notifyDataSetChanged()。
解决:在StudentListActivity.java的新增逻辑末尾,添加:

long id = studentDao.insert(newStudent); if (id > 0) { // 刷新列表(推荐:先查新数据,再更新Adapter) List<StudentEntity> newList = studentDao.getAll(); adapter.updateData(newList); // updateData()方法内部调用notifyDataSetChanged() }

5.2 现象:在StudentDetailActivity修改姓名后,返回列表页,该条目仍显示旧姓名

原因:StudentDetailActivity中调用studentDao.update()成功,但StudentListFragment的RecyclerView未监听到数据变化,因Room未开启LiveData或Flow观察。本项目用的是手动刷新模式。
解决:在StudentDetailActivity的onBackPressed()中,添加结果回传:

@Override public void onBackPressed() { Intent result = new Intent(); result.putExtra("updated_student", editedStudent); setResult(RESULT_OK, result); super.onBackPressed(); }

然后在StudentListActivity的onActivityResult()中接收并刷新:

@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK && data != null) { StudentEntity updated = data.getParcelableExtra("updated_student"); // 找到列表中对应项,更新数据 for (int i = 0; i < students.size(); i++) { if (students.get(i).id == updated.id) { students.set(i, updated); adapter.notifyItemChanged(i); break; } } } }

5.3 现象:导出Excel时App闪退,Logcat报OutOfMemoryError

原因:使用了XSSFWorkbook而非SXSSFWorkbook,大数据量(>500条)时内存爆满。
解决:确认ExcelExporter.java中创建的是SXSSFWorkbook,且构造参数为new SXSSFWorkbook(100)。若已用SXSSFWorkbook仍崩溃,检查是否漏掉workbook.dispose()—— 未调用会导致临时文件堆积。

5.4 现象:搜索框输入“张”,结果为空,但数据库里有“张三”

原因:searchByNameOrId()的SQL中LIKE语句写成了'%:keyword%',Room无法解析冒号参数。正确写法是'%'+:keyword+'%',但Room不支持字符串拼接,必须用||运算符。
解决:确保StudentDao.java中的Query为:

@Query("SELECT * FROM students WHERE name LIKE '%' || :keyword || '%' OR student_id LIKE '%' || :keyword || '%'")

而不是'%:keyword%'或CONCAT('%', :keyword, '%')(SQLite不支持CONCAT)。

5.5 现象:App在Android 12+设备上安装失败,提示INSTALL_FAILED_UPDATE_INCOMPATIBLE

原因:AndroidManifest.xml中package="com.example.mystudent"与之前安装的同名App签名不一致(如用不同电脑编译)。
解决:

  • 方案1(推荐):卸载旧版adb uninstall com.example.mystudent
  • 方案2:修改build.gradle中applicationId "com.example.mystudent"为新包名,如com.example.mystudent2,并同步修改AndroidManifest.xml的package属性。
  • 方案3:用同一台电脑、同一个keystore签名(课设无需正式签名,用AS默认debug keystore即可)。

6. 二次开发实战:3个高频改造需求与落地代码

6.1 需求1:增加“照片”字段,支持从相册选择并显示在列表页

改造步骤:

  1. 修改数据库实体:在StudentEntity.java中添加字段:

    @ColumnInfo(name = "photo_path") public String photoPath; // 保存图片绝对路径,如 /storage/emulated/0/DCIM/Camera/IMG_2023.jpg

    同时将version从1改为2,并删除fallbackToDestructiveMigration(),改为手动Migration:

    // 在StudentDatabase.java中添加 static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE students ADD COLUMN photo_path TEXT"); } }; // 在getDatabase()中替换为 .addMigrations(MIGRATION_1_2)
  2. 修改布局:在item_student.xml中LinearLayout内添加ImageView:

    <ImageView android:id="@+id/ivPhoto" android:layout_width="40dp" android:layout_height="40dp" android:scaleType="centerCrop" android:src="@drawable/ic_photo_default" />
  3. 修改Adapter绑定逻辑:在StudentAdapter.java的onBindViewHolder中:

    if (student.photoPath != null && !student.photoPath.isEmpty()) { Glide.with(holder.itemView.getContext()) .load(new File(student.photoPath)) .placeholder(R.drawable.ic_photo_default) .into(holder.ivPhoto); } else { holder.ivPhoto.setImageResource(R.drawable.ic_photo_default); }

    (需添加依赖implementation 'com.github.bumptech.glide:glide:4.15.1')

  4. 在详情页添加图片选择:StudentDetailActivity.java中:

    private void openGallery() { Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); startActivityForResult(intent, REQUEST_CODE_PICK_IMAGE); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_CODE_PICK_IMAGE && resultCode == RESULT_OK && data != null) { Uri selectedImage = data.getData(); String[] filePathColumn = {MediaStore.Images.Media.DATA}; Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null); if (cursor != null) { cursor.moveToFirst(); int columnIndex = cursor.getColumnIndex(filePathColumn[0]); String imagePath = cursor.getString(columnIndex); cursor.close(); // 保存到student.photoPath并更新数据库 currentStudent.photoPath = imagePath; studentDao.update(currentStudent); } } }

6.2 需求2:导出Excel时增加“导出时间”列,并自动填充当前时间

改造点:ExcelExporter.java的导出循环中,为每行增加第10列(索引9):

// 在for循环内,row.createCell(9)之后添加: row.createCell(10).setCellValue(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()));

同时更新表头数组:

String[] headers = {"学号", "姓名", "性别", "专业", "年级", "班级", "手机号", "邮箱", "入学日期", "状态", "导出时间"};

注意:SimpleDateFormat非线程安全,此处为单次导出,无并发风险。若需高并发,改用DateTimeFormatter(Java 8+)。

6.3 需求3:将“状态”字段从数字改为下拉选择,提升用户体验

改造逻辑:不改变数据库存储(仍用int),但UI层用Spinner替代文本显示。

  1. 在activity_student_detail.xml中,将状态TextView替换为Spinner:

    <Spinner android:id="@+id/spinnerStatus" android:layout_width="match_parent" android:layout_height="wrap_content" android:entries="@array/status_options" />

    并在res/values/arrays.xml中定义:

    <string-array name="status_options"> <item>在读</item> <item>休学</item> <item>毕业</item> <item>退学</item> </string-array>
  2. 在StudentDetailActivity.java中绑定Spinner:

    Spinner spinnerStatus = findViewById(R.id.spinnerStatus); ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(this, R.array.status_options, android.R.layout.simple_spinner_item); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinnerStatus.setAdapter(adapter); // 加载时设置选中项 spinnerStatus.setSelection(currentStudent.status); // 保存时获取选中值

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

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

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

立即咨询