0 引言
fragment的好处:可以复用 UI、动态替换不同界面、管理生命周期
对我而言,我感觉fragment像web开发里面的盒子模型(fragment英语直译就是碎片,像是我们把画面分成了许多拼图块拼在一起),关于其基础知识点可以自行搜索,这里不重点讲解知识点,而是注重实践运用
先来效果图,整个项目非常干净,代码基础简单,非常适合慢慢加码

这三本书是我随便找的txt,书面是默认封面
再来看项目文件结构

大家先熟悉一下,首先大概介绍一下整个项目的实现
一个基于 Fragment 和 SQLite 的电子书阅读器原型
该应用包含了书架(ShelfFragment)和阅读界面(ReadFragment),并使用数据库来存储书籍信息和阅读进度。
以下是实现的关键部分:
-
(Book.kt & DatabaseHelper.kt)
我们定义了一个 Book 数据类,并创建了 DatabaseHelper 来管理 SQLite 数据库。它负责在首次启动时插入书籍,并保存用户的阅读进度(滚动位置)。
-
主界面布局 (activity_main.xml & MainActivity.kt)
主界面采用了一个 FrameLayout 作为 Fragment 容器。MainActivity 负责初始加载书架。
-
书架界面 (ShelfFragment.kt & fragment_shelf.xml)
书架使用了 GridView 来模仿木质书架的布局。它从数据库中读取所有书籍,并通过 BookAdapter 显示封面和标题。点击书籍会通过 FragmentTransaction 切换到阅读界面。
-
阅读界面 (ReadFragment.kt & fragment_read.xml)
阅读界面使用 ScrollView 和 TextView 显示内容。它具备以下功能:
- 读取内容:从应用的 raw 资源目录读取文本文件(如 桃花夫人.txt)。
- 进度保存:当用户离开界面或按下返回键时,自动将当前的滚动位置(scrollY)保存到 SQLite 数据库中。
- 进度恢复:再次打开同一本书时,界面会自动滚动回上次保存的位置。
这种架构利用了 Fragment 的灵活性,使得在书架和阅读器之间切换非常流畅,同时利用 SQLite 保证了用户数据的持久化。如果需要添加"删除"或"设置"等更多功能,可以在现有的 DatabaseHelper 和 Fragment 中继续扩展。
1 代码详解
(1)main主界面布局
MainActivity 只是一个"容器",真正显示内容的是 Fragment(ShelfFragment)
kotlin
package com.example.myreader
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, ShelfFragment())
.commit()
}
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
</FrameLayout>
-
onCreate = 生命周期入口,savedInstanceState 是 系统帮你保存的临时数据包,类型是 Bundle?,意思是
可空的 Bundle(一个 键值对容器,类似 Map<String, Any>)
-
super.onCreate(savedInstanceState),初始化父类,AppCompatActivity(父类)有很多内部初始化操作
-
setContentView = 加载布局
-
Fragment 动态加载
- if (savedInstanceState == null) 防止 Fragment 重复创建
- supportFragmentManager管理 Activity 里的 Fragment:增删和替换
- beginTransaction()开启一次 Fragment 事务(Transaction)
- .addToBackStack(null)加入这个方法调用,用户就能使用回退键"回到"上一个屏幕
- .replace(...) 把 ShelfFragment 放进这个容器里(如果之前有 Fragment,会先移除再添加)
- .commit() 提交这次操作,让它真正生效
(2) 书架界面
kotlin
package com.example.myreader
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.GridView
import androidx.fragment.app.Fragment
class ShelfFragment : Fragment() {
private lateinit var dbHelper: DatabaseHelper
private lateinit var gridView: GridView
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_shelf, container, false)
gridView = view.findViewById(R.id.grid_view)
dbHelper = DatabaseHelper(requireContext())
loadBooks()
return view
}
private fun loadBooks() {
val books = dbHelper.getAllBooks()
val adapter = BookAdapter(requireContext(), books)
gridView.adapter = adapter
gridView.setOnItemClickListener { _, _, position, _ ->
val book = books[position]
val readFragment = ReadFragment.newInstance(book)
parentFragmentManager.beginTransaction()
.replace(R.id.fragment_container, readFragment)
.addToBackStack(null)
.commit()
}
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#D2B48C">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#8B4513">
<Button
android:id="@+id/btn_bookstore"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="书店"
android:layout_centerVertical="true"
android:layout_marginStart="10dp"
style="?android:attr/buttonBarButtonStyle"
android:textColor="@android:color/white"/>
<TextView
android:id="@+id/textView"
android:layout_width="56dp"
android:layout_height="35dp"
android:layout_alignParentBottom="true"
android:layout_marginStart="33dp"
android:layout_marginEnd="26dp"
android:layout_marginBottom="4dp"
android:layout_toStartOf="@+id/linearLayout"
android:layout_toEndOf="@+id/btn_bookstore"
android:text="iReader"
android:textColor="@android:color/white"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/linearLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:orientation="horizontal">
<Button
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"
android:textColor="@android:color/white" />
<Button
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="列表"
android:textColor="@android:color/white" />
<Button
style="?android:attr/buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="设置"
android:textColor="@android:color/white" />
</LinearLayout>
</RelativeLayout>
<GridView
android:id="@+id/grid_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:numColumns="3"
android:padding="16dp"
android:verticalSpacing="24dp"
android:horizontalSpacing="12dp"
android:gravity="center"
android:stretchMode="columnWidth"/>
</LinearLayout>

- onCreateView() 是 Fragment 创建界面时调用的函数
inflater:LayoutInflater布局膨胀器,用它把 XML 布局文件加载成实际的 View 对象。
container;ViewGroup?当前 Fragment 的父容器,通常是 Activity 的布局里的一个占位区域。- inflater.inflate(R.layout.fragment_shelf, container, false) 这个是把 布局 XML (fragment_shelf.xml) 加载成实际可显示的 View。 container 是父容器,false 表示还没挂到父容器。
- gridView = view.findViewById(R.id.grid_view)找到布局里的 GridView 控件,用来显示书籍列表。
- dbHelper = DatabaseHelper(requireContext())创建一个数据库助手类实例,用于从数据库获取书籍数据。
- loadBooks()调用自定义函数加载书籍到 GridView。
- 在 Kotlin 中
?表示 可空类型
- loadBooks()加载书籍到 GridView
- Lambda 表达式(匿名函数),GridView 是 Android 的 格子布局控件

- GridView 的 OnItemClickListener 定义如下:

_表示这个参数 你不需要用它,直接忽略。
position表示点击的是 第几个书格子,用它拿到对应的书籍数据 - val books = dbHelper.getAllBooks()从数据库拿到所有书籍(Book 对象列表)
- val adapter = BookAdapter(requireContext(), books) 适配器 Adapter:把数据(书籍列表)转换成 View(显示在 GridView)。 适配器负责"每个格子显示什么"
- gridView.adapter = adapter 把适配器绑定到 GridView,页面上就能显示书籍了
- gridView.setOnItemClickListener { ... } 点击某本书时触发:
- ReadFragment.newInstance(book) 创建阅读界面。
- replace() 替换当前 Fragment。
- addToBackStack(null) 可以按返回键回到书架
- Lambda 表达式(匿名函数),GridView 是 Android 的 格子布局控件
(3) 阅读界面
kotlin
package com.example.myreader
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ScrollView
import android.widget.TextView
import androidx.fragment.app.Fragment
import java.io.InputStream
class ReadFragment : Fragment() {
private lateinit var book: Book
private lateinit var dbHelper: DatabaseHelper
private lateinit var scrollView: ScrollView
companion object {
private const val ARG_BOOK = "arg_book"
fun newInstance(book: Book): ReadFragment {
val fragment = ReadFragment()
val args = Bundle()
args.putSerializable(ARG_BOOK, book)
fragment.arguments = args
return fragment
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
book = arguments?.getSerializable(ARG_BOOK) as Book
dbHelper = DatabaseHelper(requireContext())
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_read, container, false)
val tvTitle = view.findViewById<TextView>(R.id.tv_read_title)
val tvContent = view.findViewById<TextView>(R.id.tv_content)
val btnBack = view.findViewById<Button>(R.id.btn_back)
scrollView = view.findViewById(R.id.scroll_view)
tvTitle.text = book.title
tvContent.text = readTextFromRaw(book.rawId)
btnBack.setOnClickListener {
saveProgress()
parentFragmentManager.popBackStack()
}
// Restore progress
scrollView.post {
scrollView.scrollTo(0, book.progress)
}
return view
}
private fun readTextFromRaw(rawId: Int): String {
return try {
val inputStream: InputStream = resources.openRawResource(rawId)
inputStream.bufferedReader().use { it.readText() }
} catch (e: Exception) {
"读取失败"
}
}
private fun saveProgress() {
val progress = scrollView.scrollY
dbHelper.updateProgress(book.id, progress)
}
override fun onPause() {
super.onPause()
saveProgress()
}
}
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="#F5F5DC">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="#8B4513">
<Button
android:id="@+id/btn_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="返回"
android:layout_centerVertical="true"
android:layout_marginStart="10dp"/>
<TextView
android:id="@+id/tv_read_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="书名"
android:textColor="@android:color/white"
android:textSize="18sp"
android:layout_centerInParent="true"/>
</RelativeLayout>
<ScrollView
android:id="@+id/scroll_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="16dp">
<TextView
android:id="@+id/tv_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="@android:color/black"
android:textSize="18sp"
android:lineSpacingExtra="8dp"/>
</ScrollView>
</LinearLayout>
- 显示一本书的内容
- 允许滚动查看全文
- 记录用户读到哪一页,下次打开时可以继续阅读
为了实现上面功能
1.类里的成员变量
kotlin
private lateinit var book: Book
private lateinit var dbHelper: DatabaseHelper
private lateinit var scrollView: ScrollView
book: 代表当前正在阅读的书(包含书名、资源文件ID、阅读进度等信息)
dbHelper: 用于操作数据库(保存用户的阅读进度)
scrollView: 页面里的滚动视图,用来显示全文,可以上下滑动
lateinit 表示这个变量稍后才初始化,保证使用前一定会赋值。
2.伴生对象(Companion Object)
kotlin
companion object {
private const val ARG_BOOK = "arg_book"
fun newInstance(book: Book): ReadFragment {
val fragment = ReadFragment()
val args = Bundle()
args.putSerializable(ARG_BOOK, book)
fragment.arguments = args
return fragment
}
}
-
ARG_BOOK 是一个 key,用于在 Fragment 之间传递书对象。
-
newInstance() 是一个静态方法(方便外部调用):
- 传入一个 Book 对象
- 创建 Fragment
- 把书放到 arguments 里(相当于给 Fragment 准备参数)
注意:Book 必须实现 Serializable,这样才能通过 Bundle 传递。
3.onCreate() 方法
kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
book = arguments?.getSerializable(ARG_BOOK) as Book
dbHelper = DatabaseHelper(requireContext())
}
- book = ...:从 arguments 中取出传过来的书
- dbHelper = DatabaseHelper(requireContext()):创建数据库助手,用来保存阅读进度
requireContext() 返回当前 Fragment 所在 Activity 的上下文,数据库操作需要这个上下文。
4.onCreateView() 方法
kotlin
val view = inflater.inflate(R.layout.fragment_read, container, false)
把 fragment_read.xml 布局文件加载进来
view 就是这个布局的根视图
然后通过 findViewById 找到布局里的控件:
kotlin
val tvTitle = view.findViewById<TextView>(R.id.tv_read_title)
val tvContent = view.findViewById<TextView>(R.id.tv_content)
val btnBack = view.findViewById<Button>(R.id.btn_back)
scrollView = view.findViewById(R.id.scroll_view)
kotlin
tvTitle.text = book.title
tvContent.text = readTextFromRaw(book.rawId)//显示书名和内容
kotlin
btnBack.setOnClickListener {
saveProgress()
parentFragmentManager.popBackStack()//返回按钮功能
}
kotlin
scrollView.post {
scrollView.scrollTo(0, book.progress)//恢复上次阅读进度
}
5.读取文本内容的方法
kotlin
private fun readTextFromRaw(rawId: Int): String {
return try {
val inputStream: InputStream = resources.openRawResource(rawId)
inputStream.bufferedReader().use { it.readText() }
} catch (e: Exception) {
"读取失败"
}
}
- rawId 是书在 res/raw 文件夹里的资源ID
- openRawResource() → 打开文件
- bufferedReader().use { it.readText() } → 读取全文
- 异常处理:如果文件不存在或出错,返回 "读取失败"
6.保存阅读进度
kotlin
private fun saveProgress() {
val progress = scrollView.scrollY
dbHelper.updateProgress(book.id, progress)
}
scrollView.scrollY → 当前滚动的垂直位置
调用 dbHelper.updateProgress() → 保存到数据库
同时在 onPause() 里也会保存进度(防止用户突然离开):
kotlin
override fun onPause() {
super.onPause()
saveProgress()
}
7.布局文件(fragment_read.xml)
bash
LinearLayout(垂直方向)
├─ RelativeLayout(顶部栏)
│ ├─ Button(返回)
│ └─ TextView(书名)
└─ ScrollView(可滚动内容区)
└─ TextView(书的内容)
(4) 书籍块
1.item_book.xml
xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:padding="8dp">
<ImageView
android:id="@+id/img_cover"
android:layout_width="100dp"
android:layout_height="140dp"
android:scaleType="fitCenter"
android:src="@drawable/cover_defualt"
android:background="@android:color/white"
android:elevation="4dp"/>
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="书名"
android:textColor="@android:color/black"
android:textSize="14sp"
android:maxLines="2"
android:ellipsize="end"
android:gravity="center"/>
</LinearLayout>

