ARTICLE DETAIL

资讯详情

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

基于Android的信息化医疗服务系统开发实战:架构与数据流

基于Android的信息化医疗服务系统开发实战:架构与数据流 简介面向Android移动开发与高校课程设计场景这套信息化医疗服务系统源自大三课程设计项目包含患者端App、SpringBoot与jFinal实现的后端接口以及简化版Web管理后台整体架构清晰适合需要医疗类项目参考的学生和开发者。压缩包共1106个文件约74.32MB涵盖Android工程中以java、xml、json为代表的源码与资源文件后端SpringBoot与jFinal的Java接口代码以及Web端js/html/css页面另附约13000字、60页Word课程报告按论文格式编写并保留了构建所需的gradle、jar等配置。目前已有1000余人学习下载对于课程设计场景具备较强的参考意义。读者可从中获取完整源码、后端数据接口设计与课程报告既可作为毕业设计或课程设计的参考框架也能在此基础上扩展功能、准备答辩与参赛非常适合医疗信息化方向的实践学习。1. 先搞清楚你手里的Android信息化医疗服务系统要做成什么样如果你在毕设选题或实训工程里拿到一个名为「基于Android的信息化医疗服务系统的设计与实现.zip」的工程包先别急着往Android Studio里拖。这类移动医疗课题的真实工作量往往不在登录页和首页宫格而在挂号排班的数据一致性、问诊记录的状态流转以及App端与后端接口的约定上。换句话说界面只是外壳数据流才是要讲清楚的东西。这套系统面向三类角色患者用手机完成登录、找医院、挂号、查报告医生侧看到排班与患者记录管理员维护科室和医生信息。适合的人群也很明确第一次独立做完整App工程的在校生以及想快速接手医疗类项目的初级Android工程师。文章按可交付、可答辩的路径展开从选型、骨架、业务落地讲到最后联调与验收。2. 技术选型与Android工程骨架为什么这套组合能撑起医疗业务把技术栈定下来比先写代码重要得多。一个医疗信息化课题如果选错了容器后面所有页面都要返工。这里说的「容器」不只是编程语言还包括架构模式和数据落地方式。下面这套组合是这类课题最常见、也最容易在答辩时讲清楚的方案Android原生开发Kotlin作为主语言MVVM作为分层思想配合Room做本地缓存、Retrofit做网络请求。2.1 为什么是Android原生而非跨端框架课题答辩时最常被追问的一个问题是「你为什么不用uni-app一套代码跑微信小程序和App」。我的回答一般分三层。第一层这套系统不是展示型页面它涉及本地数据库缓存、文件分享、后台任务和后续可扩展的蓝牙设备接入这些能力在跨端框架里要么要写原生插件要么被框架抽象层挡了一道。第二层Android/iOS/鸿蒙三端同时维护对一个小团队是灾难单平台的Android原生可以保证一个人在一个学期内交付。第三层数据流可解释性强从界面到数据库的每一层都能落成代码讲给老师听。跨端方案的另一个问题是调试链路。Android原生可以用Android Studio的Profiler看内存、用Logcat看日志、用布局检查器看层级出问题时定位路径短。医疗系统有一个特殊性业务链条长从点击按钮到写库可能要跨五个方法调试工具链越直接排错成本越低。所以这个课题我始终坚持原生方案不追求「一套代码到处跑」。2.2 Gradle依赖与SDK版本先把工程地基铺平新建工程时选Empty Activity模板包名建议用com.example.hospital这类可读性强的结构后面混淆规则、FileProvider的authorities都要依赖它。SDK版本这里有个原则minSdk决定你能覆盖多少老设备targetSdk决定系统以多严格的方式来限制你。医院场景里还存在大量Android 7.0到9.0的设备所以minSdk放24比较现实targetSdk则跟着当前主流版本走避免新机型上分区存储、后台限制这类问题爆出来。plugins { id com.android.application id org.jetbrains.kotlin.android id kotlin-kapt } android { namespace com.example.hospital compileSdk 35 defaultConfig { applicationId com.example.hospital minSdk 24 targetSdk 35 versionCode 1 versionName 1.0.0 } buildFeatures { viewBinding true } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } } dependencies { implementation androidx.core:core-ktx:1.13.1 implementation androidx.appcompat:appcompat:1.7.0 implementation com.google.android.material:material:1.12.0 implementation androidx.constraintlayout:constraintlayout:2.1.4 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4 implementation androidx.datastore:datastore-preferences:1.1.1 implementation androidx.room:room-runtime:2.6.1 implementation androidx.room:room-ktx:2.6.1 kapt androidx.room:room-compiler:2.6.1 implementation com.squareup.retrofit2:retrofit:2.11.0 implementation com.squareup.retrofit2:converter-gson:2.11.0 implementation com.squareup.okhttp3:okhttp:4.12.0 implementation com.squareup.okhttp3:logging-interceptor:4.12.0 implementation com.github.bumptech.glide:glide:4.16.0 implementation io.github.youth5201314:banner:2.2.3 implementation androidx.viewpager2:viewpager2:1.1.0 }几个参数说明一下。compileSdk用35是因为新版本Android对分区存储和前台服务的限制越来越严格编译SDK太低连AAR里的API都调不了。kotlin-kapt要带上Room的编译期代码生成依赖它。DataStore用来替代SharedPreferences存登录Token它基于协程天然适合Kotlin的suspend函数。Banner库我用的2.2.3它内部基于ViewPager2和后面CoordinatorLayout联动比较顺。版本号不一定非要追最新Android Studio模板里如果提示依赖更新不强制升因为医疗系统这类课题更看重「在你电脑上能一遍编译过」。工程一建好先做一件事用Git提交一个空基线。Android Studio里配置好Git之后右键项目选Git Add然后Commit这个基线就是你整个开发周期的后悔药。后面改挂了直接回滚比到处找备份强得多。另外如果觉得英文界面不适应Android Studio首选项里可以切中文但我建议保留英文因为报错日志、官方迁移文档和你能搜到的解决方案几乎都是英文关键词切换语言反而增加对不上号的概率。2.3 底部导航与首页容器CoordinatorLayoutBanner的骨架实现病人打开App第一眼是首页结构上分三块顶部可折叠的Banner轮播、中间功能九宫格入口、下面常用医院列表。这里最常用的容器组合是CoordinatorLayout套AppBarLayout再在AppBarLayout里放CollapsingToolbarLayout和ViewPager2。CoordinatorLayout的价值是让折叠头布局和下面RecyclerView的滚动联动起来网上滑Banner折叠往下拉Banner展开这个交互就是热词里「协调布局Banner」的典型场景。androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_widthmatch_parent android:layout_heightmatch_parent com.google.android.material.appbar.AppBarLayout android:layout_widthmatch_parent android:layout_heightwrap_content com.google.android.material.appbar.CollapsingToolbarLayout android:layout_widthmatch_parent android:layout_height200dp app:layout_scrollFlagsscroll|exitUntilCollapsed androidx.viewpager2.widget.ViewPager2 android:idid/bannerPager android:layout_widthmatch_parent android:layout_height200dp / androidx.appcompat.widget.Toolbar android:idid/toolbar android:layout_widthmatch_parent android:layout_height?attr/actionBarSize app:layout_collapseModepin / /com.google.android.material.appbar.CollapsingToolbarLayout /com.google.android.material.appbar.AppBarLayout androidx.recyclerview.widget.RecyclerView android:idid/homeList android:layout_widthmatch_parent android:layout_heightmatch_parent app:layout_behaviorstring/appbar_scrolling_view_behavior / com.google.android.material.bottomnavigation.BottomNavigationView android:idid/bottomNav android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_gravitybottom / /androidx.coordinatorlayout.widget.CoordinatorLayout这段布局里最关键的是app:layout_scrollFlagsscroll|exitUntilCollapsed和RecyclerView上的app:layout_behaviorstring/appbar_scrolling_view_behavior。前者告诉AppBarLayout滚动时要折叠但保留Toolbar后者告诉CoordinatorLayout让下面RecyclerView的滚动事件去驱动上面的折叠动画。首页列表我通常用GridLayoutManager实现两列宫格作为挂号、问诊、健康档案、报告查询的功能入口这比静态九宫格图片更规范因为每个入口本身就是一个可点击的列表项。底部导航用BottomNavigationView配合FragmentContainerView三个tab分别放首页、问诊、我的。这里有个顺序问题先搭底部导航和首页容器再往里填业务代码。骨架稳定了后面的挂号流程、问诊记录都只是往对应Fragment里加布局和数据源。首页首屏加载时需要一个进度条反馈做法是在首页ViewModel里用MutableStateFlowBoolean控制加载状态列表数据回来前显示ProgressBar回来后隐藏。这个状态千万别写在Activity里否则旋转屏幕状态就丢了。3. Android端核心业务落地挂号、问诊与健康档案的数据流骨架搭好之后开始填最核心的三条业务链路登录与Token管理、挂号三级联动、健康档案和报告文件。这三条链路有一个共同点不能只做界面跳转每一层都要有数据落点和异常处理。我见过太多医疗课题只做到「点按钮跳页面」后台一问数据存在哪就答不上来所以这一章全部按「可被追问」的标准来讲。3.1 登录态与Token管理别让「登录成功」只是一次界面跳转登录接口返回的是一个Token字符串和一个用户基本信息对象。Token要持久化保存App每次启动时用它恢复登录态而不是让用户重新输入账号密码。我用DataStore而不是SharedPreferences因为DataStore读写是同步安全的并且在协程作用域里调用非常自然。// TokenStore.kt import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map private val Context.dataStore by preferencesDataStore(name session) class TokenStore(private val context: Context) { companion object { private val TOKEN_KEY stringPreferencesKey(token) } suspend fun saveToken(token: String) { context.dataStore.edit { prefs - prefs[TOKEN_KEY] token } } val tokenFlow: FlowString? context.dataStore.data.map { prefs - prefs[TOKEN_KEY] } suspend fun clearToken() { context.dataStore.edit { it.remove(TOKEN_KEY) } } }这段代码的逻辑是preferencesDataStore创建一个名为session的DataStore实例stringPreferencesKey定义Token字段的键名tokenFlow以Flow形式暴露Token界面层可以用repeatOnLifecycle收集它来判断「是否已登录」。参数上注意两点DataStore的name一旦定下来不要随便改否则旧数据读不到clearToken()在退出登录和Token失效两个场景都要调用。Token怎么用呢网络请求拦截器统一把Token加到Authorization头里后端根据这个头识别用户。如果接口返回401拦截器捕获后发送一个「登录过期」事件App回到登录页。这里要避免一个反模式每次请求失败都弹Toast提示「登录过期」正确做法是只在401时弹一次其他错误码走各自的业务提示。3.2 挂号流程实现医院、科室、排班三级联动的状态控制挂号是整个系统最核心的流程交互上分四步选医院、选科室、选医生、选时间槽。这四步是典型的级联数据关系后一层的列表依赖前一层的选中项。常见错误是一进入页面就把所有医院、科室、医生一次性查出来数据量一大App就卡。正确做法是每一层独立的接口选中上一层后再请求下一层。// AppointmentRepository.kt class AppointmentRepository(private val api: HospitalApi) { suspend fun getHospitals(): ListHospital { return api.getHospitals().data } suspend fun getDepartments(hospitalId: Int): ListDepartment { return api.getDepartments(hospitalId).data } suspend fun getDoctors(departmentId: Int): ListDoctor { return api.getDoctors(departmentId).data } suspend fun getSchedules(doctorId: Int, date: String): ListSchedule { return api.getSchedules(doctorId, date).data } suspend fun createAppointment( doctorId: Int, scheduleId: Int, patientId: Int ): AppointmentResult { return api.createAppointment( AppointmentRequest( doctorId doctorId, scheduleId scheduleId, patientId patientId ) ).data } }这四个方法对应着四级界面的数据源。注意getSchedules把date作为参数而不是在接口里写死当天因为排班要支持「明天」「后天」切换。界面上每次切换层级时用viewModelScope.launch发起请求加载期间给列表区域盖一个进度条iOS式的中转圈也行但Android上更推荐用列表自身的SwipeRefreshLayout配合isRefreshing状态这样用户知道数据正在刷新。时间槽用FlowRow或RecyclerView的多列网格展示每个槽位显示时间段和剩余号源数0号源置灰不可点。这里还有一个隐藏的坑创建订单时后端要判断「这个号是否刚被别人挂走」。为了演示体验前端通常会做防重复点击按钮点击后立刻置灰isSubmitting状态为true时拒绝再次提交。状态放在ViewModel里而不是Activity局部变量因为旋转屏幕时Activity重建ViewModel还活着提交状态不会丢。课题里把这个细节讲出来比多做一个列表页加分得多。3.3 健康档案与报告文件Room缓存与FileProvider分享健康档案和报告查询是医疗系统区别于普通电商App的地方。体检报告、检查单这些数据有两个特点实时从后端拉取慢而且用户希望断网时也能看到历史记录。所以本地用Room做缓存保存最近一次拉取的数据。Room是Android官方的ORM框架编译期生成SQL语句避免手写SQLiteOpenHelper那一堆样板代码。// HealthProfile.kt import androidx.room.Entity import androidx.room.PrimaryKey Entity(tableName health_profile) data class HealthProfile( PrimaryKey val userId: Int, ColumnInfo(name allergy) val allergy: String?, ColumnInfo(name blood_type) val bloodType: String?, ColumnInfo(name emergency_contact) val emergencyContact: String?, ColumnInfo(name updated_at) val updatedAt: Long )这个实体的userId既是主键也是对应用户ID。医疗数据有隐私属性App端只缓存当前登录用户的数据退出登录时要把这张表清掉别让下一个登录的人看到上一个用户的历史记录。updatedAt存的是时间戳界面上转成可读格式这样每次数据刷新后可以判断缓存是否过期。报告文件是PDF格式路径放在App私有外部目录的reports子目录下即getExternalFilesDir(reports)。有人图方便把报告直接写到/storage/emulated/0/Android/data/包名/files这个路径下本地看没问题但当你需要把报告分享给微信、QQ或其他系统应用时系统会因分区存储限制而读不到这个目录对方应用也没有权限打开你的内部数据。这个问题在Android 11以上的机型上几乎是必现的正确做法是用FileProvider把文件授权出去。!-- AndroidManifest.xml 的 application 节点内 -- provider 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 / /provider!-- res/xml/file_paths.xml -- paths external-files-path namereports pathreports/ / cache-path nameshared_cache pathcache/ / /pathsManifest里authorities用${applicationId}.fileprovider会自动替换成com.example.hospital.fileprovider这个值在分享时组成content://路径系统用它来定位提供者。file_paths.xml里external-files-path的pathreports/对应getExternalFilesDir(reports)目录。分享时拿到文件的Uri用FileProvider.getUriForFile(context, applicationId .fileprovider, file)生成再配上Intent.FLAG_GRANT_READ_URI_PERMISSION授权给对方应用。这套机制就是热词里content://那一长串路径的来历理解了FileProvider的配置任何应用之间互传文件的路数都一样。4. 服务端接口与Android联调URL约定、Token鉴权与JSON字段前端做得再漂亮没有接口就是空壳。这一章讲清楚两类问题一是接口本身该怎么设计让Android端开发起来不别扭二是联调过程中最常见的网络环境、明文流量、数据解析问题怎么处理。医疗系统的接口设计原则只有一个语义按资源走状态码分层处理。4.1 一套最小可用的挂号系统RESTful接口后端我用Spring Boot这类框架来对接但这章不展开后端代码只讲接口约定。约定先于实现前端和后端各写各的最后联调不吵架。接口统一返回一个JSON外壳{ code: 0, message: ok, data: { ... } }code为0表示成功非0表示业务失败HTTP状态码只承担传输层语义。方法路径说明POST/api/auth/login登录参数为账号密码返回Token和用户信息GET/api/hospital/list医院列表支持分页参数GET/api/hospital/{id}/department某医院下的科室列表GET/api/department/{id}/doctor某科室下的医生列表GET/api/doctor/{id}/schedule?date2025-06-01某医生某天的排班POST/api/appointment创建挂号订单参数含医生ID、排班ID、患者IDGET/api/appointment/my当前用户的挂号记录GET/api/report/{patientId}报告查询返回报告元数据列表URL里只出现名词不出现动词createAppointment的意义通过POST方法表达出来。路径中带ID的是资源嵌套关系/api/hospital/1/department表示医院1下的科室。时间参数统一用yyyy-MM-dd格式传字符串避免前后端时区不一致。错误码分三类1开头是参数错误2开头是权限问题3开头是业务冲突比如号源已被挂走。Android端拿到非0的code时直接读message展示给用户不要自己去拼错误文案。4.2 网络层统一封装Retrofit与OkHttp拦截器的职责划分Android端网络层我分成三个部分Retrofit定义接口、OkHttp拦截器处理Token与日志、Repository层调用并解析结果。很多初学者把网络请求直接写在Activity里接口一多就失控。正确的分层是Activity只跟ViewModel说话ViewModel调RepositoryRepository调Retrofit接口。// HospitalApi.kt interface HospitalApi { POST(api/auth/login) suspend fun login(Body body: LoginRequest): ApiResponseLoginResult GET(api/hospital/list) suspend fun getHospitals(): ApiResponseListHospital GET(api/hospital/{id}/department) suspend fun getDepartments(Path(id) hospitalId: Int): ApiResponseListDepartment GET(api/doctor/{id}/schedule) suspend fun getSchedules( Path(id) doctorId: Int, Query(date) date: String ): ApiResponseListSchedule POST(api/appointment) suspend fun createAppointment( Body body: AppointmentRequest ): ApiResponseAppointmentResult } // ApiResponse.kt data class ApiResponseT( val code: Int, val message: String, val data: T? )ApiResponseT的泛型T就是data字段的类型JSON反序列化时Gson会自动把data的内容转成ListHospital或AppointmentResult。这里有个关键前提后端的code、message、data字段名必须和这个数据类一致不一致就解析失败。所以联调的第一件事不是跑通页面而是先确认三个字段名。// AuthInterceptor.kt class AuthInterceptor(private val tokenProvider: () - String?) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val original chain.request() val token tokenProvider() val request original.newBuilder() .apply { if (!token.isNullOrBlank()) { addHeader(Authorization, Bearer $token) } } .build() return chain.proceed(request) } }拦截器的职责只有一个把Token加到请求头里。tokenProvider是一个Lambda每次请求时从TokenStore拿最新Token这样登录后不用重建Retrofit实例。OkHttp的chain.proceed(request)执行真正的网络请求如果后端返回401可以在拦截器里判断response.code() 401发一个事件或抛一个自定义异常由上层统一处理。不要把业务逻辑写进拦截器拦截器只处理传输层的事。创建Retrofit实例时记得加logging-interceptor设置level Level.BASIC这样可以看每个请求的URL、耗时和响应码但不会打印完整的响应体防止医疗数据在日志里泄露。Gson解析器要注册LocalDateTime适配器否则字段为空或序列化格式不一致时直接崩。4.3 真机联调三板斧adb reverse、明文流量与抓包过滤联调时最常见的问题是「手机连不上电脑的后端」。后端跑在电脑的8080端口手机和电脑连同一个局域网时要处理IP、防火墙、后端绑定地址三个问题很折腾。更省事的办法是USB连接后用adb做端口转发让手机上的App访问http://127.0.0.1:8080直接打到电脑的8080端口。# 电脑上先启动后端服务确认监听 8080 # 手机USB连接电脑并开启USB调试 adb reverse tcp:8080 tcp:8080 # 验证转发是否生效在手机浏览器打开 http://127.0.0.1:8080/api/hospital/list # 如果返回JSON说明电脑的后端已经能被手机访问这条命令把手机的本地8080端口转发到电脑的8080端口全程不依赖局域网和防火墙USB连着就能调。App的BASE_URL这时候写http://127.0.0.1:8080/即可。注意一点adb reverse只在USB连接期间有效拔线后要重新执行。如果你的后端自带前端页面也需要联调这招同样适用。第二个必踩的坑是Android 9以上默认禁止明文HTTP流量直接访问http://127.0.0.1:8080会报CLEARTEXT communication not permitted。临时解法是在AndroidManifest.xml的application节点加一行android:usesCleartextTraffictrue。但我更推荐用networkSecurityConfig因为把所有流量放开不安全尤其是医疗系统涉及患者数据上线前必须收紧。注意usesCleartextTraffic只适合开发阶段。正式发布前应删除或者用network-security-config只放行特定开发域名。抓包工具我用Charles这类图形化工具联调时只过滤hospital这个host把无关流量全部屏蔽定位问题快得多。抓包能看到三样东西请求URL是否拼对、请求头里的Token有没有带上、响应JSON结构和Android端数据类字段是否对得上。这三个问题解决了联调就成功了一大半。弱网场景可以打开Charles的Throttle设置模拟3G网络验证App在慢网下会不会白屏或崩溃这比答辩时被老师现场断网问住强得多。5. 避坑清单Android医疗APP从开发到交付的5个翻车现场这一章是血泪经验汇总。每条都按「现象 → 原因 → 解决」的结构写遇到同样问题可以直接对照排查。5个翻车现场覆盖了文件存储、混淆、数据库升级、布局嵌套和版本适配这些是医疗类Android项目交付前最容易出事的地方。5.1 写入/storage/emulated/0/Android/data后换机型就找不到文件现象把体检报告PDF保存到/storage/emulated/0/Android/data/包名/files/reports目录下自己手机上显示正常换一台Android 12的测试机后文件读不到分享时也提示路径不可用。原因Android 11起分区存储强制生效App访问公共目录和自身Android/data目录都受到限制其他应用也无法读取你的Android/data目录。就算你在自己设备上有文件管理的ROOT权限能看到不代表其他应用能通过content://协议访问它。解决将文件改存到context.getExternalFilesDir(reports)目录这个目录在外部存储但属于App私有系统不会拦截。分享时用FileProvider的content://路径代替file://路径。如果需要让用户能在「文件管理」里看到报告用MediaStore写入Downloads公共目录但这涉及权限声明和分区存储规范做起来比重定向复杂课题演示阶段用私有目录加分享就够了。5.2 自定义混淆字典配了却没生效Release包一进首页就闪退现象在proguard-rules.pro里配置了自定义混淆字典Release包编译成功但一进首页就闪退Logcat里报NullPointerException或ClassNotFoundExceptionDebug包完全正常。原因自定义混淆字典只影响混淆后的类名和方法名替换解决的是「反编译看不懂」的问题但并没有解决「被Gson、Room、Retrofit通过反射访问的类不能混淆」的问题。医疗系统里有大量实体类要经过Gson反序列化这些类的字段名一旦被混淆掉解析时赋值全为空运行时自然崩。解决在proguard-rules.pro里显式保留实体类和Gson注解字段。自定义词典可以继续用但keep规则是必须的两者不冲突。# proguard-rules.pro -keep class com.example.hospital.entity.** { *; } -keepclassmembers class * { com.google.gson.annotations.SerializedName fields; } -keepattributes Signature, *Annotation*, EnclosingMethod-keep class ...entity.** { *; }表示entity包下所有类的类名、字段、方法都不混淆。SerializedName这一行保留带该注解的字段确保JSON映射不出错。Signature属性是Gson处理泛型集合时要用的丢了会导致ListHospital反序列化成ListLinkedTreeMap的经典问题。如果你把数据库实体也放在entity包下Room的类一并被保留不会出问题。5.3 Room数据库升级导致覆盖安装崩溃现象用户装了1.0版后直接覆盖安装2.0版启动时崩溃Logcat提示Room cannot verify the data integrity或者在health_profile表上找不到新增的字段列。原因Room的数据库版本从1升到2时需要提供Migration迁移脚本。没有Migration的情况下Room默认认为数据库结构不合法直接抛异常保数据安全。这里不是代码bug而是数据库版本的版本管理没跟上。解决每次表结构变化都要写一个Migration对象并递增版本号。我给health_profile表加emergency_contact字段时迁移代码是这样写的val MIGRATION_1_2 object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL( ALTER TABLE health_profile ADD COLUMN emergency_contact TEXT ) } } val db Room.databaseBuilder(context, AppDatabase::class.java, hospital.db) .addMigrations(MIGRATION_1_2) .build()Migration(1, 2)表示从版本1迁移到版本2db.execSQL执行实际SQL。如果只是给旧表加字段用ALTER TABLE就行。如果改动多要按顺序在事务里执行多条SQL。还有一个保险手段.fallbackToDestructiveMigration()数据库版本不匹配时直接把旧表删掉重建。这个对演示环境来说省事但正式系统里用户数据会丢。我的建议是课题演示用fallback兜底代码里保留Migration作为加分项答辩时能讲清楚两者的取舍。5.4 ScrollView嵌套RecyclerView滑动失灵首页Banner跟着卡现象科室和排班页面为了上下滑动流畅外层套了ScrollView内层放RecyclerView显示医生列表。结果发现RecyclerView只能滑出一屏再往下滑没反应首页的CoordinatorLayout折叠动画也跟着掉帧。原因ScrollView和RecyclerView都是可滚动容器嵌套时两者的滑动事件会互相抢占。系统默认情况下内层RecyclerView优先消费滑动事件但外层ScrollView的高度计算又把RecyclerView内容限制在了一屏内结果就是「想滑滑不动想看看不全」。解决如果外层是ScrollView内层RecyclerView要关闭自身的嵌套滚动让事件统一交给外层处理。recyclerView.isNestedScrollingEnabled false recyclerView.layoutManager LinearLayoutManager(context) recyclerView.adapter doctorAdapterisNestedScrollingEnabled false让RecyclerView不参与嵌套滚动机制列表高度按内容自动测量铺开整个页面由ScrollView统一驱动。代价是内层RecyclerView复用失效数据量大时性能稍差。医疗系统一个科室的医生一般不超过几十人这个代价可以接受。如果列表数据超过一两百条更合理的设计是直接用单层RecyclerView配合多Type实现而不是嵌套。首页的Banner卡顿跟这个类似Banner基于ViewPager2本身有独立的滑动逻辑不要把它放在内层RecyclerView的item里放在CollapsingToolbarLayout里就不会抢事件。5.5 升级targetSdk后后台服务与时间格式一起翻车现象把targetSdk从30升到35后App在后台上传体检报告时经常被杀同时从后端拿到的日期字段出现两种格式一种是2025-06-01T10:30:00另一种是数字数组界面上直接显示成乱码。原因新版本Android对后台运行和前台服务做了更严的限制。没有声明前台服务类型和对应权限时后台任务被系统判定为非法启动进程随时被回收。时间问题则是后端LocalDateTime序列化默认格式与Android端Gson默认格式不一致LocalDateTime在Jackson里可能输出数组在Gson里可能输出ISO格式双方口径不统一。解决清单文件里给服务补上类型和权限同时前后端约定统一时间格式。uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_DATA_SYNC / service android:name.service.ReportUploadService android:foregroundServiceTypedataSync android:exportedfalse /// LocalDateTimeAdapter.kt class LocalDateTimeAdapter : JsonDeserializerLocalDateTime { private val formatter DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss) override fun deserialize( json: JsonElement, typeOfT: Type, context: JsonDeserializationContext ): LocalDateTime { return LocalDateTime.parse(json.asString, formatter) } } val gson GsonBuilder() .registerTypeAdapter(LocalDateTime::class.java, LocalDateTimeAdapter()) .create()foregroundServiceTypedataSync告诉系统这个服务用于数据同步配合FOREGROUND_SERVICE_DATA_SYNC权限Android 14及以上机型才能正常启动。时间适配器把yyyy-MM-dd HH:mm:ss作为唯一合法格式前后端都按这个来。新机型适配的检查清单我一般固定五步targetSdk升到当前编译SDK、分区存储适配、前台服务类型声明、16KB内存页对齐、预测性返回手势兼容。这五步跑通Android 16这类新版本的适配就算过关了剩下的是后续系统逐个版本验证。6. 演示与验收的最小测试集让答辩现场不翻车系统写完到答辩之间需要一个「最小可行测试集」。它不是完整的测试方案而是保证演示不出丑的底线。我把每次带课题验收前必测的用例固定成一张表真机跑一遍录屏存一份现场投影坏了也有备份。场景操作预期结果登录态预置账号登录杀掉进程重开直接进入首页不用重新登录首页加载打开首页看Banner和九宫格首屏有进度条Banner自动轮播不卡顿挂号三级联动医院→科室→医生→时间槽→提交每级列表有加载反馈提交按钮防重复点击断网重试飞行模式打开App再恢复缓存页可浏览网络失败页有重试按钮Release包打Release包跑一遍核心用例登录、挂号、报告查看均不闪退多机型模拟器和真机各跑一遍无布局错位、无文件路径报错演示前还有几个细节后端接口地址固定成本机局域网IP不要在答辩现场改代码预置一个测试账号和一套测试数据不要现场注册手机开启勿扰模式并关闭自动旋转防止来电和转屏打断流程。最容易被忽略的是「失败态」的演示比如故意断网给老师看App不会白屏反而比一帆风顺的演示更能体现工程意识。如果你想给课题加分有两个可落地的进阶方向。一是在导诊模块接入端侧AI大模型Android上可以用GGUF量化格式的模型配合llama.cpp的JNI封装实现离线分诊问答不依赖云端也符合医疗数据的隐私场景。二是通过BLE接入体检手环用BluetoothLeScanner拿心率、步数数据并写入Room。这两个方向都直接复用系统现有的网络层和数据库层但一下把系统的深度撑起来了。顺带一提这条从登录态到数据缓存再到文件分享的链路本身就是一组高频Android面试题的答案理解了再答任何相关问题都有的放矢。带过十几个这类课题之后我养成的习惯是先画失败态再写成功路径。每个页面的「加载中」「空数据」「网络错误」三种状态先做出来再填网络请求这样演示时永远不怕突发状况。希望帮到你。本文还有配套的精品资源点击获取
返回列表