Files
diary-news/docs/android/README.md
Mavis 02f0260dfc docs(android): 完整方案 + logo 资源 + 启动屏
新增 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 备用通道上次已验证可用。
2026-06-10 14:11:43 +08:00

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. 启动指令(开工第一步)

  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


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 看会不会破坏现有分层。