273 lines
10 KiB
Markdown
273 lines
10 KiB
Markdown
|
|
# 04 · 7 天里程碑
|
||
|
|
|
||
|
|
> 目标:第 7 天能装一个 APK 到真机,跑通 **登录 → 列表 → 详情 → 收藏 → 离线缓存** 完整链路。
|
||
|
|
>
|
||
|
|
> 工作量估计基于:**全栈老哥**(你已经会 Kotlin/Compose 基础),按一天 3-4 小时有效开发时间算。
|
||
|
|
>
|
||
|
|
> **每个里程碑有明确 DoD**(Definition of Done)。不达 DoD 不算完成。
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 全局 DoD(贯穿 7 天)
|
||
|
|
|
||
|
|
- [ ] `./gradlew assembleDebug` 每次都 BUILD SUCCESSFUL
|
||
|
|
- [ ] 真机/模拟器能装能跑,不闪退
|
||
|
|
- [ ] `git commit` 节奏:每个里程碑一个 commit,信息写清楚
|
||
|
|
- [ ] logcat 无 ERROR 级别的 crash
|
||
|
|
- [ ] 没有 hardcoded 颜色 / 字体大小(全部走 `MaterialTheme`)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Day 1 — 工程骨架 + 鉴权三件套
|
||
|
|
|
||
|
|
**目标**:能登录、能持久化 token、401 自动 refresh、空 UI 能跳转。
|
||
|
|
|
||
|
|
### 任务清单
|
||
|
|
|
||
|
|
| # | 任务 | 文件 | 估时 |
|
||
|
|
|---|---|---|---|
|
||
|
|
| 1 | 新建 Android Studio 工程,改 `libs.versions.toml` + `app/build.gradle.kts` | 见 03 | 30min |
|
||
|
|
| 2 | 配 `network_security_config.xml` + 4 个 res 文件 | 见 03 §4 | 15min |
|
||
|
|
| 3 | `DiaryNewsApp.kt`(`@HiltAndroidApp`)| `app/` | 5min |
|
||
|
|
| 4 | `MainActivity.kt`(`@AndroidEntryPoint`,装 `AppNav`)| `app/` | 15min |
|
||
|
|
| 5 | DTO 文件 12 个 | `data/api/dto/` | 1h |
|
||
|
|
| 6 | `ApiService.kt` | `data/api/` | 30min |
|
||
|
|
| 7 | `TokenStore.kt`(EncryptedSharedPreferences)| `data/auth/` | 30min |
|
||
|
|
| 8 | `AuthInterceptor.kt` + `TokenAuthenticator.kt` | `data/api/` | 1h |
|
||
|
|
| 9 | `NetworkModule.kt`(Hilt)| `di/` | 30min |
|
||
|
|
| 10 | `LoginScreen.kt` + `LoginViewModel.kt` | `ui/login/` | 1h |
|
||
|
|
| 11 | `AppNav.kt` 骨架(Login ↔ Feed)| `ui/nav/` | 15min |
|
||
|
|
| 12 | `Theme.kt` + `Color.kt` + `Type.kt`(复用 web 的蓝 #2080f0)| `ui/theme/` | 30min |
|
||
|
|
|
||
|
|
### Day 1 DoD
|
||
|
|
|
||
|
|
- [ ] `./gradlew assembleDebug` BUILD SUCCESSFUL
|
||
|
|
- [ ] 装到真机,启动能看到登录页
|
||
|
|
- [ ] 输入 `owner / test1234`,点登录 → 跳到一个空白 Feed 页
|
||
|
|
- [ ] 杀掉 app 再开 → 直接进 Feed 页(token 持久化生效)
|
||
|
|
- [ ] 等 60min 后,任意接口请求 → 自动 refresh → 用户无感
|
||
|
|
- [ ] logcat 里能看到 `OkHttp` 打印的请求日志(debug build)
|
||
|
|
- [ ] 进 `设置 → 应用 → Diary News → 存储`,看不到明文 token(只在加密 SP 里)
|
||
|
|
|
||
|
|
### 验证方法
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Day 1 验证清单(贴在 issue / PR 描述里):
|
||
|
|
- [x] 登录成功,TokenStore 保存到 EncryptedSP
|
||
|
|
- [x] 重启 app 自动进 Feed(读 SP 成功)
|
||
|
|
- [x] 改密码 → 旧 token 在 server 失效 → app 收到 401 → 跳登录
|
||
|
|
- [x] 等 access 过期(60min)→ 任意请求 → 自动 refresh → 200
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Day 2 — Feed 列表(分页 + 卡片)
|
||
|
|
|
||
|
|
**目标**:能滚能动,看到真实的新闻列表,卡片视觉对齐 web。
|
||
|
|
|
||
|
|
### 任务清单
|
||
|
|
|
||
|
|
| # | 任务 | 文件 | 估时 |
|
||
|
|
|---|---|---|---|
|
||
|
|
| 1 | `ArticlePagingSource.kt` | `ui/feed/` | 30min |
|
||
|
|
| 2 | `FeedViewModel.kt`(Paging 流)| `ui/feed/` | 30min |
|
||
|
|
| 3 | `ArticleCard.kt`(对齐 web 视觉)| `ui/common/` | 1h |
|
||
|
|
| 4 | `FeedScreen.kt`(LazyColumn + 加载状态)| `ui/feed/` | 1h |
|
||
|
|
| 5 | 源筛选 + 关键词搜索(下拉 + 输入)| `ui/feed/` | 1h |
|
||
|
|
| 6 | 空状态 / 错误状态 / 加载状态组件 | `ui/common/` | 30min |
|
||
|
|
|
||
|
|
### Day 2 DoD
|
||
|
|
|
||
|
|
- [ ] 列表能加载第一页(50 条)
|
||
|
|
- [ ] 滑到底自动加载下一页(Paging 3 自动)
|
||
|
|
- [ ] 卡片展示:源 / 语言 / 分类 tag / 时间 / 原标题(灰)/ 中标题 / 插图 / 译文正文摘要 / 评论钩子
|
||
|
|
- [ ] 源筛选:选一个源,列表只剩该源
|
||
|
|
- [ ] 关键词搜索:输入 "AI" → 列表过滤
|
||
|
|
- [ ] 无网络时显示空状态 + 重试按钮
|
||
|
|
- [ ] 滚得快时图片不卡(Coil 默认就 OK)
|
||
|
|
|
||
|
|
### 视觉对齐清单
|
||
|
|
|
||
|
|
对照 web `frontend/src/views/Feed.vue`:
|
||
|
|
- 卡片圆角、间距、字号 1:1
|
||
|
|
- 中文标题字号 18sp,原标题 13sp 灰
|
||
|
|
- 评论钩子块背景 `#f6f8ff`,左边框 `#2080f0` 3dp
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Day 3 — 详情页(三段 Tab)
|
||
|
|
|
||
|
|
**目标**:点卡片进详情,看到完整译文 + 评论 + 原文。
|
||
|
|
|
||
|
|
### 任务清单
|
||
|
|
|
||
|
|
| # | 任务 | 文件 | 估时 |
|
||
|
|
|---|---|---|---|
|
||
|
|
| 1 | `ArticleDetailDto` → `domain.ArticleDetail` mapper | `data/` | 30min |
|
||
|
|
| 2 | `ArticleViewModel.kt`(`StateFlow<UiState>`)| `ui/article/` | 30min |
|
||
|
|
| 3 | `ArticleScreen.kt` 骨架 + TabRow | `ui/article/` | 30min |
|
||
|
|
| 4 | `ArticleTabs.kt`:`CommentaryTab` / `TranslationTab` / `OriginalTab` | `ui/article/` | 1h |
|
||
|
|
| 5 | `TranslationTab`:WebView 渲染 `body_zh_formatted`(无 JS)| `ui/article/` | 1h |
|
||
|
|
| 6 | `OriginalTab`:WebView 渲染 `body_html`(无 JS)| `ui/article/` | 15min |
|
||
|
|
| 7 | 详情页顶部卡片(标题 / 分类 / 时间 / 插图)| `ui/article/` | 30min |
|
||
|
|
| 8 | 收藏按钮 ☆(Day 4 接 API,Day 3 先显示静态)| `ui/article/` | 15min |
|
||
|
|
|
||
|
|
### Day 3 DoD
|
||
|
|
|
||
|
|
- [ ] 点列表卡片 → 进详情页
|
||
|
|
- [ ] 顶部展示:原标题(灰)/ 中标题(主)/ 插图 / 分类 tag / 发布时间 / 作者
|
||
|
|
- [ ] 三个 Tab 可切换:评论 / 译文 / 原文
|
||
|
|
- [ ] 译文 Tab 用 `body_zh_formatted`,如果没有就 fallback 到 `body_zh_text`
|
||
|
|
- [ ] WebView 不开 JS(验证方式:`runJavaScript` 调用失败)
|
||
|
|
- [ ] 详情页旋转屏幕 / 切换深色模式不崩
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Day 4 — 收藏(乐观更新 + Room 缓存)
|
||
|
|
|
||
|
|
**目标**:能收藏 / 取消,UI 秒响应,失败可回滚。
|
||
|
|
|
||
|
|
### 任务清单
|
||
|
|
|
||
|
|
| # | 任务 | 文件 | 估时 |
|
||
|
|
|---|---|---|---|
|
||
|
|
| 1 | Room:`AppDatabase.kt` + `BookmarkDao.kt` + `BookmarkCacheEntity` | `data/db/` | 1h |
|
||
|
|
| 2 | `BookmarkRepository.kt`(乐观更新)| `data/repository/` | 1h |
|
||
|
|
| 3 | `DatabaseModule.kt`(Hilt)| `di/` | 15min |
|
||
|
|
| 4 | 详情页收藏按钮接 `ArticleViewModel.toggleBookmark()` | `ui/article/` | 30min |
|
||
|
|
| 5 | `BookmarksScreen.kt` + `BookmarksViewModel.kt` | `ui/bookmarks/` | 1h |
|
||
|
|
| 6 | 离线显示:无网络时用 Room 缓存渲染 | `ui/bookmarks/` | 30min |
|
||
|
|
|
||
|
|
### Day 4 DoD
|
||
|
|
|
||
|
|
- [ ] 详情页点 ☆ → 立刻变实心(乐观更新)
|
||
|
|
- [ ] 后台调 `POST /bookmarks`,失败时 → 回滚 ☆ + Snackbar "收藏失败,已撤销"
|
||
|
|
- [ ] 收藏列表页能看到所有收藏
|
||
|
|
- [ ] 离线时收藏列表仍可看(读 Room 缓存)
|
||
|
|
- [ ] 滑动列表时无卡顿(用 `LazyColumn` + `key`)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Day 5 — 源 + 订阅
|
||
|
|
|
||
|
|
**目标**:能看所有源、订阅 / 取消订阅。
|
||
|
|
|
||
|
|
### 任务清单
|
||
|
|
|
||
|
|
| # | 任务 | 文件 | 估时 |
|
||
|
|
|---|---|---|---|
|
||
|
|
| 1 | `SourceRepository.kt` | `data/repository/` | 30min |
|
||
|
|
| 2 | `SourcesScreen.kt` + `SourcesViewModel.kt` | `ui/sources/` | 1h |
|
||
|
|
| 3 | 订阅按钮 + `POST/DELETE /subscriptions` | `ui/sources/` | 1h |
|
||
|
|
| 4 | 底部导航栏:Feed / Sources / Bookmarks | `ui/nav/` | 1h |
|
||
|
|
|
||
|
|
### Day 5 DoD
|
||
|
|
|
||
|
|
- [ ] 底部三 Tab:Feed / Sources / Bookmarks
|
||
|
|
- [ ] Sources 页能看到所有 enabled 源,带订阅状态
|
||
|
|
- [ ] 点订阅按钮 → 立刻变化 + 后台请求
|
||
|
|
- [ ] 失败 Snackbar 提示 + 回滚
|
||
|
|
- [ ] Feed 页源筛选多选能跨 Tab 状态保留(navigation 状态管理)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Day 6 — 主题 + 通用打磨
|
||
|
|
|
||
|
|
**目标**:视觉统一、空状态友好、加载体验好。
|
||
|
|
|
||
|
|
### 任务清单
|
||
|
|
|
||
|
|
| # | 任务 | 文件 | 估时 |
|
||
|
|
|---|---|---|---|
|
||
|
|
| 1 | Material3 主题:浅色 + 深色(Dynamic Color,Android 12+)| `ui/theme/` | 1h |
|
||
|
|
| 2 | 自定义启动屏 + 应用图标 | `res/` | 30min |
|
||
|
|
| 3 | 通用加载 / 空 / 错误组件统一风格 | `ui/common/` | 30min |
|
||
|
|
| 4 | 下拉刷新(`SwipeRefreshLayout` / `PullToRefreshContainer`)| `ui/feed/` | 30min |
|
||
|
|
| 5 | 网络状态监听(ConnectivityManager)| `data/` | 30min |
|
||
|
|
| 6 | 离线条 / Snackbar 提示 | `ui/common/` | 30min |
|
||
|
|
| 7 | 长按卡片 → 分享 / 复制 URL 菜单 | `ui/common/` | 30min |
|
||
|
|
|
||
|
|
### Day 6 DoD
|
||
|
|
|
||
|
|
- [ ] 切深色模式 → 全 app 颜色自动切(阅读体验好)
|
||
|
|
- [ ] 启动屏不闪白(用 SplashScreen API)
|
||
|
|
- [ ] 列表支持下拉刷新
|
||
|
|
- [ ] 飞行模式下进入 app → 各页有合理提示
|
||
|
|
- [ ] 长按文章卡片弹出菜单:复制链接 / 在浏览器打开 / 分享
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Day 7 — 收尾 + 真机端到端测试 + APK 打包
|
||
|
|
|
||
|
|
**目标**:出第一个 release-ready APK,完整跑一遍所有功能。
|
||
|
|
|
||
|
|
### 任务清单
|
||
|
|
|
||
|
|
| # | 任务 | 文件 | 估时 |
|
||
|
|
|---|---|---|---|
|
||
|
|
| 1 | `README.md`(装 app 说明 + 调试指南)| 根目录 | 30min |
|
||
|
|
| 2 | ProGuard / R8 配置 + release build 验证 | `proguard-rules.pro` | 1h |
|
||
|
|
| 3 | 真机端到端测试(用 [e2e-checklist.md](#) 列表)| — | 2h |
|
||
|
|
| 4 | crash 报告接入(可选:Sentry / Firebase Crashlytics)| — | 1h |
|
||
|
|
| 5 | 修最后发现的 bug | — | 1h |
|
||
|
|
|
||
|
|
### Day 7 DoD
|
||
|
|
|
||
|
|
- [ ] `./gradlew assembleRelease` 出 `app-release-unsigned.apk`
|
||
|
|
- [ ] debug APK 跑完下面所有场景,全过
|
||
|
|
|
||
|
|
### E2E 测试场景清单
|
||
|
|
|
||
|
|
- [ ] 第一次启动 → 登录页
|
||
|
|
- [ ] 登录成功 → 进 Feed
|
||
|
|
- [ ] 列表能加载,滑到底加载更多
|
||
|
|
- [ ] 源筛选 / 关键词搜索生效
|
||
|
|
- [ ] 点卡片进详情,三段 Tab 都能切
|
||
|
|
- [ ] 收藏 / 取消收藏 UI 秒响应,后台请求成功
|
||
|
|
- [ ] 收藏页能看到刚收藏的
|
||
|
|
- [ ] 杀进程,重启,直接进 Feed(token 持久化)
|
||
|
|
- [ ] 飞行模式启动,各页空状态 + 离线缓存(收藏)仍可用
|
||
|
|
- [ ] access 过期后任意请求,自动 refresh,UI 无感
|
||
|
|
- [ ] 改密码,旧 token → app 跳登录
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 风险 & 应对
|
||
|
|
|
||
|
|
| 风险 | 概率 | 影响 | 应对 |
|
||
|
|
|---|---|---|---|
|
||
|
|
| Compose / Kotlin 版本不兼容,build 失败 | 中 | 高 | 严格用本文档指定版本;失败先查 [Compose-Kotlin Compatibility Map](https://developer.android.com/jetpack/androidx/releases/compose-kotlin) |
|
||
|
|
| 后端 schema 改了,app 跑不起来 | 中 | 中 | 启动时 catch `SerializationException`,跳错误页 + 重启按钮 |
|
||
|
|
| 真机 7.0 / 7.1 系统太老,某些 API 没有 | 低 | 中 | 用 AndroidX 兼容层;遇到问题查 [API 24 compat matrix](https://developer.android.com/training/backward-compatible-support) |
|
||
|
|
| 网络安全白名单加错,全 app 报错 | 低 | 高 | 第 1 次 build 一定先跑清单 Day 1 DoD |
|
||
|
|
| 11 章(ProGuard)没做就 release,接口全找不到 | 中 | 高 | Day 7 必须先跑 `./gradlew assembleRelease` 再装机 |
|
||
|
|
| EncryptedSharedPreferences 在某些定制 ROM 上崩 | 极低 | 中 | catch `GeneralSecurityException`,fallback 引导用户重新登录 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 后续迭代方向(7 天后看)
|
||
|
|
|
||
|
|
| 优先级 | 功能 | 估时 |
|
||
|
|
|---|---|---|
|
||
|
|
| P1 | FCM 推送(新文章到时通知)| 2-3 天 |
|
||
|
|
| P1 | 阅读历史(本地)| 半天 |
|
||
|
|
| P2 | 平板 adaptive(WindowSizeClass + 双栏)| 1-2 天 |
|
||
|
|
| P2 | 离线下载包(整周报导出)| 2-3 天 |
|
||
|
|
| P3 | Wear OS 端 | 1 周 |
|
||
|
|
| P3 | 主屏 widget | 2-3 天 |
|
||
|
|
| P3 | 多账号切换 | 1 天 |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 任务量快速核算
|
||
|
|
|
||
|
|
| Day | 估时 | 主要内容 |
|
||
|
|
|---|---|---|
|
||
|
|
| 1 | 7h | 工程 + 鉴权 |
|
||
|
|
| 2 | 4.5h | Feed + 分页 + 卡片 |
|
||
|
|
| 3 | 4.5h | 详情 + WebView |
|
||
|
|
| 4 | 4h | 收藏 + Room |
|
||
|
|
| 5 | 3.5h | 源 + 订阅 + 底导 |
|
||
|
|
| 6 | 4h | 主题 + 通用 |
|
||
|
|
| 7 | 5.5h | 测试 + APK |
|
||
|
|
| **总计** | **~33h** | 7 个有效工作日 |
|