Android保险理赔App实战:从离线暂存到图片上传全解析 简介《Insurance-Claims-App-for-Android》是一份基于 Java 的 Android 保险理赔应用工程源码面向 Android 初/中级开发者、移动应用课程设计者以及保险科技领域学习者展示了一个完整业务 App 的模块划分与开发思路可帮助读者理解界面搭建、数据管理、网络通信、权限适配、测试发布等环节的实际落地。压缩包共 77 个文件、约 793KB以 Java 类、XML 布局和资源文件为核心并包含 Gradle 构建脚本、ProGuard 混淆配置、PNG 图片素材及 README 说明工程目录规整适合在 Android Studio 中直接导入分析。工程将 Activity 与布局分离能直观体现界面与逻辑的组织方式项目场景围绕登录注册、理赔信息填写、进度反馈等常见功能既适合初学者对照模仿也为后续扩展二次开发提供了可运行基础。目前已有 190 人学习下载对于想快速走通保险理赔类 Android 项目的开发者来说是一份实用的参考工程。开篇为什么我会写一个保险理赔的Android应用如果你做过移动端开发大概率会有这种感觉市面上教程最多的App类型是电商、社交、工具类而真正涉及行业业务深水区的项目少之又少。保险理赔App恰好就是这样一个“看起来简单做起来全是细节”的领域。我最近在一个外包项目里完整做了一个Insurance-Claims-App-for-Android也就是保险理赔应用。它的核心场景很简单用户出险后通过手机拍照上传事故现场、填写理赔信息、提交单据保险公司后台审核最后完成赔付。但真把这套流程跑通涉及的知识点远不止“写几个页面调几个接口”那么简单包括离线暂存、图片压缩、多文件上传、OCR识别、任务状态流转、弱网处理每一个环节都是坑。这篇博文就把我从零到一搭建这个项目的完整过程记录下来包括架构思路、关键模块的实现细节、踩过的坑和排查方法。如果你正在做或者准备做类似的业务型App保险、银行、政务、医疗类这篇文章可以直接当参考手册用。1. 项目整体设计与思路拆解1.1 核心需求解析理赔流程的痛点在哪保险理赔的线下流程用户端最烦的几件事是什么填一堆纸质单子、拍的照片不清晰被驳回、不知道理赔进度、来回跑门店。所以App端的核心价值就两个词提效和透明。把这个目标拆解成功能点就是下面这些在线报案用户录入保单号/身份证号系统自动带出保单信息减少手动输入。事故信息采集填写事故时间、地点、类型、经过描述这是理赔审核的重要依据。拍照上传现场照片、证件照片、维修单据多类型、多张数。理赔进度查询提交后能看到审核状态流转比如“已提交 → 审核中 → 待补充材料 → 已结案”。离线暂存与恢复用户可能在事故现场网络很差填了一半的内容不能丢。这些功能单看都不难但组合在一起就要考虑很多联动问题。比如照片拍了之后是立即上传还是等用户点“提交”再统一上传如果用户填到一半切到后台草稿怎么保存弱网环境下图片上传失败怎么重试1.2 技术选型为什么用这些方案这一块直接说结论我最终选型如下语言Kotlin协程处理异步任务。架构MVVM JetpackViewModel、LiveData、Room、DataStore。网络层Retrofit OkHttp拦截器做日志与token注入上传用Multipart。图片处理CameraX拍照 系统相册选择Compressor库压缩Glide加载预览。本地存储Room存草稿和任务记录DataStore存用户偏好。依赖注入Koin轻量且Kotlin友好。定位高德地图SDK获取事故地点坐标和反地理编码。为什么这么选我一个个说。Kotlin 协程现在基本是Android开发的默认组合协程在处理“多个图片串行上传”、“接口失败重试”这类场景时比回调嵌套或者RxJava的链式调用直观得多。Room做草稿箱特别好用。理赔信息是强结构化数据保单号、时间、地点、字段固定用SQLite存储天然合适。Room的DAO可以很方便地实现“插入草稿→更新状态→查询列表”这一套操作。图片上传这块我踩过不少坑后面会细说。这里先提一个原则永远不要在客户端传原图。相机拍出来一张照片动辄3-8MB即使WiFi环境多张图排队上传的耗时和流量消耗也会让用户崩溃。所以压缩是必选项。2. 核心细节解析与实操要点2.1 投保信息录入与核验逻辑用户输入保单号后App需要调后台的“保单查询接口”获取投保信息。这里有两个关键点第一输入校验要前置。保单号格式各家保险公司不一样有纯数字的有字母数字混合的。接口文档里如果没写清楚一定要找后端确认正则规则不要自己猜。我在项目里用的校验逻辑是这样的先按长度过滤再按字符类型匹配前端校验通过后才允许点击“下一步”。第二查询状态要处理。保单查询接口返回的信息里有个字段叫“保单状态”可能是有效、已过期、已退保等。App端需要根据状态决定用户能不能继续报案。这个逻辑一定要做否则用户拿着一张已过期的保单也能走到拍照那一步最后后台审核打回来体验极差。另外这里需要做一个缓存优化。用户在同一个页面重复输入保单号查询时短时间内不要反复请求网络。我的做法是在ViewModel里维护一个查询结果的Map以保单号为key5分钟内命中缓存就直接返回避免无意义的网络开销。2.2 事故信息采集表单设计的隐藏细节事故信息这块看起来就是一个普通表单但实际做的时候有几个交互细节值得注意事故地点自动填充。用户在事故现场点击“定位”按钮App调起定位SDK拿到经纬度反向地理编码成地址文字。这里注意定位SDK返回的地址可能带“道路”或“POI名称”用户需要的往往是“XX路口”或“XX停车场”这种描述性地址。我在拿到SDK返回后做了个简单的拼接逻辑区县 道路名 门牌号如果拼不出来就fallback到POI名称。时间默认值处理。事故时间默认填充当前时间但用户可能填的是“昨天下午”的事故。这里我加了一个快捷选择今天、昨天、前天再配上完整的日期选择器。实测下来用户用快捷选项的概率超过70%这个功能虽然不起眼但对提效帮助很大。事故经过的多行文本输入。这个字段容易出问题很多人会一口气打几百字还不加标点。我的处理办法是做了字数限制500字以内并且加了“待办提示”文案引导用户分点描述比如“碰撞部位、对方车辆情况、是否有人员受伤”。这样后台审核人员拿到的事故描述质量会高很多。2.3 拍照采集与图片压缩方案这是整个项目技术含量最高的部分我单独拆开讲。CameraX接入本身不复杂但相机拍摄后照片的处理是个大学问。原始方案是直接用CameraX的ImageCapture.takePicture回调拿到ImageProxy然后转成Bitmap做后续处理。但这个流程有几个隐藏问题相机照片默认是旋转的需要根据EXIF信息做旋转校正。转Bitmap的过程在大分辨率照片上容易卡顿甚至OOM。回调里的ImageProxy必须手动close否则内存泄漏。我的最终实现方案是CameraX拍完照片后先把文件写到缓存目录不经过Bitmap再用Compressor库做压缩压缩到长边不超过1920px、质量80%左右最后再压缩后的文件路径保存到Room的草稿数据里。为什么要1920px因为保险审核人员看照片是在电脑屏幕上1920px足够清楚再大除了浪费流量没有意义。相册选择也有坑。直接用系统相册的ACTION_PICK在部分国产ROM上会得到一颗带EXIF旋转的图片而且相册返回的缩略图可能非常小。我的做法是走ACTION_GET_CONTENT让用户选图拿到URI后先query出真实路径再做和相机一样的压缩流程。这里注意Android 10以上的分区存储机制下很多路径不能直接访问所以统一用ContentResolver拷贝到应用缓存目录再处理。3. 实操过程与核心环节实现3.1 项目基础框架搭建我用Android Studio创建一个空项目包名按要求定为com.example.insuranceclaims最低API级别24目标API级别34。这里多说一句targetSdkVersion一定要跟进到34以上因为从Android 13开始通知权限、照片选择器等行为都变了不升级会踩很多坑。依赖库版本也分享一下以我当时做项目的稳定版本为准implementation androidx.core:core-ktx:1.12.0 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0 implementation androidx.room:room-runtime:2.6.1 implementation androidx.room:room-ktx:2.6.1 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:logging-interceptor:4.12.0 implementation io.insert-koin:koin-android:3.5.0 implementation io.github.PolishDraughts:android-compressor:1.0.5网络层的封装我用了典型的方式OkHttp统一设置超时连接10秒读取30秒日志拦截器只Debug包启用每个请求自动带上Authorization: Bearer token的拦截器。上传接口用Retrofit的MultipartMultipart POST(claim/upload) suspend fun uploadImage( Part(claimId) claimId: RequestBody, Part file: MultipartBody.Part ): ApiResponseUploadResult3.2 草稿与离线暂存的实现保险理赔场景里用户很可能在信号差的地下停车场、郊区路段操作所以离线暂存不是“加分项”而是“必要项”。我用Room建了一张claim_draft表字段包括保单号、事故时间、事故地点经纬度、事故描述、照片路径列表用JSON字符串存、当前步骤、更新时间。用户在每一步操作后都自动调DAO的upsert写入草稿。这里有一个设计细节要强调照片路径列表默认存的是压缩后的图片路径而不是原图路径。虽然原图路径在预览时会更清晰但一旦用户清缓存原图和压缩图文件都没了到时候草稿里的路径指向一个不存在的文件预览一加载就直接裂图。所以我在保存草稿时连压缩图也一起做持久化保存到内部存储的files/claims/目录这样即使缓存被清图片还在。Room的实体定义大概长这样Entity(tableName claim_draft) data class ClaimDraft( PrimaryKey(autoGenerate true) val id: Long 0, val policyNo: String, val accidentTime: Long, val latitude: Double?, val longitude: Double?, val address: String?, val description: String?, val photoPaths: String, // JSON数组 val currentStep: Int, val updatedAt: Long )序列化和反序列化JSON我直接用Gson简单可靠不需要引入额外的序列化库。3.3 提交理赔与状态机流转用户点“提交”之后整个流程进入一个状态机。状态包括SUBMITTING提交中转圈等待。UPLOADING图片上传中显示进度条第几张/共几张。SUCCESS提交成功跳转进度页。PARTIAL_SUCCESS部分信息提交成功比如文字信息提交了但图片失败需要引导用户重试。FAILED完全失败可以点重试。因为图片上传和文字信息提交是两次不同的网络请求所以我选择“先传文字、再传图片”的顺序。文字先提交的好处是即使图片全部失败后台也能看到一条理赔记录用户不会完全“失联”。图片上传失败时App记录哪些图片传成功了下次点“重试”只传失败的不重复上传。这个逻辑在协程里实现很优雅viewModelScope.launch { _uiState.value SubmitState.SUBMITTING val claimId submitClaimInfo(draft) // 文字信息 var failedCount 0 val successPaths mutableListOfString() for (path in draft.photoPaths) { runCatching { uploadImage(claimId, path) } .onSuccess { successPaths.add(path) } .onFailure { failedCount } } // 根据 failedCount 决定进入 SUCCESS / PARTIAL_SUCCESS }runCatching在这里配合协程非常好用单个图片上传失败不会中断整个流程而且CoroutineScope取消时循环里的协程会自动响应取消不会出现“Activity销毁了还在传图”的尴尬。4. 常见问题与排查技巧实录4.1 拍照后图片旋转90度的问题这个坑几乎每个做拍照功能的Android开发者都会遇到。相机传感器不是固定朝上的不同手机在竖屏拍摄时EXIF信息里的Orientation值可能是90、180、270。直接加载Bitmap会看到图片方向不对。解决思路是读取EXIF后手动旋转或者交给支持EXIF识别的库处理。我的做法是用Compressor库的configureWith方法显式设置setOrientation从EXIF读取并纠正val compressedImageFile Compressor.compress(context, originalFile) { quality(80) maxWidth(1920) maxHeight(1920) // 读取并应用EXIF方向 }Compressor库底层已经处理了EXIF旋转的问题这是我选它而不是手写压缩逻辑的原因之一。如果你不用库记住一个要点ExifInterface要查的是原图文件的属性不是压缩后文件的属性很多人在这里栽跟头。4.2 图片上传超时或失败图片上传在弱网环境下极容易超时。OkHttp默认读取超时设为30秒但如果网络实在差一张1MB的图片传30秒也可能不够。我的优化方案是单张图片重试3次第一次失败后间隔2秒重试第二次失败后间隔5秒再试。如果3次都失败跳过该图片记录失败列表用户可在“待上传”区域手动触发重传。整体上传的时候后台任务不要用GlobalScope用viewModelScope配合Dispatchers.IO切换线程。另外一个容易忽略的点上传图片时不要直接传原文件而要先检查文件是否存在。用户可能拍完照、压缩完、还没提交就手动清理了应用缓存。上传时拿到一个不存在的File路径OkHttp会直接抛异常。所以我在上传前加了File.exists()的判断不存在的直接跳过并记录原因。4.3 草稿恢复与图片路径失效的问题草稿恢复时我遇到过这么一种情况用户填了几步、拍了几张照片App被系统杀了。重启App后草稿列表能看到但点击进去照片预览区全部裂图。排查后发现原因有两点一是存储草稿时我存的是“压缩后的图片路径”但压缩是异步的用户可能在压缩还没完成时就要退出这时候路径还没写入数据库二是应用进程被杀后图片文件还在但路径因为缓存目录被系统清理而失效。解决方法是双保险压缩完成后才允许退出当前步骤加一个等位Flow压缩完再解除“下一步”按钮的禁用状态图片保存位置从cacheDir挪到filesDir因为cache目录随时可能被系统清理files目录在应用存活期间基本稳定。做完这两步之后这个问题的复现率降到接近0。4.4 真机调试时Android 14及以上版本的权限问题这次项目我targetSdk升到了34踩的最大的坑是Android 14对前台服务和通知权限的收紧。如果需要在提交理赔时做后台上传必须申请FOREGROUND_SERVICE权限并对应用类型做声明否则直接SecurityException崩溃。还有就是Android 13以上的POST_NOTIFICATIONS运行时权限。我的项目里理赔提交成功、后台审核有结果时会推本地通知后台返回状态后App拉取并弹通知。这个权限不申请通知会静默失效用户完全不知道进度有更新。这些权限的处理方式我把它们集中在启动流程里做了一次性引导用一个专门的PermissionManager统一处理避免在业务代码里到处插入权限请求逻辑。5. 性能优化与体验细节补充5.1 图片列表的懒加载与内存管理理赔照片通常是多张的我用了RecyclerView展示。加载图片用Glide但它默认会把图片加载到内存缓存里照片多且大的时候容易OOM。这里有几个优化点Glide加载时使用override(400, 400)缩小目标尺寸预览图不需要1920px。skipMemoryCache(true)在本页面可以做因为照片文件就在本地重新加载磁盘的开销远小于内存溢出的风险。RecyclerView的item复用时Glide的clear()方法要在onViewRecycled里调用防止图片错位。5.2 提交进度反馈用户提交理赔是个“长时间等待”的操作如果界面只是干巴巴一个转圈用户很容易以为卡死了。我做了两段式进度反馈文字信息提交阶段显示“正在提交理赔信息…”。图片上传阶段显示“正在上传照片2/5…”并配进度条。另外增加了一个“后台提交”按钮允许用户提交后直接退出当前页面App自动在后台完成剩余上传完成后通过通知告知结果。这个功能极大提升了真实场景下的用户体验很多用户提交完不会傻等着看进度条直接锁屏走了。5.3 数据上报与日志埋点最后补充一个项目上线后才意识到的点一定要埋点。至少需要记录这几个事件用户从点击“报案”到提交成功的时间间隔分析哪个环节流失最多。每一步表单页的停留时长看用户是否在某一步反复犹豫。图片上传失败的重试次数和成功率决定后端图片服务器要不要扩容。崩溃日志接入Firebase Crashlytics或自有日志渠道。我接手的一个旧项目就是因为完全没埋点上线后做产品迭代全靠拍脑袋。这次我从第一天就埋了日志到了优化版本的时候直接拿数据说话效率完全不一样。结尾做完这个项目后我的几点实在体会保险理赔类App跟普通工具类App有个本质区别用户是在真正的压力场景下使用你的产品。出险的人心情是焦躁的App每一步卡顿、每一个看不懂的按钮都会放大他的负面情绪。所以这个项目里比起炫技式的动画效果和酷炫界面更能提升口碑的反而是那些不起眼的“兜底设计”——草稿不丢、图片能传上去、失败了有点重试的途径、通知能准确触达。做这类业务型App先踏实把基础链路走稳再谈体验优化这是我的核心体会。另外提醒一句上线前一定做几台不同品牌的真机测试尤其是拍照和相册选择这两个环节ROM差异带来的坑远比你能想象的要多。本文还有配套的精品资源点击获取