这个 XML 文件就是一个"书籍条目"的布局模板
2.数据类 Book Book.kt
kotlin
package com.example.myreader
import java.io.Serializable
data class Book(
val id: Int = 0,
val title: String,
val rawId: Int,
val coverId: Int,
var progress: Int = 0
) : Serializable
-
data class
- Kotlin 的
data class用于存储数据,它自动帮你生成toString()、equals()、hashCode()、copy()等方法,非常方便。
- Kotlin 的
-
属性
id: Int→ 每本书的唯一 ID。title: String→ 书名。rawId: Int→ 对应资源文件中的原始书籍文本 ID(比如 raw 文件夹里的 txt)。coverId: Int→ 封面图片的资源 ID。progress: Int→ 阅读进度,默认 0。
-
Serializable
- 让 Book 对象可以在不同组件之间传递,比如通过 Intent 传给另一个 Activity。
总结:Book 类就是"每本书的数据模型"。
3.适配器 BookAdapter BookAdapter.kt)
kotlin
package com.example.myreader
import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import android.widget.TextView
class BookAdapter(private val context: Context, private val books: List<Book>) : BaseAdapter() {
override fun getCount(): Int = books.size
override fun getItem(position: Int): Any = books[position]
override fun getItemId(position: Int): Long = books[position].id.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: View = convertView ?: LayoutInflater.from(context).inflate(R.layout.item_book, parent, false)
val book = books[position]
val imgCover = view.findViewById<ImageView>(R.id.img_cover)
val tvTitle = view.findViewById<TextView>(R.id.tv_title)
imgCover.setImageResource(book.coverId)
tvTitle.text = book.title
return view
}
}
...
}
什么是 Adapter?
在 Android 中,如果你要显示一个列表(比如 ListView 或 GridView),你需要 Adapter。Adapter 就像"翻译官":
- 它把数据(Book 列表)翻译成屏幕上每一行的视图(XML 布局)。
关键方法
-
getCount()
kotlinoverride fun getCount(): Int = books.size- 告诉系统列表有多少条数据。
-
getItem(position: Int)
kotlinoverride fun getItem(position: Int): Any = books[position]- 返回指定位置的 Book 对象。
-
getItemId(position: Int)
kotlinoverride fun getItemId(position: Int): Long = books[position].id.toLong()- 返回每条数据的唯一 ID。
-
getView(position: Int, convertView: View?, parent: ViewGroup?)
kotlinval view: View = convertView ?: LayoutInflater.from(context).inflate(R.layout.item_book, parent, false) val book = books[position] val imgCover = view.findViewById<ImageView>(R.id.img_cover) val tvTitle = view.findViewById<TextView>(R.id.tv_title) imgCover.setImageResource(book.coverId) tvTitle.text = book.title return viewconvertView是"可重用的旧视图",如果为空就创建新的。LayoutInflater.from(context).inflate(...)→ 把 XML 布局转换成真正的 View。findViewById→ 找到布局里的 ImageView 和 TextView。imgCover.setImageResource(book.coverId)→ 设置封面图片。tvTitle.text = book.title→ 设置书名。- 返回这个 View 给 ListView 显示。
✅ 总结:BookAdapter 就是把 Book 数据变成界面上的一条条书籍条目。
一个问题:为什么item不用fragment
- 性能问题:Fragment 的生命周期管理比普通 View 复杂。 每个 Fragment 都会有自己的FragmentManager、生命周期回调,如果每个 item 都是 Fragment,会消耗大量内存和 CPU。 假设有 100本书,每本书都是 Fragment,会导致界面卡顿甚至崩溃。
- 过度设计: item 很小,只是显示封面 + 书名,没有独立逻辑和状态。 用 Fragment 显得重型,不必要Adapter + XML + View 就足够了。
- 更新数据更简单 使用 View(XML)+ Adapter,只需要 getView() 更新 item 即可。 如果用Fragment,每次滑动或刷新都要创建 Fragment、管理生命周期,逻辑复杂。
小控件 / 列表项 → 用 View + Adapter
复杂页面 / 独立模块 → 用 Fragment(如底部菜单(Bottom Navigation Bar))
(5) 数据库
kotlin
package com.example.myreader
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
class DatabaseHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
private const val DATABASE_NAME = "ebook_reader.db"
private const val DATABASE_VERSION = 1
private const val TABLE_BOOKS = "books"
private const val COLUMN_ID = "id"
private const val COLUMN_TITLE = "title"
private const val COLUMN_RAW_ID = "raw_id"
private const val COLUMN_COVER_ID = "cover_id"
private const val COLUMN_PROGRESS = "progress"
}
override fun onCreate(db: SQLiteDatabase?) {
val createTable = ("CREATE TABLE " + TABLE_BOOKS + "("
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ COLUMN_TITLE + " TEXT,"
+ COLUMN_RAW_ID + " INTEGER,"
+ COLUMN_COVER_ID + " INTEGER,"
+ COLUMN_PROGRESS + " INTEGER" + ")")
db?.execSQL(createTable)
// Insert initial data
insertInitialData(db)
}
private fun insertInitialData(db: SQLiteDatabase?) {
val books = listOf(
Book(title = "桃花夫人", rawId = R.raw.tao_hua_fu_ren, coverId = R.drawable.cover_defualt),
Book(title = "罡体神尊", rawId = R.raw.gang_ti_sheng_zun, coverId = R.drawable.cover_defualt),
Book(title = "向心公转", rawId = R.raw.xiang_xin_gong_zhuan, coverId = R.drawable.cover_defualt)
)
books.forEach { book ->
val values = ContentValues().apply {
put(COLUMN_TITLE, book.title)
put(COLUMN_RAW_ID, book.rawId)
put(COLUMN_COVER_ID, book.coverId)
put(COLUMN_PROGRESS, book.progress)
}
db?.insert(TABLE_BOOKS, null, values)
}
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
db?.execSQL("DROP TABLE IF EXISTS $TABLE_BOOKS")
onCreate(db)
}
fun getAllBooks(): List<Book> {
val bookList = mutableListOf<Book>()
val db = readableDatabase
val cursor = db.rawQuery("SELECT * FROM $TABLE_BOOKS", null)
if (cursor.moveToFirst()) {
do {
val book = Book(
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_TITLE)),
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_RAW_ID)),
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_COVER_ID)),
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_PROGRESS))
)
bookList.add(book)
} while (cursor.moveToNext())
}
cursor.close()
return bookList
}
fun updateProgress(bookId: Int, progress: Int) {
val db = writableDatabase
val values = ContentValues().apply {
put(COLUMN_PROGRESS, progress)
}
db.update(TABLE_BOOKS, values, "$COLUMN_ID = ?", arrayOf(bookId.toString()))
}
}
这个类的作用是电子书管理器,负责存储书籍信息和用户进度。
- 创建数据库和书籍表
- 插入初始书籍数据
- 查询所有书籍
- 更新书籍阅读进度
- 如果数据库结构变化,自动升级
1️⃣ 整体作用
一个叫 DatabaseHelper的 Android 中用来管理本地数据库的类 ,继承了 SQLiteOpenHelper。
- 数据库(Database):用来存储你电子书的信息,比如书名、封面、阅读进度等。
- SQLite:Android 自带的一种轻量级数据库,不需要额外安装。
- SQLiteOpenHelper:帮你创建、更新和管理 SQLite 数据库的工具类,你只需要继承它,然后实现里面的几个方法就行了。
2️⃣ 关键部分解析
(1) companion object
kotlin
companion object {
private const val DATABASE_NAME = "ebook_reader.db"
private const val DATABASE_VERSION = 1
private const val TABLE_BOOKS = "books"
private const val COLUMN_ID = "id"
private const val COLUMN_TITLE = "title"
private const val COLUMN_RAW_ID = "raw_id"
private const val COLUMN_COVER_ID = "cover_id"
private const val COLUMN_PROGRESS = "progress"
}
companion object相当于 类里面的静态变量,在整个类里都可以用。- 定义了数据库的名字、版本号、表名和每一列的名字。
- 数据库版本号 DATABASE_VERSION:以后如果你改了数据库结构,需要升级数据库时用。
(2) 构造函数
kotlin
class DatabaseHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION)
context:Android 上下文,用来打开数据库或文件。- 调用
SQLiteOpenHelper构造函数,告诉它数据库名字和版本。
(3) 创建数据库:onCreate()
kotlin
override fun onCreate(db: SQLiteDatabase?) {
val createTable = ("CREATE TABLE " + TABLE_BOOKS + "("
+ COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ COLUMN_TITLE + " TEXT,"
+ COLUMN_RAW_ID + " INTEGER,"
+ COLUMN_COVER_ID + " INTEGER,"
+ COLUMN_PROGRESS + " INTEGER" + ")")
db?.execSQL(createTable)
insertInitialData(db)
}
-
当数据库第一次被创建时,这个方法会被调用。
-
CREATE TABLE是 SQL 语句,用来创建表格。id:书籍唯一标识,自增title:书名raw_id:书籍在res/raw文件夹的资源 IDcover_id:封面资源 IDprogress:阅读进度
-
然后调用
insertInitialData(db)插入初始数据。
(4) 插入初始数据:insertInitialData()
kotlin
private fun insertInitialData(db: SQLiteDatabase?) {
val books = listOf(
Book(title = "桃花夫人", rawId = R.raw.tao_hua_fu_ren, coverId = R.drawable.cover_defualt),
Book(title = "罡体神尊", rawId = R.raw.gang_ti_sheng_zun, coverId = R.drawable.cover_defualt),
Book(title = "向心公转", rawId = R.raw.xiang_xin_gong_zhuan, coverId = R.drawable.cover_defualt)
)
books.forEach { book ->
val values = ContentValues().apply {
put(COLUMN_TITLE, book.title)
put(COLUMN_RAW_ID, book.rawId)
put(COLUMN_COVER_ID, book.coverId)
put(COLUMN_PROGRESS, book.progress)
}
db?.insert(TABLE_BOOKS, null, values)
}
}
- 定义了一些书籍数据,放进数据库。
- ContentValues :就是一个键值对,
列名 -> 值 db.insert:把这行数据插入表格中。
⚠️ 注意:这里
Book对象里的progress没写初始化值的话,默认应该是 0,否则会报错。
(5) 升级数据库:onUpgrade()
kotlin
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
db?.execSQL("DROP TABLE IF EXISTS $TABLE_BOOKS")
onCreate(db)
}
- 当数据库版本号升级(DATABASE_VERSION 改大了)时调用
- 这里做法是 先删掉旧表,再重新创建表(注意,这会丢掉旧数据)
(6) 查询所有书籍:getAllBooks()
kotlin
fun getAllBooks(): List<Book> {
val bookList = mutableListOf<Book>()
val db = readableDatabase
val cursor = db.rawQuery("SELECT * FROM $TABLE_BOOKS", null)
if (cursor.moveToFirst()) {
do {
val book = Book(
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_ID)),
cursor.getString(cursor.getColumnIndexOrThrow(COLUMN_TITLE)),
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_RAW_ID)),
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_COVER_ID)),
cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_PROGRESS))
)
bookList.add(book)
} while (cursor.moveToNext())
}
cursor.close()
return bookList
}
readableDatabase:获取可读数据库cursor:查询结果的指针,类似 Excel 的每一行moveToFirst():移动到第一行getColumnIndexOrThrow:根据列名拿列索引do ... while (cursor.moveToNext()):遍历所有行,创建 Book 对象- 最后返回所有书籍的列表。
(7) 更新阅读进度:updateProgress()
kotlin
fun updateProgress(bookId: Int, progress: Int) {
val db = writableDatabase
val values = ContentValues().apply {
put(COLUMN_PROGRESS, progress)
}
db.update(TABLE_BOOKS, values, "$COLUMN_ID = ?", arrayOf(bookId.toString()))
}
writableDatabase:获取可写数据库- 更新某本书的
progress列 $COLUMN_ID = ?表示更新条件,用bookId替换?- 这样你就能记录用户读到哪一页了。
为了方便,这三本书我都用了默认封面,相信讲解完就能明白,只需要改一下数据库里面的图片来源即可
多巩固多新!