19 KiB
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 的作品列表
数据流
- 用户操作 → 点击"热门"Tab
- MainActivity → 调用
loadHotWorks(page)方法 - ApiService → 发送 GET 请求到
/api/works/hot - 后端服务器 → 查询
is_hot=1的作品,按hot_time降序排序 - ApiService → 接收
PageResponse<WorksResponse>数据 - WorksAdapter → 渲染作品列表
- UI → 显示热门作品,每个作品卡片显示🔥标识
Components and Interfaces
1. MainActivity 修改
新增成员变量
// 热门作品相关
private final List<WorksResponse> hotWorks = new ArrayList<>(); // 热门作品列表
private int currentHotWorksPage = 1; // 当前热门作品页码
private boolean isLoadingHotWorks = false; // 是否正在加载热门作品
private boolean hasMoreHotWorks = true; // 是否还有更多热门作品
新增方法
/**
* 加载热门作品列表
* @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:
<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 监听器
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);
}
}
});
修改下拉刷新逻辑
binding.swipeRefresh.setOnRefreshListener(() -> {
if ("热门".equals(currentCategory)) {
refreshHotWorks();
} else {
fetchDiscoverRooms();
}
});
修改滚动加载更多逻辑
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 布局中添加热门标识:
<!-- 热门标识 -->
<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 方法
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 字段:
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 接口
已存在的接口:
/**
* 获取热门作品列表
*/
@GET("api/works/hot")
Call<ApiResponse<PageResponse<WorksResponse>>> getHotWorks(
@Query("page") int page,
@Query("pageSize") int pageSize);
Data Models
WorksResponse
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
public class PageResponse<T> {
private List<T> list; // 数据列表
private Integer total; // 总数
private Integer page; // 当前页码
private Integer pageSize; // 每页大小
private Integer totalPage; // 总页数
}
ApiResponse
public class ApiResponse<T> {
private Integer code; // 响应码(200表示成功)
private String message; // 响应消息
private T data; // 响应数据
public boolean isOk() {
return code != null && code == 200;
}
}
Data Models
数据库字段(后端)
-- 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 响应格式
{
"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
错误场景处理
-
网络错误
- 显示错误提示:"网络错误,请重试"
- 提供重试按钮
- 保留已加载的数据
-
API 返回错误
- 显示服务器返回的错误消息
- 提供重试按钮
-
空数据
- 显示空状态视图:"暂无热门作品"
- 提供刷新按钮
-
加载超时
- 设置合理的超时时间(30秒)
- 显示超时提示
- 提供重试选项
错误处理代码
@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
-
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
-
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
-
API 接口测试
- 测试getHotWorks接口返回PageResponse
- 测试page和pageSize参数正确传递
- 测试错误响应正确处理
Integration Tests
-
端到端测试
- 测试从点击"热门"Tab到显示作品列表的完整流程
- 测试下拉刷新功能
- 测试上拉加载更多功能
- 测试点击作品跳转到详情页
-
UI 测试
- 测试"热门"Tab在最左侧显示
- 测试热门标识🔥正确显示
- 测试加载动画正确显示
- 测试空状态和错误状态正确显示
测试数据准备
-- 准备测试数据:设置一些作品为热门
UPDATE eb_works SET is_hot = 1, hot_time = NOW() WHERE id IN (1, 2, 3, 4, 5);
测试用例
| 测试用例 | 输入 | 预期输出 |
|---|---|---|
| 加载热门作品 | page=1, pageSize=20 | 返回20条热门作品 |
| 空数据 | 数据库无热门作品 | 显示"暂无热门作品" |
| 网络错误 | 网络断开 | 显示"网络错误,请重试" |
| 下拉刷新 | 下拉列表 | 重新加载第1页数据 |
| 上拉加载更多 | 滚动到底部 | 加载下一页数据 |
| 点击作品 | 点击作品卡片 | 跳转到作品详情页 |