zhibo/.kiro/specs/android-hot-tab/design.md

579 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Design Document
## Overview
本设计文档描述了在Android直播应用中添加"热门"Tab功能的技术实现方案。该功能将在"发现"页面的分类标签栏categoryTabs中添加一个"热门"Tab位于最左侧"推荐"Tab左边用于展示后台管理员标记为热门的作品内容。
## Architecture
### 系统架构
```
┌─────────────────────────────────────────────────────────────┐
│ MainActivity │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ topTabs: 关注 | 发现 | 附近 │ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ categoryTabs: 热门 | 推荐 | 直播 | 视频 | 音乐 | 游戏│ │
│ └───────────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ RecyclerView (WorksAdapter) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ 作品1 │ │ 作品2 │ │ 作品3 │ │ │
│ │ │ 🔥热门 │ │ 🔥热门 │ │ 🔥热门 │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
ApiService
GET /api/works/hot?page=1&pageSize=20
后端服务器
返回 is_hot=1 的作品列表
```
### 数据流
1. **用户操作** → 点击"热门"Tab
2. **MainActivity** → 调用 `loadHotWorks(page)` 方法
3. **ApiService** → 发送 GET 请求到 `/api/works/hot`
4. **后端服务器** → 查询 `is_hot=1` 的作品,按 `hot_time` 降序排序
5. **ApiService** → 接收 `PageResponse<WorksResponse>` 数据
6. **WorksAdapter** → 渲染作品列表
7. **UI** → 显示热门作品,每个作品卡片显示🔥标识
## Components and Interfaces
### 1. MainActivity 修改
#### 新增成员变量
```java
// 热门作品相关
private final List<WorksResponse> hotWorks = new ArrayList<>(); // 热门作品列表
private int currentHotWorksPage = 1; // 当前热门作品页码
private boolean isLoadingHotWorks = false; // 是否正在加载热门作品
private boolean hasMoreHotWorks = true; // 是否还有更多热门作品
```
#### 新增方法
```java
/**
* 加载热门作品列表
* @param page 页码从1开始
*/
private void loadHotWorks(int page) {
if (isLoadingHotWorks) return;
isLoadingHotWorks = true;
// 显示加载状态
if (page == 1) {
if (hotWorks.isEmpty()) {
LoadingStateManager.showSkeleton(binding.roomsRecyclerView, 6);
}
}
ApiClient.getService(getApplicationContext())
.getHotWorks(page, 20)
.enqueue(new Callback<ApiResponse<PageResponse<WorksResponse>>>() {
@Override
public void onResponse(Call<ApiResponse<PageResponse<WorksResponse>>> call,
Response<ApiResponse<PageResponse<WorksResponse>>> response) {
isLoadingHotWorks = false;
binding.swipeRefresh.setRefreshing(false);
if (response.isSuccessful() && response.body() != null && response.body().isOk()) {
PageResponse<WorksResponse> pageData = response.body().getData();
if (pageData != null && pageData.getList() != null) {
if (page == 1) {
hotWorks.clear();
}
hotWorks.addAll(pageData.getList());
// 检查是否还有更多数据
hasMoreHotWorks = pageData.getList().size() >= 20;
// 更新UI
updateHotWorksUI();
}
} else {
showErrorState("加载失败,请重试");
}
}
@Override
public void onFailure(Call<ApiResponse<PageResponse<WorksResponse>>> call, Throwable t) {
isLoadingHotWorks = false;
binding.swipeRefresh.setRefreshing(false);
showErrorState("网络错误,请重试");
}
});
}
/**
* 更新热门作品UI
*/
private void updateHotWorksUI() {
if (hotWorks.isEmpty()) {
showEmptyState("暂无热门作品");
} else {
hideEmptyState();
hideErrorState();
// 转换为FeedItem列表
List<FeedItem> feedItems = new ArrayList<>();
for (WorksResponse work : hotWorks) {
feedItems.add(FeedItem.fromWorks(work));
}
adapter.submitList(feedItems);
}
}
/**
* 刷新热门作品
*/
private void refreshHotWorks() {
currentHotWorksPage = 1;
hasMoreHotWorks = true;
loadHotWorks(1);
}
/**
* 加载更多热门作品
*/
private void loadMoreHotWorks() {
if (!isLoadingHotWorks && hasMoreHotWorks) {
currentHotWorksPage++;
loadHotWorks(currentHotWorksPage);
}
}
```
#### 修改 categoryTabs 初始化
`activity_main.xml` 中的 `categoryTabs` 添加"热门"Tab
```xml
<com.google.android.material.tabs.TabLayout
android:id="@+id/categoryTabs"
...>
<!-- 新增热门Tab -->
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="热门" />
<com.google.android.material.tabs.TabItem
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="推荐" />
<!-- 其他Tab保持不变 -->
...
</com.google.android.material.tabs.TabLayout>
```
#### 修改 categoryTabs 监听器
```java
binding.categoryTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
if (tab == null) return;
CharSequence title = tab.getText();
currentCategory = title != null ? title.toString() : "热门";
// 保存选中的分类
CategoryFilterManager.saveLastCategory(MainActivity.this, currentCategory);
// 根据分类加载数据
if ("热门".equals(currentCategory)) {
// 加载热门作品
refreshHotWorks();
} else {
// 应用其他分类筛选
applyCategoryFilterWithAnimation(currentCategory);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {
if (tab == null) return;
CharSequence title = tab.getText();
currentCategory = title != null ? title.toString() : "热门";
if ("热门".equals(currentCategory)) {
// 刷新热门作品
refreshHotWorks();
} else {
applyCategoryFilterWithAnimation(currentCategory);
}
}
});
```
#### 修改下拉刷新逻辑
```java
binding.swipeRefresh.setOnRefreshListener(() -> {
if ("热门".equals(currentCategory)) {
refreshHotWorks();
} else {
fetchDiscoverRooms();
}
});
```
#### 修改滚动加载更多逻辑
```java
binding.roomsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy <= 0) return;
RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
if (!(lm instanceof GridLayoutManager)) return;
GridLayoutManager glm = (GridLayoutManager) lm;
int total = glm.getItemCount();
int lastVisible = glm.findLastVisibleItemPosition();
if (total <= 0) return;
if (lastVisible >= total - 4) {
if ("热门".equals(currentCategory)) {
loadMoreHotWorks();
} else {
// 其他分类的加载逻辑
}
}
}
});
```
### 2. WorksAdapter 修改
#### 添加热门标识显示
`item_works.xml` 布局中添加热门标识:
```xml
<!-- 热门标识 -->
<ImageView
android:id="@+id/hotBadge"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_margin="8dp"
android:src="@drawable/ic_fire_24"
android:visibility="gone"
app:tint="#FF4500" />
```
#### 修改 ViewHolder 的 bind 方法
```java
public void bind(WorksResponse works, OnWorksClickListener listener) {
if (works == null) return;
// ... 现有代码 ...
// 显示热门标识
ImageView hotBadge = itemView.findViewById(R.id.hotBadge);
if (hotBadge != null) {
Boolean isHot = works.getIsHot();
if (isHot != null && isHot) {
hotBadge.setVisibility(View.VISIBLE);
} else {
hotBadge.setVisibility(View.GONE);
}
}
// ... 现有代码 ...
}
```
### 3. WorksResponse 数据模型
确保 `WorksResponse` 类包含 `isHot` 字段:
```java
public class WorksResponse {
private Long id;
private String title;
private String coverUrl;
private String videoUrl;
private String type;
private Integer userId;
private String userName;
private String authorName;
private Integer likeCount;
private Boolean isLiked;
private Boolean isHot; // 新增:是否热门
private String hotTime; // 新增:设置热门的时间
// Getters and Setters
public Boolean getIsHot() {
return isHot;
}
public void setIsHot(Boolean isHot) {
this.isHot = isHot;
}
public String getHotTime() {
return hotTime;
}
public void setHotTime(String hotTime) {
this.hotTime = hotTime;
}
}
```
### 4. ApiService 接口
已存在的接口:
```java
/**
* 获取热门作品列表
*/
@GET("api/works/hot")
Call<ApiResponse<PageResponse<WorksResponse>>> getHotWorks(
@Query("page") int page,
@Query("pageSize") int pageSize);
```
## Data Models
### WorksResponse
```java
public class WorksResponse {
private Long id; // 作品ID
private String title; // 作品标题
private String coverUrl; // 封面图片URL
private String videoUrl; // 视频URL
private String type; // 作品类型VIDEO/IMAGE
private Integer userId; // 用户ID
private String userName; // 用户名
private String authorName; // 作者名
private Integer likeCount; // 点赞数
private Boolean isLiked; // 当前用户是否已点赞
private Boolean isHot; // 是否热门
private String hotTime; // 设置热门的时间
}
```
### PageResponse<T>
```java
public class PageResponse<T> {
private List<T> list; // 数据列表
private Integer total; // 总数
private Integer page; // 当前页码
private Integer pageSize; // 每页大小
private Integer totalPage; // 总页数
}
```
### ApiResponse<T>
```java
public class ApiResponse<T> {
private Integer code; // 响应码200表示成功
private String message; // 响应消息
private T data; // 响应数据
public boolean isOk() {
return code != null && code == 200;
}
}
```
## Data Models
### 数据库字段(后端)
```sql
-- eb_works 表
ALTER TABLE `eb_works`
ADD COLUMN `is_hot` tinyint DEFAULT 0 COMMENT '是否热门1-是 0-否' AFTER `status`,
ADD COLUMN `hot_time` datetime DEFAULT NULL COMMENT '设置热门的时间' AFTER `is_hot`,
ADD INDEX `idx_is_hot` (`is_hot`);
```
### API 请求参数
```
GET /api/works/hot?page=1&pageSize=20
```
### API 响应格式
```json
{
"code": 200,
"message": "success",
"data": {
"list": [
{
"id": 1,
"title": "精彩作品",
"coverUrl": "https://example.com/cover.jpg",
"videoUrl": "https://example.com/video.mp4",
"type": "VIDEO",
"userId": 10,
"userName": "用户A",
"authorName": "作者A",
"likeCount": 100,
"isLiked": false,
"isHot": true,
"hotTime": "2026-01-08 10:00:00"
}
],
"total": 50,
"page": 1,
"pageSize": 20,
"totalPage": 3
}
}
```
## Error Handling
### 错误场景处理
1. **网络错误**
- 显示错误提示:"网络错误,请重试"
- 提供重试按钮
- 保留已加载的数据
2. **API 返回错误**
- 显示服务器返回的错误消息
- 提供重试按钮
3. **空数据**
- 显示空状态视图:"暂无热门作品"
- 提供刷新按钮
4. **加载超时**
- 设置合理的超时时间30秒
- 显示超时提示
- 提供重试选项
### 错误处理代码
```java
@Override
public void onFailure(Call<ApiResponse<PageResponse<WorksResponse>>> call, Throwable t) {
isLoadingHotWorks = false;
binding.swipeRefresh.setRefreshing(false);
String errorMsg = "网络错误";
if (t != null) {
if (t.getMessage() != null && t.getMessage().contains("Unable to resolve host")) {
errorMsg = "无法连接服务器,请检查网络";
} else if (t.getMessage() != null && t.getMessage().contains("timeout")) {
errorMsg = "连接超时,请重试";
} else if (t.getMessage() != null && t.getMessage().contains("Connection refused")) {
errorMsg = "连接被拒绝,请确保服务器已启动";
}
}
showErrorState(errorMsg);
}
```
## Testing Strategy
### Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system-essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
#### Property 1: 热门Tab仅显示作品内容
*For any* 热门Tab中显示的数据项该数据项应该是WorksResponse类型不应包含Room直播间类型的数据
**Validates: Requirements 2.3**
#### Property 2: 热门作品显示热门标识
*For any* 在热门Tab中显示的作品如果该作品的isHot字段为true则作品卡片上应该显示热门图标hotBadge可见
**Validates: Requirements 3.1**
### Unit Tests
1. **MainActivity 单元测试**
- 测试"热门"Tab在categoryTabs的索引为0最左侧**Validates: Requirements 1.1, 1.2**
- 测试categoryTabs包含6个Tab顺序为热门、推荐、直播、视频、音乐、游戏 **Validates: Requirements 1.4**
- 测试进入"发现"Tab时默认选中"热门"Tab **Validates: Requirements 1.3**
- 测试点击"热门"Tab调用loadHotWorks方法 **Validates: Requirements 2.1**
- 测试loadHotWorks方法调用ApiService.getHotWorks接口 **Validates: Requirements 2.1**
- 测试接收到数据后WorksAdapter被正确更新 **Validates: Requirements 2.2**
- 测试加载中显示骨架屏或加载动画 **Validates: Requirements 2.4**
- 测试加载失败显示错误提示 **Validates: Requirements 2.5**
- 测试空数据显示"暂无热门作品" **Validates: Requirements 2.6**
- 测试下拉刷新触发refreshHotWorks方法 **Validates: Requirements 4.1**
- 测试刷新时SwipeRefreshLayout.isRefreshing为true **Validates: Requirements 4.2**
- 测试刷新完成后isRefreshing为false且列表更新 **Validates: Requirements 4.3**
- 测试刷新失败显示错误提示且保留原数据 **Validates: Requirements 4.4**
- 测试滚动到底部触发loadMoreHotWorks方法 **Validates: Requirements 5.1**
- 测试加载更多时显示底部加载指示器 **Validates: Requirements 5.2**
- 测试hasMoreHotWorks为false时显示"没有更多内容" **Validates: Requirements 5.3**
- 测试加载更多失败显示错误提示 **Validates: Requirements 5.4**
- 测试切换Tab后hotWorks列表数据保留 **Validates: Requirements 6.1**
- 测试返回热门Tab后列表数据和滚动位置恢复 **Validates: Requirements 6.2**
- 测试停留超过5分钟后返回触发自动刷新 **Validates: Requirements 6.3**
2. **WorksAdapter 单元测试**
- 测试isHot=true时hotBadge可见 **Validates: Requirements 3.1**
- 测试isHot=false时hotBadge不可见 **Validates: Requirements 3.1**
- 测试点击作品触发onWorksClick回调 **Validates: Requirements 3.3**
- 测试点击作品启动WorkDetailActivity **Validates: Requirements 3.3**
3. **API 接口测试**
- 测试getHotWorks接口返回PageResponse<WorksResponse>
- 测试page和pageSize参数正确传递
- 测试错误响应正确处理
### Integration Tests
1. **端到端测试**
- 测试从点击"热门"Tab到显示作品列表的完整流程
- 测试下拉刷新功能
- 测试上拉加载更多功能
- 测试点击作品跳转到详情页
2. **UI 测试**
- 测试"热门"Tab在最左侧显示
- 测试热门标识🔥正确显示
- 测试加载动画正确显示
- 测试空状态和错误状态正确显示
### 测试数据准备
```sql
-- 准备测试数据:设置一些作品为热门
UPDATE eb_works SET is_hot = 1, hot_time = NOW() WHERE id IN (1, 2, 3, 4, 5);
```
### 测试用例
| 测试用例 | 输入 | 预期输出 |
|---------|------|---------|
| 加载热门作品 | page=1, pageSize=20 | 返回20条热门作品 |
| 空数据 | 数据库无热门作品 | 显示"暂无热门作品" |
| 网络错误 | 网络断开 | 显示"网络错误,请重试" |
| 下拉刷新 | 下拉列表 | 重新加载第1页数据 |
| 上拉加载更多 | 滚动到底部 | 加载下一页数据 |
| 点击作品 | 点击作品卡片 | 跳转到作品详情页 |