00-总览与架构

00 · 总览与架构

一句话职责 :给出安卓端 archive-management-app 的整体架构、分层与调用约定、构建与部署形态、跨端硬契约,作为其余 17 篇模块文档的共同前提。

本篇入口 :无(所有模块共享的基础设施)

对接后端 :Spring Boot(archive-management-backend),无 context-path,接口挂根路径

源码范围

源码 职责
MainApplication.kt Application 入口:安装崩溃记录、启动 Koin
MainActivity.kt 唯一 Activity(FragmentActivitysingleTop),承载 Compose 与通知深链投递
di/AppModule.kt 全部 Koin 装配:21 个 single + 17 个 viewModel
data/remote/Constants.kt BASE_URL / API_PREFIX 两个环境值
build.gradle.kts(根 + app) 插件版本、SDK 与编译选项、依赖清单
settings.gradle.kts 仓库镜像(阿里云优先)与模块声明
AndroidManifest.xml 权限、组件(Activity / Service / Receiver / Provider)声明
res/values/themes.xmlres/values-v31/themes.xmlres/drawable/splash_* 启动主题与闪屏
res/xml/network_security_config.xml 明文 HTTP 放行白名单

1. 模块概述

1.1 交付形态

单个 Android 应用(applicationId = com.lvyq.archiveapp),与 Web 中台、后端共享同一套接口。业务覆盖档案全生命周期:收集(人员/合同/设备/报告四类主档)→ 整理(实体子件入盒)→ 保管(档案盒/档案室)→ 利用(借阅申请、审批、核销)→ 盘点与鉴定销毁

1.2 技术栈与版本

类别 选型 版本
语言 Kotlin(JVM 目标 17) 2.0.21
构建 AGP / Gradle Wrapper 8.11.0 / 8.13(腾讯镜像)
UI Jetpack Compose + Material3 BOM 2024.09.00(Compose 1.7.0 / Material3 1.3.0)
适配 compileSdk / targetSdk / minSdk 34 / 34 / 24
网络 Retrofit + converter-gson / OkHttp 2.11.0 / 4.12.0
长连接 OkHttp WebSocket 4.12.0
本地存储 DataStore Preferences 1.0.0
依赖注入 Koin(纯 Kotlin,无注解处理器) 3.5.3
二维码 ZXing core(生成)/ zxing-android-embedded(扫码) 3.5.3 / 4.3.0
生物识别 BiometricPrompt + EncryptedSharedPreferences 1.1.0 / 1.1.0-alpha06
导航 无第三方导航库,自研状态机 + 深链 ---

1.3 依赖关系

  • 强依赖后端:所有业务数据均来自后端,App 无离线业务能力(CacheStore 只做弱网缓存补偿,见 02 篇)。
  • 强依赖后端菜单:首页宫格入口由 GET /system/menu/app 下发,本地 AppMenuConfig 仅作离线兜底(见 06 篇)。
  • 弱依赖 Web 端:仅「扫一扫登录」需要 Web 端展示二维码(见 05 篇)。

2. 架构与运行骨架

2.1 分层与单向数据流

复制代码
ui/**/*Screen.kt            Compose 界面(无业务逻辑,只订阅状态 + 派发事件)
      │ collectAsStateWithLifecycle(uiState)
      ▼
ui/**/*ViewModel.kt         StateFlow<UiState>;提交统一 try/catch(Throwable)
      │ 调用
      ▼
