当前位置: 首页 > news >正文

Android入门到实战(九):实现书架页——RecyclerView + GridLayoutManager + 本地数据库


一. 引言

在前几章中,我们实现了发现页,能够从网络获取推荐内容并展示给用户。而书架页则有着不同的特点:它主要展示用户已经保存或观看过的剧集数据,数据来源是本地数据库,而不是网络请求。

本章的目标是带大家从零实现书架页的核心功能,包括:

  • 使用 RecyclerView + GridLayoutManager 展示剧集网格
  • 通过 Adapter + ViewHolder 绑定数据
  • 利用 ViewModel + Room 管理本地数据
  • 实现空状态提示,当书架为空时给出友好提示

通过这篇文章,你将能够掌握如何结合 RecyclerView 与本地数据库 来构建一个实用的书架页,为用户提供直观、流畅的内容展示体验。

二. 页面布局设计(XML 部分)

书架页的整体布局使用 ConstraintLayout,包括三个核心部分:渐变背景、顶部导航栏(Toolbar)、以及主要的内容区(RecyclerView)。此外,我们还为书架为空的情况准备了一个 TextView 来提示用户“暂无内容”。 

2.1 整体结构
<androidx.constraintlayout.widget.ConstraintLayoutxmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"android:layout_width="match_parent"android:layout_height="match_parent"><!-- 渐变背景 --><Viewandroid:id="@+id/header_bg"android:layout_width="match_parent"android:layout_height="match_parent"android:background="@drawable/library_gradient_bg" /><!-- 顶部导航栏 --><com.google.android.material.appbar.MaterialToolbarandroid:id="@+id/toolbar"android:layout_width="0dp"android:layout_height="?attr/actionBarSize"android:background="@android:color/transparent"android:titleTextColor="@android:color/black"app:layout_constraintTop_toTopOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"/><!-- 书架内容列表 --><androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recyclerView"android:layout_width="match_parent"android:layout_height="0dp"android:paddingStart="24dp"android:paddingEnd="24dp"android:clipToPadding="false"android:clipChildren="false"app:layout_constraintTop_toBottomOf="@id/toolbar"app:layout_constraintBottom_toBottomOf="parent" /><!-- 空状态提示 --><TextViewandroid:id="@+id/tip_view"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="暂无内容"android:textSize="16sp"android:textColor="@android:color/darker_gray"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>

这段布局代码中,我们通过 ConstraintLayout 让 RecyclerView 占据除了 Toolbar 外的整个内容区,同时在书架为空时显示 tip_view。

2.2 RecyclerView 配置

在 Fragment 或 Activity 中,我们需要给 RecyclerView 设置 GridLayoutManager,让书架呈现两列网格的效果:

val recyclerView = binding.recyclerView
recyclerView.layoutManager = GridLayoutManager(context, 2)
recyclerView.adapter = libraryAdapter// 设置网格间距
val spacing = resources.getDimensionPixelSize(R.dimen.grid_spacing)
recyclerView.addItemDecoration(GridSpacingItemDecoration(2, spacing))

这样,每个 item 都会根据列数均匀分配左右间距,视觉效果更整齐。

2.3 Item 布局设计

书架每个 item 主要包括两部分:

  1. 上半部分:封面图 + 标题覆盖
  2. 下半部分:集数 + 更多按钮
<FrameLayout ...><LinearLayoutandroid:orientation="vertical"android:layout_width="match_parent"android:layout_height="wrap_content"><!-- 封面图 + 标题 --><androidx.constraintlayout.widget.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><androidx.cardview.widget.CardViewandroid:layout_width="0dp"android:layout_height="0dp"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintDimensionRatio="210:297"app:cardCornerRadius="4dp"app:cardElevation="0dp"><ImageViewandroid:id="@+id/coverImageView"android:layout_width="match_parent"android:layout_height="match_parent"android:scaleType="centerCrop"/></androidx.cardview.widget.CardView><TextViewandroid:id="@+id/titleTextView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:padding="6dp"android:background="#66000000"android:text="剧名"android:textColor="@android:color/white"android:textSize="16sp"android:textStyle="bold"app:layout_constraintTop_toTopOf="@id/coverCard"app:layout_constraintBottom_toBottomOf="@id/coverCard"app:layout_constraintStart_toStartOf="@id/coverCard"app:layout_constraintEnd_toEndOf="@id/coverCard"/></androidx.constraintlayout.widget.ConstraintLayout><!-- 底部:集数 + 更多按钮 --><LinearLayoutandroid:orientation="horizontal"android:layout_width="match_parent"android:layout_height="50dp"android:gravity="center_vertical"android:paddingHorizontal="8dp"><TextViewandroid:id="@+id/episodeCountTextView"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="10集"android:textColor="@android:color/darker_gray"android:textSize="14sp"/><Viewandroid:layout_width="0dp"android:layout_height="0dp"android:layout_weight="1"/><ImageViewandroid:id="@+id/moreButton"android:layout_width="24dp"android:layout_height="24dp"android:src="@drawable/ic_more_hori" /></LinearLayout></LinearLayout>
</FrameLayout>

