新增 docs/android/ 目录: - README.md 总入口(快速上手 + 决策摘要 + 数据流) - 01-architecture.md 模块划分 + 数据流 + 选型理由 - 02-api-contract.md 每个接口的请求/响应 + DTO 字段映射 - 03-build-run.md Gradle/SDK/网络安全白名单/真机调试 - 04-milestones.md 7 天里程碑 + DoD + E2E 测试场景 新增 assets/: - logo/: 主图标 master + adaptive icon + 5 DPI launcher (方/圆) - splash/: 启动屏 logo + 完整背景预览 + 5 DPI 资源 - android_resources/: 集成所需的 XML(adaptive icon/主题/颜色/字符串/drawable/layout) - INTEGRATION.md 集成指南 - logo.svg + _make_logo.py 设计源 设计风格:参考用户提供的木质方块字母积木图,米色木纹底 + 深棕色字母 D,代表 'Diary',温暖私人日记感。 服务器体检:所有容器/API/DB/翻译主链路正常,TMT 本月已用 0.37%。 MaaS 备用通道上次已验证可用。
7.0 KiB
7.0 KiB
Diary News — Android App
私人新闻聚合器的 Android 客户端,读 FastAPI 后端。
状态:方案阶段(未开工)。本文档是开工前的最终蓝图,任何代码改动必须先对齐这里的约定。
0. 一句话总览
- 后端:
http://207.57.129.228:3000/api/v1(私有 IP 直连,明文 HTTP) - 客户端:Kotlin + Jetpack Compose,单 module
- 认证:Bearer JWT,access 60min,自动 refresh
- 离线:列表分页缓存 + 收藏本地库 + Token 加密存储
- 目标:7 天出第一个能跑通登录 → 列表 → 详情 → 收藏的 APK
1. 文档索引(按开工顺序读)
| 序 | 文档 | 作用 | 何时读 |
|---|---|---|---|
| 1 | 01-architecture.md | 模块划分 + 数据流 + 依赖选型理由 | 开工前 30min |
| 2 | 02-api-contract.md | 每个接口的请求/响应 + DTO 字段映射表 | 写 DTO 时对照 |
| 3 | 03-build-run.md | Gradle / SDK / network security / 真机调试 | 第一次 build 前 |
| 4 | 04-milestones.md | 7 天里程碑拆分 + DoD | 每天开工前看当天任务 |
2. 5 分钟决策摘要(免读细节直接用)
| 维度 | 选择 | 替代方案(及为啥不选) |
|---|---|---|
| 语言 | Kotlin 2.0.21 | — |
| UI | Jetpack Compose (Material3) | ❌ XML View(老) |
| 网络 | Retrofit 2.11 + OkHttp 4.12 | ❌ Ktor Client(生态薄) |
| 序列化 | kotlinx.serialization 1.7.3 | ❌ Moshi(更重) |
| DI | Hilt 2.52 | ❌ Koin(运行时,启动慢) |
| 分页 | Paging 3.3.4 | ❌ 手写 LazyColumn + offset |
| 图片 | Coil 2.7 | ❌ Glide(Compose 集成弱) |
| 路由 | Navigation Compose 2.8.4 | — |
| 加密 | security-crypto 1.1.0-alpha06(EncryptedSharedPreferences) | — |
| 本地 DB | Room 2.6.1 | — |
| minSdk | 24 (Android 7.0) | ❌ 26(放弃 7.0/7.1 ~3% 用户) |
| targetSdk | 35 (Android 15) | — |
| compileSdk | 35 | — |
| AGP | 8.7.2 | — |
| API base | http://207.57.129.228:3000/api/v1 |
❌ HTTPS(当前 server 无证书) |
3. 顶层目录(最终落地的样子)
diary-news-android/ # ← 独立 Git 仓库(不要混进 diary-news)
├── settings.gradle.kts├── build.gradle.kts # root
├── gradle/
│ ├── libs.versions.toml # 集中版本
│ └── wrapper/
├── app/
│ ├── build.gradle.kts│ ├── proguard-rules.pro│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── res/ # xml + values + mipmap
│ └── java/com/diary/news/
│ ├── DiaryNewsApp.kt # @HiltAndroidApp
│ ├── MainActivity.kt│ ├── data/ # api/auth/db/repository
│ ├── domain/ # 业务 model
│ ├── di/ # Hilt modules
│ └── ui/ # theme/nav/login/feed/article/bookmarks/sources/common
└── README.md
仓库策略:Android app 单独建 Git 仓库
diary-news-android,不要和diary-news(后端 + web)混。原因是 release 节奏 / CI / 依赖管理天然不同。
4. 端到端数据流(登录 → 刷列表 → 打开详情 → 收藏)
1. 启动
DiaryNewsApp.onCreate
→ Hilt 初始化
→ MainActivity 检查 TokenStore:有 access 且未过期 → 直接进 Feed
→ 无 / 过期 → 进 LoginScreen
2. 登录
LoginScreen → LoginViewModel.login()
→ POST /auth/login { username, password }
→ 拿到 { access_token, refresh_token, expires_in }
→ TokenStore.save() (EncryptedSharedPreferences)
→ 跳 FeedScreen
3. 列表
FeedScreen → FeedViewModel
→ Pager + ArticlePagingSource(api::listArticles)
→ ApiService.listArticles(page, page_size, ...)
→ AuthInterceptor 加 Authorization: Bearer <access>
→ 返回 ArticleListResponseDto
→ Paging 流到 LazyColumn
→ 每一项 ArticleCard 渲染
4. 详情
点 ArticleCard → ArticleScreen(articleId)
→ ArticleViewModel.load(id)
→ ApiService.getArticle(id)
→ 三段 Tab:评论(commentary)/译文(body_zh_formatted)/原文(body_html)
5. 收藏
详情页点 ☆ 按钮 → ArticleViewModel.toggleBookmark()
→ POST /bookmarks { article_id } 或 DELETE /bookmarks/{id}
→ 乐观更新 UI(立刻变实心★,失败再回滚 + Snackbar)
6. 401 自动 refresh
任意接口返回 401
→ OkHttp TokenAuthenticator 拦截
→ 拿 refresh_token 调 /auth/refresh
→ 拿到新 access → 重发原请求
→ 用户完全无感
5. 不要做的事(踩坑清单)
| 坑 | 说明 | 正确做法 |
|---|---|---|
| ❌ 把 access_token 存普通 SharedPreferences | root 手机秒读 | EncryptedSharedPreferences + Keystore |
| ❌ 在主线程调 Retrofit | ANR | suspend fun + ViewModelScope(自动主线程安全) |
| ❌ 在每次请求前同步读 token | 阻塞 UI | TokenStore 缓存到内存,只在 save/load 时动 SP |
| ❌ refresh 接口并发触发 N 次 | 触发限流 / 死锁 | synchronized(this) 单飞锁 |
| ❌ 全量缓存所有文章 | DB 撑爆,启动慢 | 只缓存当前可见页 |
| ❌ WebView 开 JS | XSS 风险 | settings.javaScriptEnabled = false |
| ❌ 信任 HTTPS 证书所有 CA | 中间人 | 默认 system trust anchor,不动 |
| ❌ 让 HTTP 走全网 | 不安全 | network_security_config.xml 白名单单一 IP |
| ❌ 在 ViewModel 里持有 Context | 内存泄漏 | 用 @HiltAndroidApp / AndroidEntryPoint,Context 通过 Hilt 注入 |
| ❌ ProGuard 不留 keep 规则就 release | retrofit 接口全找不到 | 详见 03-build-run.md §5 |
6. 启动指令(开工第一步)
- Android Studio Hedgehog(2023.1.1)+ →
File → New → New Project → Empty Activity (Compose) - 包名:
com.diary.news - Application name:
Diary News - Min SDK:24 / Target SDK:35
- Kotlin DSL + Version Catalog(
gradle/libs.versions.toml) - 改完
gradle/libs.versions.toml后第一次 Sync —— 视网络,5-15 分钟 - 跑
gradlew assembleDebug出第一个 debug APK - 真机或模拟器装上,网络选 宿主机的桥接(模拟器用
10.0.2.2:3000临时绕开,真机直接走 server IP)
完整步骤见 03-build-run.md。
7. 与 web 端的关系
| 项 | web (Vue) | Android (Kotlin) |
|---|---|---|
| 主题色 | #2080f0 |
同 |
| 字体 | 系统字体 + 14px/13px | Material3 Typography scale |
| 卡片布局 | 标题 → 译标 → 摘要 → 评论钩子 | 同(插图在中间) |
| 列表分页 | 12345 页码 (NPagination) | 滚动加载(Paging 3) |
| 详情页布局 | 评论 / 译文 / 原文 三段 | 同(改用 Tab) |
| 鉴权 | localStorage 存 token | EncryptedSharedPreferences |
视觉与交互保持一致,不要做出两个产品的分裂感。
8. 后续可能加的东西(不在 MVP)
- 推送通知(FCM / 极光)
- 离线下载包(整本周报导出 PDF)
- 阅读历史(本地,不上服务端)
- 暗色主题(Material3 Dynamic Color)
- 主屏幕 widget(显示今日头条)
- Wear OS 端(以后再说)
原则:MVP 先能跑,再加料。每加一项功能,先回到 01-architecture.md 看会不会破坏现有分层。