十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android教学管理系统开发:从Room数据库到MVVM架构与定位签到实践

Android教学管理系统开发:从Room数据库到MVVM架构与定位签到实践 简介一份基于Android的教学管理系统设计与实现文档主要面向高校计算机相关专业学生及需要完成教学管理类毕业设计或课程设计的开发者。文档从系统背景、开发工具、总体设计到功能模块展开设计语言采用Java结合Android客户端与后台管理系统整合教师管理、班级管理、科目管理、签到、作业、反馈、信息查询等功能配套MySQL数据库支持能够帮助读者快速理解教学管理系统的业务框架和实现思路。资源包内为1个docx文件大小约56KB属于Word格式论文文稿适合直接阅读、修改和归档便于在Word中进行批注与二次编辑。目前已有45人学习下载内容紧凑精炼适合作为需求分析、模块划分、技术选型及论文排版方面的参考素材。文档详列了摘要、Abstract、目录及正文结构涵盖JDBC、Jquery、Ajax、Eclipse、MyEclipse、Tomcat、Android、MySQL等关键技术的介绍既有理论阐述又有系统功能说明可以在写论文或做项目演示时快速定位有效信息。1. 这个项目存在的真实场景为什么教学系统要单独做Android端期末周下午四点教务处的刘老师还在对着Excel核对三个班的补课签到单而班级群里已经被“老师今天点不点名”刷屏。这个场景在高校和企业内训里都很熟悉管理端要统计、学生端要即时反馈通用教务系统要么太笨重、要么根本没有移动端。基于Android教学管理系统的定位就是在系统边界上做减法学生端负责课表、成绩、公告与一键签到教师端负责发起签到和录入成绩管理员端管账号和汇总数据。它不是要替换教务系统而是把课堂内的高频动作搬进手机。这篇内容按做这类项目最常见的工程路径展开模块怎么拆、架构怎么选、数据层怎么写、签到防作弊怎么设计以及打包上真机前必须处理的三类问题。2. 架构设计先行从用例图到MVVM与Room选型很多同学拿到这个题目会直接新建一个Android空项目然后往MainActivity里堆代码。等写到底部导航、学生端教师端共用一套入口时Activity就会膨胀到上千行。做教学管理系统这类业务最值得花时间的是先确定角色边界和数据流。2.1 角色与用例一张权限矩阵降低一半返工先把需求拆成三个角色学生、教师、管理员。它们共用同一个App进程但入口和可见内容不同。下面这张权限矩阵在需求评审阶段就能拦住至少一半的“能不能顺便加个功能”需求。角色核心功能默认数据范围写权限学生课表查询、课堂签到、查看成绩、接收公告仅本人选课数据仅发起签到确认教师发布课程、发起签到、录入成绩、发送公告本人任课班级可增删改课程与成绩管理员账号管理、课程开班、导出签到统计全部学生与教师账号可禁用账号、重置密码用例不需要画得很标准但要清楚一点课表查询和签到是读写最频繁的模块必须支持离线基本可用成绩录入和公告发布是低频模块可以放在网络请求失败时提示“稍后重试”。我会把高频模块的数据放在本地Room里低频模块走Retrofit直接拉取网络数据。2.2 技术选型为什么选MVVM而不是Activity堆逻辑MVC、MVP、MVVM三者的对比在这个项目里最直观的体现就是“签到状态变化后谁去更新界面”。架构界面更新方式适合规模坑点MVCActivity既是Controller又是View界面少于5个Activity膨胀无法单元测试MVPPresenter回调View接口中等项目接口文件太多状态容易泄漏MVVMLiveData/Flow自动通知本系统规模需要理解数据绑定和生命周期MVVM在这个系统里更合适。Room返回的Flow可以直接在ViewModel里做数据转换再通过LiveData暴露给Activity链路短且不依赖Activity销毁。Jetpack对生命周期做了托管旋转屏幕时不会重复请求数据库。2.3 Android Studio工程结构与Gradle参数在Android Studio里新建项目时我用“Empty Views Activity”而不是“Empty Activity”因为新版AS对Compose的默认导入会增加大量依赖而这个系统里业务逻辑更适合传统View体系。分包结构固定为data、ui、viewmodel、common四层资源文件按下拉刷新、签到、课程表分组命名。plugins { id com.android.application id org.jetbrains.kotlin.android id kotlin-kapt } android { namespace com.example.teachassist compileSdk 33 defaultConfig { applicationId com.example.teachassist minSdk 23 targetSdk 33 versionCode 1 versionName 1.0 testInstrumentationRunner androidx.test.runner.AndroidJUnitRunner } buildFeatures { viewBinding true } compileOptions { sourceCompatibility JavaVersion.VERSION_11 targetCompatibility JavaVersion.VERSION_11 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1 implementation androidx.room:room-runtime:2.5.2 implementation androidx.room:room-ktx:2.5.2 kapt androidx.room:room-compiler:2.5.2 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 }compileSdk 33对应Android 13的新特性targetSdk保持33可以避免分区存储和前台服务类型等兼容问题。minSdk 23覆盖了绝大多数在校生使用的手机也省去了动态权限适配的低版本分支。viewBinding开启后在Activity里就少写findViewById也能避免adapter里手写ViewHolder时把布局文件传错。提示Android Studio汉化包不影响编译但gradle配置和报错信息里的路径最好保持英文否则复制报错到搜索引擎时会被字体转义干扰。3. 数据层落地用Room把课表、用户与签到记录管理起来教学管理系统的数据模型不复杂但表间关系繁琐用户表、课程表、选课表、签到表、公告表、成绩表。如果继续用SQLiteOpenHelper手写SQL开发周期会被建表、升级、读写线程切换拖慢。Room是Android官方在SQLite之上封装的ORM核心优势是编译期校验SQL语句和返回LiveData/Flow。3.1 从SQLiteOpenHelper到Room不是炫技是省心手写SQLiteOpenHelper的典型问题是字符串拼接SQL容易在运行期崩溃版本升级时要写上百行的onUpgrade迁移代码查询结果必须手动封装成Bean。Room通过注解生成实现类写错的表名在编译期就会暴露。数据处理链路从“SQLiteCursor转Java对象”简化为“Dao直接返回实体类”。3.2 Entity、Dao与Database的实现这里的实体类我只列出Course、User、SignRecord的典型写法。Entity(tableName course) data class Course( PrimaryKey(autoGenerate true) val id: Long 0, val name: String, val teacher: String, val classroom: String, val dayOfWeek: Int, val startSection: Int, val endSection: Int, val semester: String, ColumnInfo(name created_at) val createdAt: Long System.currentTimeMillis() )字段里dayOfWeek保存周几startSection和endSection保存第几节到第几节这样按天查询课表时只需一个索引。createdAt在调试时很有用可以用来排查数据覆盖问题。Dao interface CourseDao { Query(SELECT * FROM course WHERE semester :semester AND dayOfWeek :day ORDER BY startSection) fun getCoursesByDay(semester: String, day: Int): FlowListCourse Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insertCourse(course: Course) Query(DELETE FROM course WHERE id :courseId) suspend fun deleteCourse(courseId: Long) }Query返回值用Flow而不是List能让界面在数据更新时自动收到通知。insertCourse是挂起函数调用时必须在协程环境里执行避免主线程写数据库产生的StrictMode警告。Database(entities [Course::class, User::class, SignRecord::class], version 1) abstract class AppDatabase : RoomDatabase() { abstract fun courseDao(): CourseDao abstract fun userDao(): UserDao abstract fun signRecordDao(): SignRecordDao companion object { Volatile private var instance: AppDatabase? null fun get(context: Context): AppDatabase instance ?: synchronized(this) { instance ?: Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, teach_assist.db ).fallbackToDestructiveMigration().build().also { instance it } } } }这个单例使用了双重检查锁定保证App进程内只有一个数据库实例。fallbackToDestructiveMigration是开发期过渡写法如果已经上线一定要把它替换成Migration代码否则升级后用户数据会被清空。3.3 用Repository统一数据来源ViewModel不直接操作CourseDao而是通过CourseRepository做中间层这样以后接后端时不需要改ViewModel。class CourseRepository(private val dao: CourseDao, private val api: TeachAssistApi) { fun coursesForDay(semester: String, day: Int): FlowListCourse dao.getCoursesByDay(semester, day) suspend fun syncFromRemote(token: String) { val remoteCourses api.fetchCourses(token) dao.insertCourses(remoteCourses) } }首次进入课表页时先展示本地数据库里的数据再启动协程调用syncFromRemote拉取远程数据并覆盖本地。这样即使在学校隧道网络里也至少能看到上节课缓存的课表。这个设计正好对应Android官方的Repository模式测试时只需mock掉api接口不用启动Activity。4. 核心业务实现登录Token、课程表展示与定位签到这一章把系统里最常被问到的三块代码捋一遍。登录决定数据权限课程表决定页面骨架签到决定系统有没有实际使用价值。4.1 登录模块与Token持久化后端接口用Retrofit定义协议尽量简单返回体固定为{code, msg, data}。data class LoginRequest(val username: String, val password: String) data class LoginResponse(val code: Int, val msg: String, val data: TokenData?) data class TokenData(val token: String, val role: Int, val userId: Long)ApiService定义interface AuthApi { POST(auth/login) suspend fun login(Body body: LoginRequest): LoginResponse }ViewModel里做登录状态管理class LoginViewModel(private val repository: AuthRepository) : ViewModel() { private val _uiState MutableLiveDataLoginUiState() val uiState: LiveDataLoginUiState _uiState fun login(username: String, password: String) { viewModelScope.launch { _uiState.value LoginUiState.Loading try { val resp repository.login(username.trim(), password) if (resp.code 0 resp.data ! null) { SessionManager.saveToken(resp.data.token) SessionManager.saveRole(resp.data.role) _uiState.value LoginUiState.Success(resp.data.role) } else { _uiState.value LoginUiState.Error(resp.msg) } } catch (e: IOException) { _uiState.value LoginUiState.Error(网络异常请检查服务器地址) } } } }viewModelScope意味着协程随ViewModel生命周期取消退出登录页后不会回调界面。Token用SharedPreferences保存还是DataStore保存建议看后端接口覆盖范围如果只有登录态判定SharedPreferences已经够用。接口地址在接入高德地图时会用到应用签名SHA1值这个值可以在Android Studio的Gradle面板里运行signingReport任务拿到配置时要保证debug和release签名分开。4.2 课程表展示从数据库到RecyclerView课程表页面用RecyclerView按天展示适配器直接观察Flow数据。class CourseAdapter( private val onClick: (Course) - Unit ) : ListAdapterCourse, CourseAdapter.CourseViewHolder(CourseDiffCallback()) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CourseViewHolder { val binding ItemCourseBinding.inflate(LayoutInflater.from(parent.context), parent, false) return CourseViewHolder(binding, onClick) } override fun onBindViewHolder(holder: CourseViewHolder, position: Int) { holder.bind(getItem(position)) } class CourseViewHolder( private val binding: ItemCourseBinding, private val onClick: (Course) - Unit ) : RecyclerView.ViewHolder(binding.root) { fun bind(course: Course) { binding.tvCourseName.text course.name binding.tvClassroom.text ${course.teacher} · ${course.classroom} binding.root.setOnClickListener { onClick(course) } } } }Adapter里用了DiffUtil避免每次更新课表时重新刷全量列表。界面从数据库取出数据后在ViewModel里做一次asLiveData()转换Activity里用repeatOnLifecycle收集可以防止页面不可见时重复刷新课程表。4.3 基于定位的课堂签到与防作弊签到是教学系统里最容易出现“代签”的模块。做法是教师端在后台设置一个有效签到范围和一个时间窗口学生端获取定位后计算与教学楼的直线距离。val fusedLocationClient LocationServices.getFusedLocationProviderClient(context) if (ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED ) { requestPermissionLauncher.launch(Manifest.permission.ACCESS_FINE_LOCATION) return } fusedLocationClient.lastLocation.addOnSuccessListener { location - val distance calculateDistance( location.latitude, location.longitude, signInTargetLat, signInTargetLng ) if (distance signInRadiusMeters isInTimeWindow()) { viewModel.submitSignIn(courseId, location) Toast.makeText(context, 签到成功, Toast.LENGTH_SHORT).show() } else { Toast.makeText(context, 不在签到范围或时间窗口内, Toast.LENGTH_SHORT).show() } }calculateDistance可以用半正矢公式也可以用高德地图sdk的AMapUtils.calculateLineDistance后者更省心但接入前要按官网文档配置应用签名SHA1值否则定位会一直回调失败。签到参数建议按学期设置表预先配置好。参数建议值说明签到半径50-200米50米适合小教室200米适合阶梯教室或跨楼栋时间窗口课前5分钟到课后5分钟避免学生提前一天占位补签次数1次超过需要走教师审批流程刷新间隔30秒防止刷接口刷签到别忘了Android 10之后的模糊定位权限。targetSdk 33时如果只需要判断在学校内ACCESS_COARSE_LOCATION已经够用申请时优先说明是“位置服务”而不是“精确定位”过审和用户通过率都会高一些。界面在提交过程中还要有一个不可点击的加载状态比如显示一个Android进度条控件避免用户连续点击造成重复入库。5. 上线前检查清单无线调试、混淆与Android 12适配最后一章写给已经跑通核心流程、准备把安装包发给同学做内测的开发。按这三步走完能避开大部分“在我手机上运行正常在别人手机上闪退”的问题。5.1 用Android Debug Bridge做真机联调模拟器覆盖不了定位和通知权限场景习惯直接插真机调试。先用Android Studio菜单里的“Pair using Wi-Fi”连一次无线调试对应Android Debug Bridge的命令是adb devices adb connect 192.168.1.100:5555 adb logcat -v time *:E adb reverse tcp:8080 tcp:8080adb reverse把手机上的8080端口反向转发到电脑的8080本地后端部署时很有用不需要让手机和电脑处在同一个可直连的网络。如果应用在老师手机上启动时闪退先执行adb logcat -v time *:E抓一条致命日志九成都能看到Caused by那一行。5.2 多环境配置与代码混淆线上后端和本地联调不是同一个地址在buildTypes里用resValue区分不要写在Java代码里。buildTypes { debug { resValue string, api_base_url, http://10.0.2.2:8080 } release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro resValue string, api_base_url, https://api.example.edu.cn } }混淆规则里给Room的实体类加上keep否则反射读取表结构时会报ClassNotFoundException。注意拥挤的混淆规则坚持写清楚用途不然排查问题时会浪费一整个下午。-keep class com.example.teachassist.data.entity.** { *; } -keep class androidx.room.** { *; } -dontwarn okhttp3.**5.3 Android 11/12分区存储与FileProvider适配targetSdk 30之后应用不能直接访问外部存储根目录下其他应用的包路径。以前常见的/storage/emulated/0/Android/data/...目录被系统隔离后导出课程表Excel附件时只能走FileProvider。先在AndroidManifest里注册FileProviderprovider android:nameandroidx.core.content.FileProvider android:authorities${applicationId}.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /providerres/xml/file_paths.xml内容paths external-files-path nameexports pathexports/ / /paths这样导出的成绩单URI格式类似于content://com.example.teachassist.fileprovider/exports/grade_2024.csv而不是暴露真实文件绝对路径。另一个坑是Android 12对前台服务的foregroundService类型有限制如果签到后台需要持续汇报位置要在Manifest里声明android:foregroundServiceTypelocation并在启动服务时传入对应的类型常量。最后用一个调试技巧收尾真机验证时把开发者选项里的“不保留活动”打开反复切换应用重点观察签到页面在Activity重建后是否还能恢复选课状态。本文还有配套的精品资源点击获取
返回列表