
一. 引言
在上一篇文章里,我们从零开始实现了 App 的 发现页面,通过网络请求获取数据,并使用 RecyclerView 展示了剧集列表。
但光有发现页还不够,用户在点击一部剧时,自然希望进入到一个更详细的页面,去查看它的简介、标签以及剧集列表。本篇我们就来实现 发现详情页。
主要包含以下内容:
- 从发现页跳转到详情页(Activity 跳转与传值)
- 详情页的 UI 布局(背景、Toolbar、RecyclerView)
- RecyclerView 多类型布局(头部 + 剧集列表)
- ViewModel + LiveData 数据驱动(自动刷新 UI)
通过这一篇,你将掌握 Android 开发中常见的"跳转 → 数据传递 → 多类型列表 → 数据绑定"的完整流程。
二. 从发现页跳转到详情页
2.1 发送跳转
在发现页的 Adapter 中,我们可以为每一个剧集的 Item 添加点击事件,然后通过 Intent 启动 DiscoverDetailActivity,并把 DiscoverDrama 对象传递过去:
Kotlin
val intent = Intent(context, DiscoverDetailActivity::class.java)
intent.putExtra("drama", drama) // drama 是 DiscoverDrama 类型
context.startActivity(intent)
这里我们用到了 putExtra,因为 DiscoverDrama 已经实现了 Serializable,所以可以直接传递。
2.2 接收参数
在 DiscoverDetailActivity 中,通过 intent.getSerializableExtra 来接收数据:
Kotlin
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun initData() {
discoverDrama = intent.getSerializableExtra("drama", DiscoverDrama::class.java)
}
这样我们就能在详情页中拿到用户点击的剧集信息,并用于后续的 UI 展示和数据请求。
三. 详情页整体布局概览
在详情页,我们主要分为三个部分:
1. 背景与 Toolbar
- 页面顶部是一个渐变背景 (View) 和透明的 MaterialToolbar,用于展示标题"剧集详情"。
- 使用 enableEdgeToEdge() 和 WindowInsetsCompat 处理状态栏高度,让内容贴合屏幕边缘。
2. RecyclerView
占据主体区域,用于展示两类内容:
- 头部信息:封面、标题、描述、标签、词汇量
- 剧集列表:每一集的标题、文件大小、下载状态等
3. 布局特点
- RecyclerView 采用 LinearLayoutManager 垂直排列。
- 头部视图与列表项通过 Adapter 的 getItemViewType 区分,实现多类型布局。
- 数据完全通过 ViewModel + LiveData 绑定到 RecyclerView,无需在 Activity 中手动更新视图。
这种布局方式简洁而高效,既能展示剧集的详细信息,也便于扩展后续功能(例如下载按钮或播放按钮)。
四. RecyclerView 多类型布局实现
发现详情页中,我们的 RecyclerView 既要展示 头部信息 ,又要展示 剧集列表 。为此,我们采用 多类型布局的方式,实现两类 ViewHolder:
4.1 Adapter 设计
Kotlin
class DiscoverDetailAdapter(
private val discoverDrama: DiscoverDrama
): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val TYPE_HEADER = 0
const val TYPE_CONTENT = 1
}
private var episodes: List<DiscoverEpisode> = emptyList()
override fun getItemViewType(position: Int): Int {
return if (position == 0) TYPE_HEADER else TYPE_CONTENT
}
override fun getItemCount(): Int = episodes.size + 1
}
- 第一个位置 (position == 0) 是 头部视图
- 其余位置为 剧集列表
- getItemCount() 返回 episodes.size + 1,因为头部占一行
4.2 ViewHolder 绑定数据
头部视图 (HeaderViewHolder):
- 显示剧封面、标题、描述、标签、词汇量
- 使用 Glide 加载封面图片
- 标签动态生成 TextView 并添加到 LinearLayout
Kotlin
class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val cover = itemView.findViewById<ImageView>(R.id.ivCover)
private val title = itemView.findViewById<TextView>(R.id.tvTitle)
private val desc = itemView.findViewById<TextView>(R.id.tvDesc)
private val wordCount = itemView.findViewById<TextView>(R.id.tvVocab)
private val tagContainer = itemView.findViewById<LinearLayout>(R.id.tagContainer)
fun bindData(drama: DiscoverDrama) {
Glide.with(itemView.context).load(drama.realCoverUrl).into(cover)
title.text = drama.title
desc.text = drama.description
wordCount.text = "词汇量: ${drama.vocabularyCount ?: 0}"
tagContainer.removeAllViews()
drama.tags?.split(",")?.forEach { tag ->
val tv = TextView(itemView.context).apply {
text = tag
// 背景、圆角、透明度等样式
}
tagContainer.addView(tv)
}
}
}
剧集列表视图 (EpisodeViewHolder):
- 显示剧集标题、文件大小、下载状态
- 预留下载逻辑和进度条
Kotlin
class EpisodeViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
private val title: TextView = itemView.findViewById(R.id.episodeTitle)
private val size: TextView = itemView.findViewById(R.id.episodeSize)
private val statusIcon: ImageView = itemView.findViewById(R.id.statusIcon)
private val statusProgress: ProgressBar = itemView.findViewById(R.id.statusProgress)
fun bindData(episode: DiscoverEpisode) {
title.text = "${episode.index}. ${episode.title}"
size.text = episode.fileSize ?: ""
// 下载状态逻辑可在此扩展
}
}
4.3 数据更新
- 通过 ViewModel 获取剧集列表数据
- 使用 LiveData 观察数据变化,并调用 Adapter 的 setEpisodes() 更新 RecyclerView
Kotlin
viewModel.episodes.observe(this) { episodes ->
adapter.setEpisodes(episodes)
}
这样实现了 Activity 不直接操作 RecyclerView 的思想,保证了 UI 与数据的分离。
五. 数据获取与绑定流程
在详情页中,剧集列表的数据来源于网络请求。为了实现 UI 与数据分离 ,我们采用 ViewModel + LiveData 的方式管理数据。
5.1 ViewModel 请求数据
DiscoverDetailViewModel 负责请求剧集列表,并将结果通过 LiveData 暴露给 UI:
Kotlin
class DiscoverDetailViewModel : ViewModel() {
val episodes = MutableLiveData<List<DiscoverEpisode>>()
val isLoading = MutableLiveData<Boolean>()
private val discoverDramaRepository by lazy { DiscoverRespository() }
fun fetchEpisodes(drama: DiscoverDrama) {
viewModelScope.launch {
isLoading.value = true
val result = discoverDramaRepository.fetchEpisodes(drama)
result.onSuccess {
println("获取剧集 ${drama.title} 的集列表成功: ${it.size} 条数据")
episodes.value = it
}.onFailure {
episodes.value = emptyList()
}
isLoading.value = false
}
}
}
- viewModelScope.launch 在协程中发起网络请求,保证不会阻塞 UI 线程
- 成功时,将数据赋值给 episodes LiveData
- 失败时,清空列表,保证 RecyclerView 安全更新
5.2 Activity 观察数据
在 DiscoverDetailActivity 中,RecyclerView Adapter 不直接请求数据,而是 观察 LiveData:
Kotlin
viewModel.episodes.observe(this) { episodes ->
adapter.setEpisodes(episodes)
Log.d("DiscoverDetailActivity", "Episodes updated: ${episodes.size} items")
}
- 当 LiveData 更新时,Adapter 自动刷新 RecyclerView
- Activity 只负责 UI 初始化和 LiveData 绑定,无需手动刷新列表
5.3 请求与展示流程总结
- Activity 启动后,通过 Intent 获取 DiscoverDrama 参数
- 调用 viewModel.fetchEpisodes(drama) 发起网络请求
- ViewModel 请求成功后,将数据赋值给 LiveData
- Activity 观察 LiveData,并将数据传递给 Adapter
- Adapter 更新 RecyclerView,实现 UI 自动刷新
六.运行效果与总结

6.1 最终效果展示
- 用户在 发现页面 点击某部剧集
- 页面跳转到 详情页
- 页面顶部展示剧的封面、标题、描述、标签和词汇量
- 下方 RecyclerView 展示剧集列表,每一集显示标题、文件大小和下载状态(可扩展)
- UI 完全响应 LiveData 数据更新,无需手动刷新
6.2 本篇收获
通过这一篇文章,我们掌握了:
Activity 跳转与参数传递
- 使用 Intent 传递 Serializable 对象
- 在目标 Activity 中安全接收数据
RecyclerView 多类型布局
- 头部视图 + 列表视图
- Adapter 分类型管理 ViewHolder
ViewModel + LiveData 数据驱动 UI
- Activity 不直接操作数据
- RecyclerView 自动响应数据变化
这种模式不仅使代码清晰、可维护,还符合 Android 架构最佳实践。