179 lines
7.0 KiB
Markdown
179 lines
7.0 KiB
Markdown
|
|
# 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](01-architecture.md) | 模块划分 + 数据流 + 依赖选型理由 | 开工前 30min |
|
||
|
|
| 2 | [02-api-contract.md](02-api-contract.md) | 每个接口的请求/响应 + DTO 字段映射表 | 写 DTO 时对照 |
|
||
|
|
| 3 | [03-build-run.md](03-build-run.md) | Gradle / SDK / network security / 真机调试 | 第一次 build 前 |
|
||
|
|
| 4 | [04-milestones.md](04-milestones.md) | 7 天里程碑拆分 + DoD | 每天开工前看当天任务 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. 5 分钟决策摘要(免读细节直接用)
|
||
|
|
|
||
|
|
| 维度 | 选择 | 替代方案(及为啥不选)|
|
||
|
|
|---|---|---|
|
||
|
|
| 语言 | Kotlin 2.0.21 | — |
|
||
|
|
| UI | Jetpack Compose (Material3) | ❌ XML View(老)| ❌ RN/Flutter(项目太轻)|
|
||
|
|
| 网络 | 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. 启动指令(开工第一步)
|
||
|
|
|
||
|
|
1. **Android Studio Hedgehog**(2023.1.1)+ → `File → New → New Project → Empty Activity (Compose)`
|
||
|
|
2. **包名**:`com.diary.news`
|
||
|
|
3. **Application name**:`Diary News`
|
||
|
|
4. **Min SDK**:24 / **Target SDK**:35
|
||
|
|
5. **Kotlin DSL** + **Version Catalog**(`gradle/libs.versions.toml`)
|
||
|
|
6. 改完 `gradle/libs.versions.toml` 后第一次 Sync —— 视网络,5-15 分钟
|
||
|
|
7. 跑 `gradlew assembleDebug` 出第一个 debug APK
|
||
|
|
8. 真机或模拟器装上,网络选 **宿主机的桥接**(模拟器用 `10.0.2.2:3000` 临时绕开,真机直接走 server IP)
|
||
|
|
|
||
|
|
完整步骤见 [03-build-run.md](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](01-architecture.md) 看会不会破坏现有分层。
|