这个布局保证了每个剧集封面整齐显示,并在封面上叠加标题,同时下方展示集数信息和操作按钮,便于后续扩展操作(比如跳转详情、管理书架等)。

三. Adapter 与 ViewHolder 实现

在书架页中,RecyclerView 的展示效果依赖于 Adapter 和 ViewHolder。Adapter 负责把数据绑定到对应的 item 上,而 ViewHolder 则封装 item 的各个视图控件,提高 RecyclerView 的性能。

3.1 LibraryAdapter

LibraryAdapter 是书架页的核心 Adapter 类,它接收剧集列表,并通过回调处理 item 点击事件:

class LibraryAdapter(private val context: Context,private val onItemClick: (DramaWithEpisodeCount) -> Unit
) : RecyclerView.Adapter<LibraryAdapter.LibraryViewHolder>() {private var items: List<DramaWithEpisodeCount> = emptyList()private val fileHelper = FileHelper()/** 更新剧列表 */fun updateItems(newItems: List<DramaWithEpisodeCount>) {items = newItemsnotifyDataSetChanged() // 通知 RecyclerView 刷新}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LibraryViewHolder {val view = View.inflate(parent.context, R.layout.library_item_view, null)return LibraryViewHolder(view)}override fun onBindViewHolder(holder: LibraryViewHolder, position: Int) {val item = items[position]holder.bind(item)// 设置点击事件holder.itemView.setOnClickListener {onItemClick(item)}// 加载封面图片val cover = fileHelper.getDramaCoverImagePath(context, item.drama)Glide.with(context).load(cover).into(holder.coverImageView)}override fun getItemCount(): Int = items.size
}

重点说明:

  • updateItems:当书架数据更新时调用,用来刷新 RecyclerView。
  • onItemClick:通过 Lambda 回调处理点击事件,可以用来跳转到详情页。
  • Glide:加载本地封面图,保证图片显示流畅且节省内存。

3.2 LibraryViewHolder

LibraryViewHolder 封装了 item 中的视图控件,并提供 bind 方法将数据渲染到界面上:

class LibraryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {val coverImageView: ImageView = itemView.findViewById(R.id.coverImageView)val titleTextView: TextView = itemView.findViewById(R.id.titleTextView)val episodeTextView: TextView = itemView.findViewById(R.id.episodeCountTextView)val moreTextView: ImageView = itemView.findViewById(R.id.moreButton)fun bind(item: DramaWithEpisodeCount) {titleTextView.text = item.drama.titleepisodeTextView.text = "${item.episodeCount}集"}
}

说明:

  • coverImageView 和 titleTextView:显示剧集封面和标题。
  • episodeTextView:显示剧集集数。
  • moreTextView:可以用于扩展操作(如管理书架、收藏等)。
  • bind 方法:将 DramaWithEpisodeCount 数据绑定到 UI 元素上。

3.3 点击与扩展

通过 Adapter 提供的点击回调,我们可以轻松实现:

  • 点击封面或标题 → 跳转到剧集详情页
  • 点击更多按钮 → 弹出操作菜单(如删除、移动等)

这种结构让书架页的逻辑清晰且可扩展,后续功能可以在 onItemClick 或 moreButton 中增加,而不影响核心显示逻辑。

四. 网格间距控制(ItemDecoration)

在书架页中,每个剧集 item 都在 RecyclerView 的网格中排列。如果不加任何间距,item 之间会紧贴在一起,看起来比较拥挤。为了解决这个问题,我们使用了自定义的 ItemDecoration 来统一管理每个 item 的左右间距。

4.1 GridSpacingItemDecoration 实现
class GridSpacingItemDecoration(private val spanCount: Int,private val spacing: Int
) : RecyclerView.ItemDecoration() {override fun getItemOffsets(outRect: android.graphics.Rect,view: android.view.View,parent: RecyclerView,state: RecyclerView.State) {val position = parent.getChildAdapterPosition(view) // item 索引val column = position % spanCount                   // item 所在列// 左右间距均分outRect.left = spacing * column / spanCountoutRect.right = spacing - (column + 1) * spacing / spanCount}
}

说明:

  • spanCount:网格列数(例如两列)。
  • spacing:item 之间的总间距(单位:像素)。
  • 通过计算列的位置,将左右间距均匀分配,使网格左右边缘对齐,并且中间间距一致。

4.2 RecyclerView 中使用

在初始化 RecyclerView 时,添加 ItemDecoration:

val spacing = resources.getDimensionPixelSize(R.dimen.grid_spacing)
recyclerView.addItemDecoration(GridSpacingItemDecoration(2, spacing))

这样,每个剧集 item 的左右间距会自动计算并应用到视图中,整体布局更加美观。

五. 数据层设计:ViewModel + Room

书架页的数据来源不同于发现页,它直接从 本地数据库(Room) 中读取,而不是调用网络接口。为了实现数据与 UI 的解耦,我们使用 ViewModel 配合 StateFlow 来管理剧集数据。

5.1 数据模型 Drama

数据库表 drama 映射到 Kotlin 数据类 Drama:

@Entity(tableName = "drama")
data class Drama(@PrimaryKey val id: String,@ColumnInfo(name = "cover_file_name") var coverFileName: String?,@ColumnInfo(name = "index") var index: Long,@ColumnInfo(name = "pay_start_episode") var payStartEpisode: Int,@ColumnInfo(name = "product_id") var productId: String?,@ColumnInfo(name = "title") var title: String?,@ColumnInfo(name = "type") var type: Long
) : Serializable

字段说明:

  • id:主键,用于唯一标识剧集。
  • coverFileName:封面图片文件名。
  • index:排序字段,控制在书架中的显示顺序。
  • payStartEpisode:从哪一集开始付费。
  • title:剧集标题。
  • 其他字段可根据业务扩展。

5.2 LibraryViewModel

ViewModel 负责读取数据库数据并通知 UI 更新:

class LibraryViewModel : ViewModel() {private val repository by lazy { DramaRepository() }private val _dramas = MutableStateFlow<List<DramaWithEpisodeCount>>(emptyList())val dramas: StateFlow<List<DramaWithEpisodeCount>> = _dramas/** 从数据库获取剧集列表 */fun fetchDramas() {viewModelScope.launch {val dramaList = withContext(Dispatchers.IO) {repository.getAllDramasWithEpisodeCount()}_dramas.value = dramaList}}
}

说明:

  • _dramas:私有可变 StateFlow,存储书架页的剧集数据。
  • dramas:暴露给 UI 的只读 StateFlow。
  • fetchDramas():从 Repository 异步获取数据,保证不会阻塞主线程。

5.3 数据与 UI 的绑定

在 Fragment 中,使用 Kotlin 的 Flow 收集数据,并更新 Adapter:

lifecycleScope.launchWhenStarted {viewModel.dramas.collect { dramaList ->libraryAdapter.updateItems(dramaList)tipView.isVisible = dramaList.isEmpty()}
}

流程说明:

  1. 打开页面 → ViewModel 调用 fetchDramas()。
  2. Repository 从 Room 获取本地剧集列表。
  3. ViewModel 更新 _dramas → UI 收集到最新数据。
  4. Adapter 调用 updateItems() 刷新 RecyclerView。
  5. 如果书架为空,显示空状态提示。

六. 书架页完整运行流程

结合前面几节内容,我们把书架页从页面打开到数据展示的整个流程梳理清楚,让大家能够对实际运行逻辑有完整的理解。

6.1 页面初始化

Fragment 被创建时,首先加载布局:

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {// Inflate the layout for this fragmentreturn inflater.inflate(R.layout.fragment_library, container, false)}

初始化 RecyclerView:

val recyclerView = binding.recyclerView
recyclerView.layoutManager = GridLayoutManager(context, 2)
recyclerView.adapter = libraryAdapterval spacing = resources.getDimensionPixelSize(R.dimen.grid_spacing)
recyclerView.addItemDecoration(GridSpacingItemDecoration(2, spacing))

此时,RecyclerView 已经准备好显示两列网格的书架内容。

6.2 ViewModel 数据加载

创建 LibraryViewModel 实例并调用 fetchDramas():

viewModel.fetchDramas()

ViewModel 在后台线程从 Room 数据库读取所有剧集,并更新 StateFlow:

viewModelScope.launch {val dramaList = withContext(Dispatchers.IO) {repository.getAllDramasWithEpisodeCount()}_dramas.value = dramaList
}

6.3 UI 绑定与刷新

Fragment 监听 ViewModel 中的 dramas Flow,并将数据绑定到 Adapter:

lifecycleScope.launchWhenStarted {viewModel.dramas.collect { dramaList ->libraryAdapter.updateItems(dramaList)binding.tipView.isVisible = dramaList.isEmpty()}
}

解释:

  • Adapter 的 updateItems() 方法会调用 notifyDataSetChanged(),让 RecyclerView 刷新显示最新内容。
  • 如果数据库为空,tipView 显示“暂无内容”,保证空状态友好提示。

6.4 点击事件与交互

在 LibraryAdapter 中,每个 item 的点击事件回调如下:

holder.itemView.setOnClickListener {onItemClick(item)
}

  • 点击封面或标题可以进入剧集详情页。
  • 更多按钮可以扩展成删除、收藏或移动书架等操作。

这种方式使得书架页的交互逻辑简单清晰,且容易扩展。

6.5 总体流程总结

整个书架页的运行流程可以概括为:

  1. 页面初始化 → RecyclerView 和 Adapter 准备就绪
  2. ViewModel 从本地数据库读取数据
  3. 数据更新 → Adapter 刷新 UI
  4. 空状态显示/隐藏
  5. 用户点击 → 响应交互

通过这种架构,书架页实现了 UI 与数据分离、数据本地化、交互灵活可扩展 的目标。

七. 总结与扩展

本章我们实现了书架页的核心功能:

  1. 使用 RecyclerView + GridLayoutManager 展示剧集网格
  2. 通过 Adapter + ViewHolder 绑定数据并处理点击事件
  3. 利用 ViewModel + Room 从本地数据库读取数据
  4. 实现空状态提示,当书架为空时显示“暂无内容”

扩展思路:

  1. 增加分页或排序功能,让书架内容更灵活
  2. 点击封面跳转到剧集详情页
  3. 更多按钮实现删除、移动或收藏操作

通过本章学习,你已经掌握了一个完整的本地数据书架页实现流程,为后续功能扩展打下了坚实基础。


文章转载自:

http://pSW6f1h0.xcyhy.cn
http://f4eDbH5h.xcyhy.cn
http://8RpdBlro.xcyhy.cn
http://PxXD3iTh.xcyhy.cn
http://HxOpXFU3.xcyhy.cn
http://x0AHbUdZ.xcyhy.cn
http://Z9cCzA3K.xcyhy.cn
http://HRO3Swgw.xcyhy.cn
http://3ptL3pdw.xcyhy.cn
http://vFFuaBQm.xcyhy.cn
http://nTKpcLHb.xcyhy.cn
http://u5e3CGH2.xcyhy.cn
http://oTUKPZTk.xcyhy.cn
http://WxQYJSTh.xcyhy.cn
http://8pyyOIpU.xcyhy.cn
http://mx2KMFIV.xcyhy.cn
http://KmAZ9pqO.xcyhy.cn
http://dotpATI2.xcyhy.cn
http://w0NgIYm6.xcyhy.cn
http://Z6deObE0.xcyhy.cn
http://tJchs13E.xcyhy.cn
http://evCMPrZV.xcyhy.cn
http://iogJabVh.xcyhy.cn
http://iOCFM3JC.xcyhy.cn
http://qoymVaND.xcyhy.cn
http://xccRCAqo.xcyhy.cn
http://0euO16PI.xcyhy.cn
http://pphVaQ4p.xcyhy.cn
http://u9AuNNN0.xcyhy.cn
http://ronAcYsj.xcyhy.cn
http://www.dtcms.com/a/388449.html

相关文章:

  • 日常开发-20250917
  • 基于SpringBoot+Vue的近郊农场共享管理系统(Echarts图形化分析)
  • AI开发实战:从数据准备到模型部署的完整经验分享
  • 【漏洞预警】大华DSS数字监控系统 user_edit.action 接口敏感信息泄露漏洞分析
  • RFID赋能光伏电池片制造智能化跃迁
  • 大数据 + 分布式架构下 SQL 查询优化:从核心技术到调优体系
  • FPGA硬件设计-DDR
  • 卫星通信天线的跟踪精度,含义、测量和计算
  • 忘记MySQL root密码,如何急救并保障备份?
  • Java 异步编程实战:Thread、线程池、CompletableFuture、@Async 用法与场景
  • 贪心算法应用:硬币找零问题详解
  • while语句中的break和continue
  • 10cm钢板矫平机:一场“掰直”钢铁的微观战争
  • Python实现计算点云投影面积
  • C++底层刨析章节二:迭代器原理与实现:STL的万能胶水
  • 学习Python中Selenium模块的基本用法(14:页面打印)
  • 便携式管道推杆器:通信与电力基础设施升级中的“隐形推手”
  • leetcode 349 两个数组的交集
  • UV映射!加入纹理!
  • 车辆DoIP声明报文/识别响应报文的UDP端口规范
  • Elasticsearch 2.x版本升级指南
  • OpenCV 人脸检测、微笑检测 原理及案例解析
  • [Python编程] Python3 集合
  • [性能分析与优化]伪共享问题(perf + cpp)
  • OC-动画实现折叠cell
  • 关于层级问题
  • Linux基础命令汇总
  • getchar 和 putchar
  • 【序列晋升】35 Spring Data Envers 轻量级集成数据审计
  • 快速入门HarmonyOS应用开发(二)