Android图书管理系统:SQLite与RecyclerView实战 简介这套基于Android Studio开发的图书管理系统源码定位于安卓初学者及课程设计学生用于解决图书借阅信息管理需求同时帮助学习者掌握SQLite数据库操作、SharedPreferences用户持久化、MD5加密存储等关键技术。项目包含完整的欢迎页面、登录注册流程、记住密码选项以及添加、修改、删除、查询四个独立功能页面修改和删除前均可按学号检索对应借书记录功能闭环完整。资源包共1213个文件以xml布局与配置、java业务逻辑、gradle构建脚本、png界面图标、apk安装包为主整体约33.49MB目录结构清晰且包含详细注释可直接导入Android Studio编译运行。已有10412人学习使用既可满足安卓课设答辩需求也适合作为数据存储与界面交互的实战练习并可在源码基础上继续扩展预约、续借等功能。1. 图书管理系统在 Android Studio 里难在哪能跑不等于能演示课程设计里的图书管理系统十个有九个能跑但能跑和能演示是两回事。很多现场翻车的场景是列表数据显示出来但内容是写死的老师问“新增一本图书怎么做”只能当场改代码重编译搜索框一输入就卡死问到库存和借阅记录怎么保持一致答不上来。这套题目真正难的地方不在界面而在数据层有没有结构化交互有没有闭环。这篇文章把“Android Studio 实现图书管理系统”拆成一套能直接落地的方案SQLite 存三张表DAO 封装增删改查RecyclerView 展示列表EditText 做模糊搜索AlertDialog 做删除确认。看完之后从新建项目到能演示一个下午足够了。正在做安卓程序设计作业的人也别急着抄源码先把结构搭对后面会省很多事。2. 图书管理系统的技术选型SQLite 与 Room 怎么取舍三张表怎么设计新建项目之前先定数据层。图书管理系统的核心对象是图书、读者和借阅记录三者天然是关系型数据SQLite 完全够用。不要用 SharedPreferences 存 JSON 数组也不要用文件流手工拼数据。几百本图书、几十个读者的规模根本触发不了数据库性能瓶颈真正的差距在两层表结构是否合理增删改查有没有形成闭环。选型这一步能把后面一半的坑提前排掉。2.1 SQLiteOpenHelper 与 Room 的选择答辩展示选哪个Room 是官方 ORM 框架写起来比手写 SQLiteOpenHelper 简洁查询方法还能在编译期做 SQL 语法检查。但如果是课程设计或本科毕设我更倾向直接手写 SQLiteOpenHelper理由有三个。第一项目体量小Room 带来的模板代码节省有限。第二手写 SQL 在答辩时更好讲Cursor 怎么关闭、WHERE 参数怎么传、事务边界怎么控制这些恰好是图书管理系统常被追问的内容。Room 把事情封装掉以后底层逻辑一旦被问到很容易卡壳。第三Room 需要引入 kapt 或 ksp 注解处理器项目构建会多出一整段注解处理流程。Gradle 换源、AGP 升级之后偶发的构建报错经常出现在注解处理阶段。对一个要现场演示的项目来说构建越稳定留给功能的精力越多。如果后续想换 Room表结构不用变后面给的建表语句可以原样沿用只是把 DAO 方法改成 Insert、Query 注解的形式。2.2 包结构把图书管理系统按职责拆开项目刚创建时只有一个 MainActivity很多人习惯把所有代码都塞进去。我的做法是只拆四个包不多不少model、data、adapter、ui。com.example.library ├── model // Book、Reader、BorrowRecord ├── data // DbHelper、BookDao、BorrowDao ├── adapter // BookAdapter、ReaderAdapter └── ui // MainActivity、BookEditActivity、BorrowActivitymodel 包只放字段和 getter/setterdata 包集中全部 SQL 语句adapter 包只负责把数据变成 item 视图ui 包里的 Activity 负责按钮点击、页面跳转和弹窗。这样定位问题非常快新增图书界面没刷新先看 Activity 里有没有调用 adapter.setData库存不对只看 BookDao 的借书方法不用满工程翻 SQL。这个规模不需要 Repository 层。Repository 在大项目里用于抽象数据来源图书管理系统只有 SQLite 一个数据源强行加一层接口评审反而会问“这个接口为什么存在”。2.3 三张表的最小设计建表 SQL 建议直接写进 DbHelper.onCreate第一版就建三张表图书表 book、读者表 reader、借阅表 borrow。CREATE TABLE book ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT, isbn TEXT UNIQUE, category TEXT, stock INTEGER NOT NULL DEFAULT 1, location TEXT ); CREATE TABLE reader ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT, max_borrow INTEGER NOT NULL DEFAULT 5 ); CREATE TABLE borrow ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, reader_id INTEGER NOT NULL, borrow_date TEXT NOT NULL, due_date TEXT NOT NULL, return_date TEXT, FOREIGN KEY (book_id) REFERENCES book(id), FOREIGN KEY (reader_id) REFERENCES reader(id) );关键字段的逻辑要能讲清楚book.isbn 加 UNIQUE防止同一本书重复录入book.stock 表示当前还能借出的数量不是馆藏总量borrow.return_date 为 NULL 表示这条借阅还没归还。统计超期未还的 SQL只要查 return_date IS NULL AND due_date date(now) 就能出来。还要注意一点SQLite 的 FOREIGN KEY 默认不生效。需要在 DbHelper 里重写 onConfigure 方法调用 db.setForeignKeyConstraintsEnabled(true)否则上面这两行外键约束写了也不会起作用。至于借阅表的查询索引留到后面优化时再补课程演示阶段数据量小不加也能跑。2.4 用中间表表达多对多关系借阅关系不需要多想就是中间表。图书和读者是多对多一本书可以被不同人借一个读者可以借多本书。borrow 表的每一行代表一次借阅行为而不是在 book 表里存一个 reader_id 字段。如果那样设计同一本书被第二个人借时就没地方写了。还有人会把借阅人 ID 用逗号拼成一个字段这个方案最要命。按 reader_id 分开存才能写出“统计每个读者当前借了几本”的分组 SQL。后期要做热门图书排行一条 GROUP BY book_id COUNT(*) 就足够不需要在 Java 里拆字符串。3. Android Studio 写数据层DbHelper、BookDao 和借书还书的事务写法数据层是整个系统的基础。顺序是先写 DbHelper 负责建库建表再写 BookDao 负责图书增删改查最后把借书还书包成事务。先把这一层写完界面代码就只剩下拿数据和刷新列表。3.1 DbHelper 的 onCreate 与 onUpgrade 写法SQLiteOpenHelper 子类里面onCreate 只在数据库文件第一次创建时调用onUpgrade 在数据库版本号 change 时调用。手写这个类最容易被挑毛病的是 onUpgrade 直接删表重建。public class DbHelper extends SQLiteOpenHelper { private static final String DB_NAME library.db; private static final int DB_VERSION 1; public DbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onConfigure(SQLiteDatabase db) { super.onConfigure(db); db.setForeignKeyConstraintsEnabled(true); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE book (id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT, isbn TEXT UNIQUE, category TEXT, stock INTEGER NOT NULL DEFAULT 1, location TEXT)); db.execSQL(CREATE TABLE reader (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT, max_borrow INTEGER NOT NULL DEFAULT 5)); db.execSQL(CREATE TABLE borrow (id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, reader_id INTEGER NOT NULL, borrow_date TEXT NOT NULL, due_date TEXT NOT NULL, return_date TEXT, FOREIGN KEY (book_id) REFERENCES book(id), FOREIGN KEY (reader_id) REFERENCES reader(id))); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(ALTER TABLE book ADD COLUMN location TEXT); } } }构造方法的三个参数分别是上下文、数据库文件名、游标工厂最后一个通常传 null。onCreate 里建表语句要写成一条完整的 SQL最后不要带分号execSQL 会自动处理。onUpgrade 用 if 判断旧版本号再执行增量 ALTER TABLE这样新版本安装时不会把用户已有的借阅记录清掉。不要在主线程直接 new DbHelper 之后立刻做大量查询。数据库首次创建时要执行所有建表语句加上应用冷启动确实能感知到卡一下。图书管理系统的数据量不大把查询放到子线程就够不需要引入协程。3.2 BookDao参数化查询和 Cursor 关闭BookDao 写成普通类构造时接收 Context内部持有 DbHelper。这样 MainActivity 只调用 insert、update、delete、querySQL 全部收拢在 data 包里。public class BookDao { private final DbHelper helper; public BookDao(Context context) { helper new DbHelper(context); } public long insert(Book book) { SQLiteDatabase db helper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(title, book.getTitle()); values.put(author, book.getAuthor()); values.put(isbn, book.getIsbn()); values.put(category, book.getCategory()); values.put(stock, book.getStock()); return db.insert(book, null, values); } public int update(Book book) { SQLiteDatabase db helper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(title, book.getTitle()); values.put(author, book.getAuthor()); values.put(stock, book.getStock()); return db.update(book, values, id?, new String[]{String.valueOf(book.getId())}); } public int delete(int id) { SQLiteDatabase db helper.getWritableDatabase(); return db.delete(book, id?, new String[]{String.valueOf(id)}); } public ListBook query(String keyword) { ListBook result new ArrayList(); SQLiteDatabase db helper.getReadableDatabase(); String sql SELECT id, title, author, isbn, category, stock FROM book; Cursor cursor; if (keyword null || keyword.trim().isEmpty()) { cursor db.rawQuery(sql, null); } else { String like % keyword.trim() %; sql WHERE title LIKE ? OR author LIKE ? ORDER BY id DESC; cursor db.rawQuery(sql, new String[]{like, like}); } while (cursor.moveToNext()) { Book b new Book(); b.setId(cursor.getInt(0)); b.setTitle(cursor.getString(1)); b.setAuthor(cursor.getString(2)); b.setIsbn(cursor.getString(3)); b.setCategory(cursor.getString(4)); b.setStock(cursor.getInt(5)); result.add(b); } cursor.close(); return result; } }这段代码有三个参数细节值得记住。占位符写成一个问号对应的值放进 String 数组顺序不能乱这是标准的参数化查询写法能挡住 SQL 注入相关的追问。LIKE 的百分号要拼在查询词两侧赋值给 like 再传进数组不要在 SQL 里写成LIKE %?%因为占位符只替换字符串字面量百分号放在 SQL 文本里会一起传给 LIKE查出来的结果不符合预期。rawQuery 返回的 Cursor 用完必须 close否则每次搜索都会泄漏一个游标反复输入关键字后内存占用会一路涨。提示上面 WHERE 条件里用了两个问号new String[] 里传的 like 会按顺序填充同一个关键字会同时匹配书名和作者两个字段。query 方法绑定列用的是数字下标 0 到 5。这个写法有个隐患以后 ALTER TABLE 增列SELECT 列表顺序一变getInt(0) 就可能取错列。更稳的写法是改用 cursor.getColumnIndexOrThrow(title)按列名取值代码可读性也更好。3.3 借书和还书的事务边界借书时要做两件事borrow 表插入一条记录book 表的 stock 减一。即使只有一个用户在操作这两步也可能中途失败比如记录插入成功但库存更新异常就会出现借阅记录存在而库存没减的情况。把两步包进同一个事务是这个题目最值得讲清楚的点。public boolean borrowBook(long bookId, long readerId) { SQLiteDatabase db helper.getWritableDatabase(); db.beginTransaction(); try { ContentValues values new ContentValues(); values.put(book_id, bookId); values.put(reader_id, readerId); values.put(borrow_date, DateUtils.today()); values.put(due_date, DateUtils.todayPlusDays(30)); db.insert(borrow, null, values); db.execSQL(UPDATE book SET stock stock - 1 WHERE id ? AND stock 0, new Object[]{bookId}); db.setTransactionSuccessful(); return true; } finally { db.endTransaction(); } }beginTransaction 之后的代码必须执行到 setTransactionSuccessful 才会提交没有调用它就直接 endTransactionSQLite 会把整个事务回滚。setTransactionSuccessful 只能调用一次如果后面还可能有失败分支需要先做条件判断再决定是否标记成功。finally 里的 endTransaction 保证无论有没有异常写锁都会释放不会把数据库长时间锁住。还书动作反过来写把 borrow 表对应记录的 return_date 更新成今天再把 book 表的 stock 加一。有一个隐蔽的点UPDATE 语句要检查受影响行数。上面借书的代码只在 stock 0 时才更新但如果库存已经是 0SQL 不会报错实际没有减少。要拿到这个结果应该把 execSQL 换成 update() 方法用返回值判断是否真正执行成功。4. 图书列表、搜索与删除RecyclerView 适配器和查询防抖落地数据层写完开始处理界面。RecyclerView 适配器、EditText 搜索、长按删除确认这三块是安卓程序设计里最常被考察的控件基本功。图书管理系统的列表页基本就是把这三件事串起来。4.1 BookAdapterViewHolder 和数据刷新RecyclerView 的适配器必须写 ViewHolder。这个强制要求让列表滑动时不至于每次 bind 都重新 findViewById也比 ListView 时代的 convertView 判断清晰得多。public class BookAdapter extends RecyclerView.AdapterBookAdapter.BookViewHolder { private final ListBook data new ArrayList(); private OnBookClickListener listener; public interface OnBookClickListener { void onItemClick(Book book); void onItemLongClick(Book book); } public void setListener(OnBookClickListener listener) { this.listener listener; } public void setData(ListBook books) { data.clear(); data.addAll(books); notifyDataSetChanged(); } NonNull Override public BookViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_book, parent, false); return new BookViewHolder(view); } Override public void onBindViewHolder(NonNull BookViewHolder holder, int position) { Book b data.get(position); holder.tvTitle.setText(b.getTitle()); holder.tvAuthor.setText(b.getAuthor() null ? : b.getAuthor()); holder.tvStock.setText(在库 b.getStock() 本); holder.itemView.setOnClickListener(v - { if (listener ! null) listener.onItemClick(b); }); holder.itemView.setOnLongClickListener(v - { if (listener ! null) listener.onItemLongClick(b); return true; }); } Override public int getItemCount() { return data.size(); } static class BookViewHolder extends RecyclerView.ViewHolder { TextView tvTitle, tvAuthor, tvStock; BookViewHolder(View itemView) { super(itemView); tvTitle itemView.findViewById(R.id.tvTitle); tvAuthor itemView.findViewById(R.id.tvAuthor); tvStock itemView.findViewById(R.id.tvStock); } } }setData 里先 clear 再 addAll不要直接把外部列表赋给成员变量。原因在于搜索和新增图书时会复用同一个 adapter 实例如果外部 List 被重新创建或清除适配器持有的引用会失效UI 就不会刷新。item_book.xml 布局做一个简单的 LinearLayout垂直排列三个 TextView书名加大字号作者和库存用小字根节点高度设 wrap_content宽度 match_parent。在 Activity 里绑定 RecyclerView 时还要记得 setLayoutManager(new LinearLayoutManager(this))漏掉这一步列表不会显示。如果项目已经有空视图布局可以在 adapter 数据为空时把 RecyclerView 隐藏显示一个“没有找到相关图书”的 TextView。4.2 搜索框用 EditText 的 addTextChangedListener 做防抖查询搜索功能看起来简单直接在每个字符变化时查询数据库低配手机上会卡。常见做法是加防抖用户停止输入 300 毫秒后再执行查询。EditText searchInput findViewById(R.id.searchInput); Handler handler new Handler(Looper.getMainLooper()); searchInput.addTextChangedListener(new TextWatcher() { Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} Override public void onTextChanged(CharSequence s, int start, int before, int count) { handler.removeCallbacksAndMessages(null); handler.postDelayed(() - { ListBook list bookDao.query(s.toString()); adapter.setData(list); }, 300); } Override public void afterTextChanged(Editable s) {} });这里把查询任务投递到主线程的 Handler 里每次输入先取消上一个未执行的任务再延迟 300 毫秒执行。快速输入“三体”时中间的“三”“体”都不会单独查一次只有停止输入后才真正走数据库。query 方法内部每次都拿到新的 Cursor查完关闭adapter.setData 会触发整个列表刷新。如果后续数据量真的到了几千条主线程查询还是会卡。可以把 query 放进 ExecutorService查询完再通过 runOnUiThread 切回主线程刷新。图书管理系统的课程设计一般不强制做这一步防抖已经能保证演示流畅。4.3 长按删除先检查借阅再弹出确认删除图书不能只调用 bookDao.delete。如果这本书还有未归还的借阅记录直接删掉会产生悬空引用外键约束生效时会直接报错。更完整的流程是先查 borrow 表确认 book_id 对应的 return_date IS NULL 的记录数为零再允许删除。删除确认用 AlertDialognew AlertDialog.Builder(this) .setTitle(删除图书) .setMessage(确认删除《 book.getTitle() 》) .setPositiveButton(删除, (dialog, which) - { bookDao.delete(book.getId()); adapter.setData(bookDao.query(searchInput.getText().toString())); }) .setNegativeButton(取消, null) .show();长按回调里拿到的是 Book 对象删除时传 id不要按书名删重名书在业务上是允许存在的。删除完成后要重新查询当前搜索条件下的数据并刷新列表否则界面上还会留着已经删掉的那一行。清单文件里记得给 MainActivity 加上 android:windowSoftInputModeadjustResize不然键盘弹出来会把搜索框下面的列表区域挤变形。5. Android Studio 排错Gradle 换源、SDK 无法勾选和真机连接功能写完后最常见的翻车点其实在环境。下面是围绕“用 Android Studio 跑图书管理系统”这个场景最常遇到的问题按频率排一下。5.1 Gradle 换源与 AGP 版本匹配新建项目后第一次构建卡在下载依赖大多数情况是仓库地址访问不稳定。解决方法是修改项目根目录的 settings.gradle把仓库指向可用镜像pluginManagement { repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/gradle-plugin } google() mavenCentral() } }dependencyResolutionManagement 块里的 repositories 也要同步替换。换源后执行 File Sync Project with Gradle Files让 IDE 重新加载配置否则构建仍然按旧仓库走。AGP 版本要和 Gradle 版本对应不是越新越好。Android Studio 新版本会自动生成较新的 AGP但如果工程是从课件或旧设备上拷贝过来的Gradle wrapper 还停留在旧版构建就会报各种不兼容。老工程先升级 gradle wrapper再看编译 SDK 和 build-tools 是否匹配。5.2 SDK 安装时无法勾选“android studio sdk无法勾选”这个搜索词本质上多数不是勾选组件的问题而是 SDK 下载源不可达。SDK Manager 能显示平台列表但复选框是灰色通常是没有显示已安装的包。点右下角 Show Package Details再勾选需要的 SDK Platform 和 build-tools。还要检查 HTTP 代理设置。Settings Appearance Behavior System Settings HTTP Proxy如果是手动代理且代理失效SDK 管理器会被卡死。IDE 界面想要中文可以在插件市场安装中文语言包但对构建没有实质帮助不装也能正常开发。sdk.dir 路径不要带中文和空格。模拟器起不来时先看 local.properties 里这一项是否指向正确目录。模拟器镜像优先选 x86_64对应 API 级别不要太新Android 9 或 10 的镜像启动速度和资源占用都更友好。热搜里问“模拟器哪个版本消耗资源少一点”答案就是选旧 API 的 x86_64 镜像。5.3 真机调试连接小米手机和 adb 授权课堂演示用小米手机很常见。第一次连接时先打开开发者选项里的 USB 调试小米还需要在拨号键盘输码开启 USB 安装权限否则 Android Studio 无法把 APK 装进去。插线后手机会弹出 RSA 指纹确认要勾选“始终允许使用这台计算机进行调试”否则 adb devices 显示 unauthorized。用命令adb devices查看状态最快adb 状态含义处理方向device设备就绪直接运行offline驱动或数据线问题换线重插检查驱动unauthorized手机未确认授权看手机弹窗勾选始终允许no devices调试模式没开重新打开 USB 调试真机演示比模拟器稳至少不会在答辩时突然遇到模拟器内存不足。连接成功后先跑一次adb reverse tcp:8080 tcp:8080这类端口映射再启动项目后续调试会顺手很多。6. 让图书管理系统配得上满分的演示细节种子数据、库存校验和现场问答功能不缺的情况下评分差距往往来自演示体验。一个空列表的 App 和一个打开就有数据、能搜索、能当场操作的 App观感完全不同。6.1 预置数据首次启动注入十本书在 DbHelper.onCreate 里执行完建表语句后用SELECT COUNT(*) FROM book判断表是否为空为空就批量插入十本图书。这样老师装完 App 第一眼看到的是列表而不是空白屏。这十本书建议贴近校园场景比如《数据库系统概论》《算法导论》《三体》《活着》顺手把 isbn 和 category 都写上。演示时直接搜“三体”能出结果搜索动作会显得很自然。预置数据的插入也放进 onCreate因为数据库本来就是第一次创建不存在重复插入的问题。6.2 演示前把借阅数据做旧借阅列表至少要准备两条看得见的事实一条已归还一条超期未还。已归还的显示“已还”超期的在界面上标红或加“超期”文字。实现上列表查询时加一个字段判断 return_date 是否为空再判断 due_date 是否早于今天根据结果设置不同颜色。预置超期记录时把 borrow_date 写成三十天前due_date 写成七天前return_date 留空。这样不依赖系统日期直接演示“查逾期未还”的场景。展示时不需要切到数据库工具界面上就能看到结果说服力强很多。6.3 现场问答的四个高频点答辩时最可能被问到的问题以及回答方向如下这些问题对应的功能不一定全做进界面但源码里要留着字段和查询问题回答方向数据存在哪里SQLite 数据库文件在 App 私有目录路径是 data/data/包名/databases删除图书时有借阅记录怎么办先查未归还记录有则提示先还书没有才允许删除一个读者最多借几本reader.max_borrow 字段与当前未归还数量做比较热门图书排行怎么做按 borrow 表 GROUP BY book_idCOUNT 后倒序取前十条现场演示时按借书、还书、搜索、删除的顺序走一遍最后在源码里找到对应 SQL 指出来。把列表页做完整把问答准备到位这套图书管理系统离满分就只剩最后一步跑一遍./gradlew assembleDebug确认打包能出 APK然后把签名配置留到发布前再补。本文还有配套的精品资源点击获取