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

19 KiB
Raw Blame History

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 修改

新增成员变量

// 热门作品相关
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

错误场景处理

  1. 网络错误

    • 显示错误提示:"网络错误,请重试"
    • 提供重试按钮
    • 保留已加载的数据
  2. API 返回错误

    • 显示服务器返回的错误消息
    • 提供重试按钮
  3. 空数据

    • 显示空状态视图:"暂无热门作品"
    • 提供刷新按钮
  4. 加载超时

    • 设置合理的超时时间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

  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
    • 测试page和pageSize参数正确传递
    • 测试错误响应正确处理

Integration Tests

  1. 端到端测试

    • 测试从点击"热门"Tab到显示作品列表的完整流程
    • 测试下拉刷新功能
    • 测试上拉加载更多功能
    • 测试点击作品跳转到详情页
  2. 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页数据
上拉加载更多 滚动到底部 加载下一页数据
点击作品 点击作品卡片 跳转到作品详情页