Android入门到实战(八):从发现页到详情页——跳转、传值与RecyclerView多类型布局
一. 引言
在上一篇文章里,我们从零开始实现了 App 的 发现页面,通过网络请求获取数据,并使用 RecyclerView 展示了剧集列表。
但光有发现页还不够,用户在点击一部剧时,自然希望进入到一个更详细的页面,去查看它的简介、标签以及剧集列表。本篇我们就来实现 发现详情页。
主要包含以下内容:
- 从发现页跳转到详情页(Activity 跳转与传值)
- 详情页的 UI 布局(背景、Toolbar、RecyclerView)
- RecyclerView 多类型布局(头部 + 剧集列表)
- ViewModel + LiveData 数据驱动(自动刷新 UI)
通过这一篇,你将掌握 Android 开发中常见的“跳转 → 数据传递 → 多类型列表 → 数据绑定”的完整流程。
二. 从发现页跳转到详情页
2.1 发送跳转
在发现页的 Adapter 中,我们可以为每一个剧集的 Item 添加点击事件,然后通过 Intent 启动 DiscoverDetailActivity,并把 DiscoverDrama 对象传递过去:
val intent = Intent(context, DiscoverDetailActivity::class.java)
intent.putExtra("drama", drama) // drama 是 DiscoverDrama 类型
context.startActivity(intent)
这里我们用到了 putExtra,因为 DiscoverDrama 已经实现了 Serializable,所以可以直接传递。
2.2 接收参数
在 DiscoverDetailActivity 中,通过 intent.getSerializableExtra 来接收数据:
@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 设计
class DiscoverDetailAdapter(private val discoverDrama: DiscoverDrama
): RecyclerView.Adapter<RecyclerView.ViewHolder>() {companion object {const val TYPE_HEADER = 0const 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
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.titledesc.text = drama.descriptionwordCount.text = "词汇量: ${drama.vocabularyCount ?: 0}"tagContainer.removeAllViews()drama.tags?.split(",")?.forEach { tag ->val tv = TextView(itemView.context).apply {text = tag// 背景、圆角、透明度等样式}tagContainer.addView(tv)}}
}
剧集列表视图 (EpisodeViewHolder):
- 显示剧集标题、文件大小、下载状态
- 预留下载逻辑和进度条
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
viewModel.episodes.observe(this) { episodes ->adapter.setEpisodes(episodes)
}
这样实现了 Activity 不直接操作 RecyclerView 的思想,保证了 UI 与数据的分离。
五. 数据获取与绑定流程
在详情页中,剧集列表的数据来源于网络请求。为了实现 UI 与数据分离,我们采用 ViewModel + LiveData 的方式管理数据。
5.1 ViewModel 请求数据
DiscoverDetailViewModel 负责请求剧集列表,并将结果通过 LiveData 暴露给 UI:
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 = trueval 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:
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 架构最佳实践。