# 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 → 返回 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) 看会不会破坏现有分层。