data/repository/**          纯数据编排:调接口、拼参数、映射错误;不依赖 Android 框架
      │
      ▼
data/remote(Retrofit / WS)  data/local(DataStore)

约定:

  • UI 状态单一真相源 :每个 ViewModel 暴露一个 StateFlow,Compose 侧用 collectAsStateWithLifecycle 订阅。
  • 异常不上抛到 UI :ViewModel 内部捕获并转换成状态字段(错误文案 / 空态),配合 safeLaunchCrashLogger 兜底,避免单点异常闪退。
  • Repository 不做 UI 决策 :文案映射只做「技术错误 → 用户可读文案」(NetError),业务分支留给 ViewModel。

2.2 依赖注入装配(di/AppModule.kt

分组 实例
基础设施(4) SessionStoreCacheStoreApiClient(baseUrl)MessageWsClient(baseUrl)
Repository(17) AuthRepositoryArchiveRepositoryProfileRepositoryBorrowRepositoryDictRepositoryWorkflowRepositoryQrLoginRepositoryNoticeRepositoryMessageRepositoryStatsRepositoryInventoryRepositoryBoxRepositoryFileRepositorySearchRepositoryMenuRepositoryAppraisalRepositoryHomeSummaryRepository
ViewModel(17) LoginViewModelArchiveListViewModelProfileViewModelBorrowViewModelScanViewModelBorrowApproveViewModelBorrowHandleViewModelBorrowApproveListViewModelBorrowApplyViewModelQrLoginViewModelNoticeListViewModelNoticeDetailViewModelMessageViewModelInventoryViewModelBoxQueryViewModelMainViewModelAppraisalViewModel
  • ApiClientMessageWsClient 都接收同一个 BASE_URL;前缀补全由 ApiPrefixInterceptor 在 OkHttp 层完成,二者共用(见 01 篇)。
  • Compose 中取 ViewModel 用 koinViewModel();在非 Composable 环境取单例用 GlobalContext.get().get<T>()

2.3 启动序列

复制代码
系统闪屏(API 31+ 走 values-v31 的 windowSplashScreen 红章 AVD;API 24--30 显示静态红章 splash_bg)
   ▼
MainApplication.onCreate      CrashLogger.install(this) → startKoin { appModule }
   ▼
MainActivity.onCreate         handleIntent(intent) → setContent { ArchiveAppTheme { AppNavigation() } }
   ▼
AppNavigation                 读本地登录态:无有效 token → 登录页;有 → MainScaffold
   ▼
MainScaffold                  加载动态菜单、拉首页大盘、申请通知权限并拉起推送前台服务

通知点击走 onNewIntent(Manifest 声明 launchMode="singleTop"):NavTarget.from(intent)PendingNavTarget.post(StateFlow)→ AppNavigationMainScaffold 消费。Activity 只投递不跳转 ,因为冷启动时 AppNavigation 还停在登录态恢复的等待态,MainScaffold 尚未挂载。

2.4 承载方式

全应用只有一个 ActivityMainActivity),其余页面均为 Compose 内的覆盖层/子页状态,扫码页由 zxing-android-embedded 的 CaptureActivity 承担(Manifest 中锁定竖屏)。


3. 工程结构与构建

3.1 目录结构

复制代码
app/src/main/java/com/lvyq/archiveapp/
├── MainApplication.kt / MainActivity.kt
├── data/remote/      ApiClient、Constants、MessageWsClient、19 个 *Api、54 个 model
├── data/local/       SessionStore(登录态/订阅/历史)、CacheStore
├── data/repository/  17 个 Repository
├── di/               AppModule
├── domain/menu/      AppMenuConfig(离线兜底菜单)、MenuIcons(矢量图标)
├── ui/               main / login / qrlogin / navigation / archive / electronic / borrow
│                     box / inventory / appraisal / notice / message / profile / theme / common
├── service/          MessagePushService(WS 前台服务)
├── receiver/         BootReceiver
└── util/             CrashLogger / NotifyHelper / NetError / PdfPreview / QrSaver / SafeClick 等

3.2 编译与产物

applicationId com.lvyq.archiveapp
versionCode / versionName 1 / 1.0.0ApiClientBuildConfig.VERSION_NAME 拼 User-Agent)
Java / Kotlin 目标 17 / 17
buildFeatures compose = truebuildConfig = true
release 混淆 isMinifyEnabled = false(保留可读堆栈,便于线上用 logcat 定位)
release 签名 buildTypes.release 未配置 signingConfigsassembleRelease 产出的是未签名包,需要另行签名后才能安装
仓库镜像 settings.gradle.kts 中阿里云镜像优先,FAIL_ON_PROJECT_REPOS 禁止模块私自声明仓库

3.3 清单与权限(AndroidManifest.xml

组件 / 权限 说明
MainActivity exported=truelaunchMode=singleTop、主题 Theme.ArchiveApp
CaptureActivity(zxing) 扫码取景,锁定竖屏
MessagePushService foregroundServiceType="dataSync"exported=false
BootReceiver 监听 BOOT_COMPLETEDMY_PACKAGE_REPLACED;声明 exported=true 是因为这两个都是受保护广播,只有系统可发,放开不引入外部触发面,却能兼容部分 ROM 的投递差异
FileProvider authorities=${applicationId}.fileprovider,预览不支持内嵌渲染的文件时授权外部应用打开
INTERNET / ACCESS_NETWORK_STATE 访问后端、网络状态判断
CAMERAneverForLocation 扫码核销 / 审核 / 登录
POST_NOTIFICATIONS Android 13+ 发送本地通知需运行时授权
FOREGROUND_SERVICE + FOREGROUND_SERVICE_DATA_SYNC 维持 WS 长连接(Android 14+ 必须声明类型)
RECEIVE_BOOT_COMPLETED 开机 / 升级后恢复消息提醒
USE_BIOMETRIC 指纹快捷登录

4. 关键设计决策

决策 为什么这样
不用 androidx.navigation 本应用是「单栈覆盖层」形态:底部两个常驻 Tab + 一批全屏子页,页面间没有深层嵌套路由与参数化路径。用一组布尔状态即可表达,且能天然做到「进二级页隐藏底栏」「返回键逐层回退」。引入 NavHost 反而要为每个覆盖层再定义一条 route 并维护回栈语义。
前缀放在 OkHttp 拦截器,而不是 BASE_URL Retrofit 对以 / 开头的相对路径按主机根 解析,会丢弃 base URL 的 path 部分;而全部接口注解都写成 @GET("/archive/..."),所以前缀只能由拦截器补。这样切换部署形态只改一个常量(见下)。
MainActivity.onNewIntent 只投递,不直接跳转 冷启动场景下 Compose 还停在登录态恢复的等待态,MainScaffold 未挂载;用 StateFlow 暂存「一次性目标」,由已挂载的页面自己消费并置空,避免丢目标或重复跳转。
WS 常驻前台服务(dataSync 消息提醒要求秒级到达且 App 退到后台也要在;只有前台服务能稳定持有长连接。服务内部自检登录态与通知开关,条件不满足即自行退出,避免无效常驻。
通知 id 按 type 派生 同一时刻只留同类最新一条、不同类型互不顶替;且 id 必须与 PendingIntent 的 requestCode 同值,否则多条通知共用一个 PendingIntent,点哪条都跳同一页(见 16 篇)。
buildConfig = true AGP 8 默认不再生成 BuildConfig,而 User-Agent 依赖 BuildConfig.VERSION_NAME
release 不开混淆 便于用线上 logcat 直接定位堆栈;当前未接崩溃上报平台,保留可读类名比压缩体积更重要。

5. 部署

5.1 两种部署形态(只改一个文件)

形态 BASE_URL API_PREFIX 实际请求
直连后端(开发 / 内网) http://192.168.1.100:8080/ "" http://192.168.1.100:8080/auth/login,WS /ws/message
经 nginx 统一入口(生产) https://archive.example.com/ "/api" https://archive.example.com/api/auth/login,WS /api/ws/message

两个值都在 data/remote/Constants.kt。注意:BASE_URL 只写到 host + 端口,且必须以 / 结尾 (Retrofit 强制要求);不要再把 /api 写进去(前缀会被丢弃但容易误导,详见 01 篇)。

nginx 侧必须配 proxy_pass http://127.0.0.1:8080/;(末尾 / 剥掉 /api);WebSocket 还需 proxy_http_version 1.1 + 透传 Upgrade/Connection + 读超时放大到 300s,完整片段见仓库 README.md 与 16 篇。

5.2 打包与安装

bash 复制代码
# 调试包(最常用)
./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

# 发布包:当前未配置 signingConfigs,产物未签名
./gradlew assembleRelease
# 需自行签名后再安装:
# apksigner sign --ks <keystore> --out app-release-signed.apk app/build/outputs/apk/release/app-release-unsigned.apk

5.3 必改配置

场景 要改什么
换后端地址 data/remote/Constants.ktBASE_URL(+ 经 nginx 时 API_PREFIX = "/api"
后端是明文 HTTP res/xml/network_security_config.xmldomain-config 里加该 IP 或域名
上生产 HTTPS 可移除整个 network_security_config.xml 与 Manifest 中的引用
版本号 app/build.gradle.ktsversionCode / versionName(后者会进 User-Agent)

5.4 后端需要具备的能力

  • 接口版本包含:动态菜单 /system/menu/app、统一检索 /archive/search、借阅全链路、盘点、鉴定销毁、消息 /system/message/*、文件 /file/{id}/file/preview-pdf/{id}
  • WebSocket 端点 /ws/message 可用(否则 App 只保留消息列表,不弹通知)。
  • Office 预览依赖后端装 LibreOffice(/file/preview-pdf/{id} 做转换)。
  • 准备账号:至少一个 admin(盘点/鉴定/档案盒)、一个 leader(审批)、一个 staff(申请借阅)。

6. 验证清单

操作步骤 预期结果 观察点
安装启动 adb install -r app-debug.apk 后点图标 系统闪屏红章 → 登录页(或已登录直接进首页),无白屏 logcat 无 FATAL EXCEPTION
Koin 装配 启动应用 NoBeanDefFoundException logcat 过滤 Koin
登录态持久化 登录 → 杀进程 → 重开 免登录直接进首页 SessionStore 有 token
网络前缀 首页下拉/切 Tab 触发刷新 请求路径带 /api 前缀(nginx 形态)且返回 200 logcat OkHttp --> GET .../api/...
动态菜单 用不同角色账号登录 宫格入口按角色变化 GET /system/menu/app 返回码与数组
明文 HTTP 后端用 http://,IP 不在白名单 请求报 CLEARTEXT communication not permitted 加 IP 后恢复正常
WS 连通 登录后停留首页 前台服务通知常驻,收到业务终态时弹通知 logcat 过滤 MessageWsClient / MessagePushService
深链 点通知 直接落到对应页面,且不重启 App MainActivity.onNewIntent 被调用
崩溃回显 人为触发一次崩溃后重开 首页弹上次崩溃摘要,可复制堆栈 私有目录 crash_last.txt
打包 ./gradlew assembleRelease 产出未签名 release 包(属预期) app/build/outputs/apk/release/

失败排查

现象 原因 处理
接口全 404 API_PREFIX 与部署形态不匹配 nginx 形态填 /api;直连填 ""
接口通但 WS 连不上 nginx 未开 proxy_http_version 1.1 / 未透传 Upgrade 见 16 篇与 README 的 nginx 片段
安装失败 INSTALL_FAILED_UPDATE_INCOMPATIBLE 签名不一致 adb uninstall com.lvyq.archiveapp
登录后白屏 菜单接口失败且兜底未生效 / token 无效 logcat 看 MainViewModel/system/menu/app 返回
收不到通知 通知权限未授权或订阅类型未勾选 系统设置授权 + 「我的 → 通知接收类型」

7. 已知边界

  • release 未配置签名与混淆assembleRelease 直接产出未签名包;如需上架需补 signingConfigs 与混淆规则。
  • 深色模式不完整Theme.kt 跟随系统并定义了 DarkColors,但业务界面大量直接使用 Brand.* 与字面量颜色,深色下卡片不会反转(见 04 篇)。
  • 无独立报表页 :存量总览以「首页家底台账」形式呈现;StatsRepository.borrowTrend()/archive/stats/borrow-trend)已封装但端上无消费(见 06 篇)。
  • 无离线业务能力CacheStore 仅覆盖鉴定与个人中心两处弱网补偿,其余页面断网即空态。
  • 消息只推新增不补存量:连接建立前产生的消息不经 WS 推送,需由各入口自行拉全量(见 16 篇)。
  • App 重启后推送依赖用户打开 App :Android 12+ 对后台启动前台服务有限制,BootReceiver 只能尽力拉起(见 16 篇)。
相关推荐
hai_android2 小时前
Android 组件化开发实践
android·java·kotlin
mmsx2 小时前
Android 地图卡成 PPT 之后:双渲染管线与空间网格渐进加载怎么救
android·app
河北清兮网络科技15 小时前
开发软件怎么找靠谱的公司?普通人最全筛选避坑指南
小程序·app·短剧·短剧app·广告联盟
传奇开心果编程20 小时前
【Jetpack Compose基础语法学与练】第6课 TextField文本输入,字符串状态与输入交互
学习·前端框架·kotlin·android jetpack
SXkehuirongsheng1 天前
APP定制开发怎么判断服务商的技术实力
百度·微信·app·网站建设·文心一言·微信公众平台
mmsx1 天前
MapLibre 实战 13|让比例尺显示 100m 而不是 347.2m:屏幕距离换算与两个易错点
android·前端·app
ai2work1 天前
ch06 Jetpack Compose 入门:状态驱动 UI
kotlin
HouWan2 天前
Flutter iOS UISceneDelegate 迁移指南:理清新的 Scene 生命周期
flutter·ios·app
JMchen2 天前
第 12 篇|项目整合与打包发布 —— 从 Demo 到可安装 APK 的完整收官指南
kotlin·android studio