Android毕业设计:基于Room与RecyclerView的点名系统实现 简介这是一套基于Android Studio开发的课堂点名系统毕业设计源码面向计算机相关专业本科生及Android初学者解决传统课堂考勤效率低、统计难、管理分散等实际教学管理问题。资源包含完整客户端教师端与后台管理双模块教师可登录、查看班级、执行点名签到、生成出勤统计管理员支持学生/教师/班级信息的增删查改具备典型MVC分层结构与基础权限控制逻辑。压缩包共1505个文件含224个Java业务逻辑文件、251个XML布局与资源定义、366个编译后Class字节码、318个PNG图标资源及10个APK安装包含StudentProject.apk等多项目示例整体69.73MB结构清晰便于模块化学习与二次开发。目前已有183人学习下载提供可直接运行的完整工程、配套数据库脚本sql、本地调试配置说明及典型场景操作路径适合课程设计、毕设选题与Android基础项目实战训练。1. 为什么一个“点名系统”值得用 Android Studio 做毕业设计在高校《Android 应用开发》或《移动应用实践》课程中“点名系统”看似简单却是一个极佳的毕业设计/课程设计选题它不依赖复杂算法但完整覆盖了 Android 开发的核心能力链——UI 构建RecyclerView 自定义 Item、本地数据持久化Room 或 SQLite、用户交互逻辑点击响应、状态切换、权限控制存储读写、甚至可扩展为蓝牙/Wi-Fi 设备扫描或二维码识别。很多学生误以为“点名就是打勾”结果交出的 App 只有静态列表无法保存记录、不能导出数据、没有班级管理、甚至运行就闪退。而 A306 这个编号暗示它是一套结构清晰、具备真实教学场景闭环的实现方案支持教师创建多个班级、为每个学生设置唯一学号、实时标记出勤状态、生成按日期/班级维度的统计报表并能将结果导出为 CSV 文件供教务系统导入。它不是玩具 Demo而是能部署到实训机房平板、接入校园 Wi-Fi 环境、经得起课堂连续使用压力的轻量级教学工具。适合大三下学期动手能力中等以上、希望用扎实工程细节拉开答辩差距的学生。2. 搭建 A306 点名系统的基础工程结构与核心组件选型2.1 创建最小可行项目空 Activity Kotlin API 21 起步A306 的定位是教学型应用不追求最新 API 特性而是强调稳定性和兼容性。我们选择Empty Activity模板语言设为 Kotlin比 Java 更简洁减少样板代码最低 SDK 版本设为minSdkVersion 21Android 5.0 Lollipop。这个版本覆盖 98.5% 的国内活跃设备同时支持 Material Design 组件和现代 Jetpack 库。避免使用Composable Activity模板——虽然 Jetpack Compose 是未来方向但在毕业设计中XML 布局 ViewBinding 的组合更易调试、文档更全、答辩时老师也更熟悉。# 在 Android Studio 中新建项目后检查 app/build.gradle 的关键配置 android { compileSdk 34 // 使用最新稳定版编译 SDK确保兼容新特性 defaultConfig { applicationId com.example.a306rollcall minSdk 21 targetSdk 34 // targetSdk 必须与 compileSdk 一致或略低34 是当前推荐值 versionCode 1 versionName 1.0 } }提示targetSdk 34意味着你必须显式处理 Android 14API 34的新限制例如PendingIntent的FLAG_IMMUTABLE要求。若跳过此步App 在新系统上可能无法启动通知或广播接收器。2.2 数据层选型Room 数据库替代裸 SQLite保证类型安全与可维护性点名数据需长期保存学生名单、每次点名记录且涉及多表关联班级表、学生表、点名记录表。裸 SQLite 手写 SQL 易出错、难调试、升级麻烦。Room 是 Google 官方推荐的抽象层它将数据库操作转化为类型安全的 Kotlin 函数调用并自动生成大部分底层代码。2.2.1 定义实体类Entity// data/Student.kt Entity(tableName student_table) data class Student( PrimaryKey(autoGenerate true) val id: Long 0, val classId: Long, // 所属班级 ID val studentNumber: String, // 学号唯一标识 val name: String, // 姓名 val gender: String 未知 // 性别可选字段 ) // data/AttendanceRecord.kt Entity(tableName attendance_record_table, indices [ Index(value [classId, date], unique true) ]) data class AttendanceRecord( PrimaryKey(autoGenerate true) val id: Long 0, val classId: Long, // 班级 ID val date: String, // 格式yyyy-MM-dd作为日期主键的一部分 val statusMap: String // JSON 字符串如 {2023001:present,2023002:absent} )注意statusMap字段存储为 JSON 字符串而非关联表是 A306 的关键权衡。它避免了 N1 查询问题一次读取即可获取整场点名结果缺点是无法用 SQL 直接统计“某学生缺勤次数”。对于课程设计规模单班 ≤ 60 人总记录 ≤ 1000 条JSON 方案更轻量、更易实现。2.2.2 创建 DAO 接口Data Access Object// dao/AttendanceDao.kt Dao interface AttendanceDao { Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insertRecord(record: AttendanceRecord): Long Query(SELECT * FROM attendance_record_table WHERE classId :classId AND date :date) suspend fun getRecordByClassAndDate(classId: Long, date: String): AttendanceRecord? Query(SELECT * FROM attendance_record_table WHERE classId :classId ORDER BY date DESC LIMIT 10) suspend fun getRecentRecords(classId: Long): ListAttendanceRecord }逻辑说明Insert(onConflict OnConflictStrategy.REPLACE)确保同一天对同一班级重复点名时旧记录被自动覆盖防止数据冗余。getRecentRecords限制返回 10 条避免在 RecyclerView 中加载过多历史数据导致卡顿。2.3 UI 层架构ViewBinding RecyclerView 自定义 Adapter 实现高效列表渲染A306 的主界面是“班级列表 → 学生列表 → 点名操作”的三级导航。RecyclerView 是唯一合理选择它复用 Item View内存占用低滚动流畅。ViewBinding 替代 findViewById杜绝空指针异常且 IDE 支持 XML ID 的智能提示。2.3.1 布局文件精简设计activity_main.xml!-- res/layout/activity_main.xml -- androidx.constraintlayout.widget.ConstraintLayout xmlns:androidhttp://schemas.android.com/apk/res/android xmlns:apphttp://schemas.android.com/apk/res-auto android:layout_widthmatch_parent android:layout_heightmatch_parent androidx.recyclerview.widget.RecyclerView android:idid/recyclerView android:layout_width0dp android:layout_height0dp app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toBottomOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / com.google.android.material.floatingactionbutton.FloatingActionButton android:idid/fabAddClass android:layout_widthwrap_content android:layout_heightwrap_content android:layout_margin16dp android:srcdrawable/ic_add app:layout_constraintBottom_toBottomOfparent app:layout_constraintEnd_toEndOfparent / /androidx.constraintlayout.widget.ConstraintLayout参数说明FloatingActionButton用于快速添加新班级符合 Material Design 规范ConstraintLayout保证布局在不同屏幕尺寸下稳定避免嵌套过深导致性能下降。2.3.2 自定义 ViewHolder 与 AdapterClassAdapter.kt// adapter/ClassAdapter.kt class ClassAdapter( private val onItemClick: (Long) - Unit ) : RecyclerView.AdapterClassAdapter.ClassViewHolder() { private val classes mutableListOfClassItem() inner class ClassViewHolder(val binding: ItemClassBinding) : RecyclerView.ViewHolder(binding.root) { fun bind(classItem: ClassItem) { binding.tvClassName.text classItem.name binding.tvStudentCount.text ${classItem.studentCount}人 binding.root.setOnClickListener { onItemClick(classItem.id) } } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ClassViewHolder { val binding ItemClassBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return ClassViewHolder(binding) } override fun onBindViewHolder(holder: ClassViewHolder, position: Int) { holder.bind(classes[position]) } override fun getItemCount() classes.size fun submitList(newList: ListClassItem) { classes.clear() classes.addAll(newList) notifyDataSetChanged() } }逻辑说明submitList方法采用全量刷新而非DiffUtil因课程设计数据量小班级数 ≤ 20notifyDataSetChanged()足够高效onItemClick是 Lambda 回调解耦 UI 与业务逻辑使 Activity 仅负责跳转不处理数据。3. 实现核心功能班级管理、学生录入与实时点名逻辑3.1 班级管理模块CRUD 操作与 Room 数据库联动A306 的起点是“创建班级”。用户输入班级名称后系统需在数据库中插入一条Class记录并立即更新 RecyclerView 列表。这要求 Activity 与 ViewModel 协作避免在 UI 线程直接操作数据库。3.1.1 定义 Class 实体与 DAO// data/Class.kt Entity(tableName class_table) data class Class( PrimaryKey(autoGenerate true) val id: Long 0, val name: String, val createdAt: Long System.currentTimeMillis() ) // dao/ClassDao.kt Dao interface ClassDao { Insert(onConflict OnConflictStrategy.IGNORE) suspend fun insertClass(classItem: Class): Long Query(SELECT * FROM class_table ORDER BY createdAt DESC) suspend fun getAllClasses(): ListClass Query(DELETE FROM class_table WHERE id :id) suspend fun deleteClass(id: Long) }注意onConflict OnConflictStrategy.IGNORE防止用户重复创建同名班级时崩溃ORDER BY createdAt DESC确保新创建的班级始终显示在列表顶部符合用户直觉。3.1.2 ViewModel 封装数据操作ClassViewModel.kt// viewmodel/ClassViewModel.kt class ClassViewModel(private val classDao: ClassDao) : ViewModel() { private val _classes MutableLiveDataListClass() val classes: LiveDataListClass _classes init { loadClasses() } private fun loadClasses() { viewModelScope.launch { _classes.value classDao.getAllClasses() } } fun addClass(name: String) { if (name.isBlank()) return viewModelScope.launch { val newClass Class(name name.trim()) classDao.insertClass(newClass) loadClasses() // 重新加载触发 UI 更新 } } fun deleteClass(id: Long) { viewModelScope.launch { classDao.deleteClass(id) loadClasses() } } }逻辑说明viewModelScope.launch确保所有数据库操作在后台协程执行不阻塞主线程loadClasses()被addClass和deleteClass共用保证数据一致性避免手动postValue导致的竞态条件。3.2 学生录入模块批量导入与手动添加双路径实际教学场景中学生名单常以 Excel 或 TXT 提供。A306 支持两种方式① 手动逐条添加适合小班② 从 SD 卡读取 CSV 文件批量导入格式学号,姓名,性别。这涉及存储权限申请与文件解析。3.2.1 动态申请存储权限Android 10 适配Android 10API 29起READ_EXTERNAL_STORAGE权限需在AndroidManifest.xml中声明并在运行时请求!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion28 / uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES / uses-permission android:nameandroid.permission.READ_MEDIA_AUDIO /提示android:maxSdkVersion28表示该权限仅对 Android 9 及以下生效Android 10 使用Scoped Storage需通过StorageManager访问公共目录或使用ActivityResultLauncher获取用户选择的文件 URI。3.2.2 CSV 解析核心逻辑CsvImporter.kt// utils/CsvImporter.kt object CsvImporter { suspend fun importFromUri( context: Context, uri: Uri, classId: Long, studentDao: StudentDao ): Int { return withContext(Dispatchers.IO) { val inputStream context.contentResolver.openInputStream(uri) ?: throw IOException(无法打开文件流) val reader BufferedReader(InputStreamReader(inputStream)) var count 0 reader.use { br - var line: String? while (br.readLine().also { line it } ! null) { if (line!!.isBlank() || line!!.startsWith(#)) continue // 跳过空行和注释 val parts line!!.split(,).map { it.trim() } if (parts.size 2) { val student Student( classId classId, studentNumber parts[0], name parts[1], gender if (parts.size 2) parts[2] else 未知 ) studentDao.insertStudent(student) count } } } count } } }参数说明parts[0]为学号强制字段parts[1]为姓名强制字段parts[2]为性别可选count返回成功导入的学生数用于 UI 提示。3.3 实时点名模块状态切换与本地持久化点名界面的核心是“点击学生姓名切换出勤状态到课/迟到/缺勤/请假”。状态需即时反映在 UI 上并在用户离开页面前保存到数据库。3.3.1 状态枚举与 UI 映射// model/AttendanceStatus.kt enum class AttendanceStatus(val code: String, val colorRes: Int) { PRESENT(present, R.color.status_present), LATE(late, R.color.status_late), ABSENT(absent, R.color.status_absent), EXCUSED(excused, R.color.status_excused); companion object { fun fromCode(code: String): AttendanceStatus values().find { it.code code } ?: PRESENT } }逻辑说明colorRes关联res/values/colors.xml中预定义的颜色资源使状态色块可统一管理fromCode提供字符串到枚举的安全转换避免 JSON 解析失败。3.3.2 点名状态保存逻辑AttendanceManager.kt// manager/AttendanceManager.kt class AttendanceManager(private val attendanceDao: AttendanceDao) { suspend fun saveAttendance( classId: Long, date: String, statusMap: MapString, AttendanceStatus ) { val json Gson().toJson(statusMap.mapValues { it.value.code }) val record AttendanceRecord( classId classId, date date, statusMap json ) attendanceDao.insertRecord(record) } suspend fun loadAttendance( classId: Long, date: String ): MapString, AttendanceStatus { val record attendanceDao.getRecordByClassAndDate(classId, date) ?: return emptyMap() return try { val map Gson().fromJson(record.statusMap, object : TypeTokenMapString, String() {}.type) map.mapValues { AttendanceStatus.fromCode(it.value) } } catch (e: Exception) { emptyMap() } } }注意Gson用于 JSON 序列化/反序列化需在app/build.gradle中添加依赖implementation com.google.code.gson:gson:2.10.1try-catch捕获 JSON 解析异常防止因数据损坏导致整个点名页崩溃。4. 导出与统计功能生成 CSV 报表与可视化图表4.1 导出点名记录为 CSV 文件适配 Android 10 分区存储导出功能需将AttendanceRecord数据转换为标准 CSV 格式并保存到应用专属目录getExternalFilesDir(null)确保无需额外权限即可访问且卸载 App 时文件自动清理。4.1.1 CSV 生成逻辑CsvExporter.kt// utils/CsvExporter.kt object CsvExporter { fun exportToCsv( context: Context, records: ListAttendanceRecord, students: MapString, String, // 学号 - 姓名 映射 fileName: String ): File? { return try { val file File(context.getExternalFilesDir(null), $fileName.csv) val writer FileWriter(file) // 写入 CSV 头 writer.append(日期,学号,姓名,状态\n) // 写入每条记录 for (record in records) { val statusMap Gson().fromJson( record.statusMap, object : TypeTokenMapString, String() {}.type ) for ((studentNumber, statusCode) in statusMap) { val name students[studentNumber] ?: 未知 writer.append(${record.date},$studentNumber,$name,$statusCode\n) } } writer.close() file } catch (e: IOException) { null } } }参数说明students参数是MapString, String由StudentDao查询所有学生学号-姓名映射一次性传入避免在循环中反复查询getExternalFilesDir(null)返回路径如/storage/emulated/0/Android/data/com.example.a306rollcall/files/此路径无需权限且对用户可见可通过文件管理器访问。4.1.2 触发导出并分享文件Activity 中// MainActivity.kt private fun exportAndShare() { val file CsvExporter.exportToCsv( this, recentRecords, // 已加载的记录列表 studentNameMap, // 学号-姓名映射 A306_点名记录_${System.currentTimeMillis()} ) if (file ! null file.exists()) { val uri FileProvider.getUriForFile( this, ${packageName}.fileprovider, file ) val intent Intent(Intent.ACTION_SEND).apply { type text/csv putExtra(Intent.EXTRA_STREAM, uri) putExtra(Intent.EXTRA_SUBJECT, A306点名系统导出报表) } startActivity(Intent.createChooser(intent, 分享报表)) } }注意FileProvider是 Android 7.0 强制要求的文件共享机制需在AndroidManifest.xml中声明 provider并在res/xml/file_paths.xml中配置路径。4.2 统计报表用 MPAndroidChart 绘制出勤率折线图单纯列表难以直观展示趋势。A306 集成MPAndroidChart库绘制“近 7 天班级出勤率变化”折线图提升答辩专业度。4.2.1 添加依赖与初始化图表build.gradle// app/build.gradle dependencies { implementation com.github.PhilJay:MPAndroidChart:v3.1.0 }4.2.2 构建数据集ChartHelper.kt// utils/ChartHelper.kt object ChartHelper { fun createAttendanceLineChart( context: Context, records: ListAttendanceRecord, totalStudents: Int ): LineData { val entries mutableListOfEntry() val dates mutableListOfString() var index 0 for (record in records.sortedBy { it.date }) { val statusMap Gson().fromJson( record.statusMap, object : TypeTokenMapString, String() {}.type ) val presentCount statusMap.values.count { it present } val rate if (totalStudents 0) presentCount * 100f / totalStudents else 0f entries.add(Entry(index.toFloat(), rate)) dates.add(record.date.substring(5)) // 取 MM-dd 显示 index } val dataSet LineDataSet(entries, 出勤率 (%)) dataSet.setColors(ContextCompat.getColor(context, R.color.chart_line)) dataSet.lineWidth 2f dataSet.circleRadius 4f dataSet.setCircleColor(ContextCompat.getColor(context, R.color.chart_circle)) return LineData(dataSet).apply { setValueTextSize(10f) } } }逻辑说明sortedBy { it.date }确保日期顺序正确rate计算为百分比totalStudents传入避免重复查询setValueTextSize(10f)优化小屏设备文字可读性。5. 调试与发布技巧规避常见编译错误与真机调试陷阱5.1 解决 Android Studio 常见构建失败tag number over 30 is not supported这是 Gradle 7.0 与旧版 AGPAndroid Gradle Plugin不兼容的典型报错多见于从老项目迁移或使用过时模板。根本原因是build.gradle中compileSdk与targetSdk版本过高而 AGP 版本未同步升级。5.1.1 正确的版本匹配表截至 2024 年AGP 版本对应 Gradle 版本支持的 compileSdk8.2.28.2348.1.48.1337.4.27.533操作步骤打开gradle/wrapper/gradle-wrapper.properties修改distributionUrl为https\://services.gradle.org/distributions/gradle-8.2-bin.zip再打开项目根目录build.gradle或build.gradle.kts将com.android.tools.build:gradle升级至8.2.2。同步后 Clean Project → Rebuild。5.2 真机调试避坑Vivo/Oppo/华为手机无线调试失效国产厂商定制 ROM 常禁用 ADB 无线调试。A306 需确保能在实训平板上调试推荐以下三步法开启开发者选项设置 → 关于手机 → 连续点击“版本号”7 次启用 USB 调试与网络调试设置 → 系统与更新 → 开发者选项 → 启用“USB 调试”、“USB 调试安全设置”、“网络调试”手动绑定 IP 与端口# 电脑端先连接手机 USB执行 adb tcpip 5555 # 断开 USB查看手机 Wi-Fi IP设置 → WLAN → 点击当前网络 → 查看 IP adb connect 192.168.1.100:5555 # 替换为实际 IP提示若adb connect失败检查手机是否开启“允许通过网络调试”部分品牌叫“ADB over Network”并确认电脑与手机在同一局域网。5.3 APK 体积优化移除无用资源与启用代码压缩毕业设计 APK 不必追求极致精简但应避免包含debug信息或未使用的图片资源否则安装包过大50MB易被老师质疑工程规范性。5.3.1 启用 R8 代码压缩app/build.gradleandroid { buildTypes { release { signingConfig signingConfigs.release minifyEnabled true // 启用代码压缩 shrinkResources true // 启用资源压缩 proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } }5.3.2 检查并清理 res 目录删除res/drawable-xxxhdpi/下的冗余图标保留drawable-mdpi和drawable-hdpi即可将 PNG 图片用pngcrush或在线工具压缩减少体积移除res/raw/中未引用的音频/视频文件。验证方法Build → Analyze APK → 查看resources.arsc和classes.dex大小优化后 release APK 应 ≤ 8MB。本文还有配套的精品资源点击获取