Android开发实战:通过网络电子书阅读器实践运用fragment知识

0 引言

fragment的好处:可以复用 UI、动态替换不同界面、管理生命周期

对我而言,我感觉fragment像web开发里面的盒子模型(fragment英语直译就是碎片,像是我们把画面分成了许多拼图块拼在一起),关于其基础知识点可以自行搜索,这里不重点讲解知识点,而是注重实践运用

先来效果图,整个项目非常干净,代码基础简单,非常适合慢慢加码

这三本书是我随便找的txt,书面是默认封面

再来看项目文件结构

大家先熟悉一下,首先大概介绍一下整个项目的实现

一个基于 Fragment 和 SQLite 的电子书阅读器原型

该应用包含了书架(ShelfFragment)和阅读界面(ReadFragment),并使用数据库来存储书籍信息和阅读进度。

以下是实现的关键部分:

  1. (Book.kt & DatabaseHelper.kt)

    我们定义了一个 Book 数据类,并创建了 DatabaseHelper 来管理 SQLite 数据库。它负责在首次启动时插入书籍,并保存用户的阅读进度(滚动位置)。

  2. 主界面布局 (activity_main.xml & MainActivity.kt)

    主界面采用了一个 FrameLayout 作为 Fragment 容器。MainActivity 负责初始加载书架。

  3. 书架界面 (ShelfFragment.kt & fragment_shelf.xml)

    书架使用了 GridView 来模仿木质书架的布局。它从数据库中读取所有书籍,并通过 BookAdapter 显示封面和标题。点击书籍会通过 FragmentTransaction 切换到阅读界面。

  4. 阅读界面 (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>
  1. onCreate = 生命周期入口,savedInstanceState 是 系统帮你保存的临时数据包,类型是 Bundle?,意思是

    可空的 Bundle(一个 键值对容器,类似 Map<String, Any>)

  2. super.onCreate(savedInstanceState),初始化父类,AppCompatActivity(父类)有很多内部初始化操作

  3. setContentView = 加载布局

  4. 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>
  1. 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 中 ? 表示 可空类型
  2. 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) 可以按返回键回到书架

(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() 是一个静态方法(方便外部调用):

    1. 传入一个 Book 对象
    2. 创建 Fragment
    3. 把书放到 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
  1. data class

    • Kotlin 的 data class 用于存储数据,它自动帮你生成 toString()equals()hashCode()copy() 等方法,非常方便。
  2. 属性

    • id: Int → 每本书的唯一 ID。
    • title: String → 书名。
    • rawId: Int → 对应资源文件中的原始书籍文本 ID(比如 raw 文件夹里的 txt)。
    • coverId: Int → 封面图片的资源 ID。
    • progress: Int → 阅读进度,默认 0。
  3. 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 中,如果你要显示一个列表(比如 ListViewGridView),你需要 Adapter。Adapter 就像"翻译官":

  • 它把数据(Book 列表)翻译成屏幕上每一行的视图(XML 布局)。

关键方法

  1. getCount()

    kotlin 复制代码
    override fun getCount(): Int = books.size
    • 告诉系统列表有多少条数据。
  2. getItem(position: Int)

    kotlin 复制代码
    override fun getItem(position: Int): Any = books[position]
    • 返回指定位置的 Book 对象。
  3. getItemId(position: Int)

    kotlin 复制代码
    override fun getItemId(position: Int): Long = books[position].id.toLong()
    • 返回每条数据的唯一 ID。
  4. getView(position: Int, convertView: View?, parent: ViewGroup?)

    kotlin 复制代码
    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
    • convertView 是"可重用的旧视图",如果为空就创建新的。
    • LayoutInflater.from(context).inflate(...) → 把 XML 布局转换成真正的 View。
    • findViewById → 找到布局里的 ImageView 和 TextView。
    • imgCover.setImageResource(book.coverId) → 设置封面图片。
    • tvTitle.text = book.title → 设置书名。
    • 返回这个 View 给 ListView 显示。

✅ 总结:BookAdapter 就是把 Book 数据变成界面上的一条条书籍条目。


一个问题:为什么item不用fragment

  1. 性能问题:Fragment 的生命周期管理比普通 View 复杂。 每个 Fragment 都会有自己的FragmentManager、生命周期回调,如果每个 item 都是 Fragment,会消耗大量内存和 CPU。 假设有 100本书,每本书都是 Fragment,会导致界面卡顿甚至崩溃。
  2. 过度设计: item 很小,只是显示封面 + 书名,没有独立逻辑和状态。 用 Fragment 显得重型,不必要Adapter + XML + View 就足够了。
  3. 更新数据更简单 使用 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. 创建数据库和书籍表
  2. 插入初始书籍数据
  3. 查询所有书籍
  4. 更新书籍阅读进度
  5. 如果数据库结构变化,自动升级

1️⃣ 整体作用

一个叫 DatabaseHelperAndroid 中用来管理本地数据库的类 ,继承了 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 文件夹的资源 ID
    • cover_id:封面资源 ID
    • progress:阅读进度
  • 然后调用 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 替换 ?
  • 这样你就能记录用户读到哪一页了。

为了方便,这三本书我都用了默认封面,相信讲解完就能明白,只需要改一下数据库里面的图片来源即可

多巩固多新!

相关推荐
wangchunting1 小时前
Jvm-垃圾回收算法
java·jvm·算法
2401_831824961 小时前
RESTful API设计最佳实践(Python版)
jvm·数据库·python
暮冬-  Gentle°1 小时前
更优雅的测试:Pytest框架入门
jvm·数据库·python
xuxie991 小时前
N10 ARM中断
jvm
00后程序员张2 小时前
iPhone 无需越狱文件管理 使用Keymob查看导出文件
android·ios·小程序·https·uni-app·iphone·webview
kcuwu.2 小时前
Python文件操作零基础及进阶
android·服务器·python
2301_793804692 小时前
Django全栈开发入门:构建一个博客系统
jvm·数据库·python
锋风Fengfeng2 小时前
Windows怎么方便查看AOSP代码
android·windows
2501_916008892 小时前
Unity3D iOS 应用防篡改实战 资源校验、 IPA 二进制保护
android·ios·小程序·https·uni-app·iphone·webview