Android图书管理系统实战:SQLite存储与ListView列表实现
发布时间:2026/9/25 6:27:29来源:尧图网络
简介基于安卓开发环境打造的图书管理系统项目面向想要入门安卓应用开发或学习SQLite数据库操作的开发者。系统实现了图书信息的增加、删除、修改与查询完整展示了如何通过数据库辅助类管理数据库的创建和版本升级如何使用数据库操作类执行结构化查询语句以及如何利用界面组件构建友好的交互页面。同时项目介绍了二维码扫码入库等可扩展功能为后续增强实用性和用户体验提供了思路。压缩包以zip格式封装整体大小约23.26兆目前已有四千余人学习下载。资源中包含项目源码与知识点梳理读者能够对照练习安卓原生数据库编程、界面布局和性能调优并在此基础上继续拓展整体设计简洁代码结构清晰便于二次开发。1. 安卓图书管理系统为什么我劝你先用 SQLite 把这条链路走通图书管理系统在校园里是最常见的课设题可真正能跑、能演示、能二次修改的版本不多。前几年做课设的人喜欢拿它练手“安卓 sqlite 数据库”因为需求足够清晰录入书、查书、借书、还书总共四件事却能把 Android 的四大组件、数据存储、列表适配全部串起来。这个 Android Studio 版本的图书管理系统我反复拆解过不止一遍整体走的是原生 Java SQLiteOpenHelper 的路线不依赖第三方网络库数据全部落在本机。它适合正在赶课程设计的学生也适合刚把 Android Studio 环境配好、想找一个完整项目练手的开发者。先把这套原生 SQLite 读写链路吃透以后再换 Room 或 GreenDao 才有底气而不是开头就掉进注入框架的坑里。2. 数据层设计表结构、类型亲和性与 SQLiteOpenHelper 的选型细节2.1 实体关系与三张表的核心字段别上来就只建一张表我第一次做图书管理系统时只建了一张 book 表后果是“借阅记录”完全没地方放每次想看谁借了哪本书只能在图书表上加一个状态字段最后连“这本书借出去几天”都算不出来。正确做法是至少拆三张表图书表 book、分类表 category、借阅记录表 borrow_record。图书表的核心字段不能只有书名和作者。书名会重复作者也会重复真正能定位到唯一一本书的是 ISBN所以 ISBN 建议建唯一索引。价格字段用 REAL 类型因为有的书会有 39.8 这样的定价库存用 INTEGER封面路径用 TEXT。还有一个很容易忽略的字段是 location也就是书架位置没有它盘点时只能靠肉眼在整间屋子里翻书。CREATE TABLE category ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, create_time TEXT DEFAULT (datetime(now, localtime)) ); CREATE TABLE book ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT NOT NULL, isbn TEXT NOT NULL UNIQUE, category_id INTEGER, price REAL DEFAULT 0.0, stock INTEGER DEFAULT 1, location TEXT, cover_path TEXT, create_time TEXT DEFAULT (datetime(now, localtime)), FOREIGN KEY (category_id) REFERENCES category(id) ON DELETE SET NULL ); CREATE TABLE borrow_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, book_id INTEGER NOT NULL, borrower_name TEXT NOT NULL, borrow_time TEXT DEFAULT (datetime(now, localtime)), return_time TEXT, FOREIGN KEY (book_id) REFERENCES book(id) ON DELETE CASCADE );这里给 book 表加了外键和唯一约束借阅记录表则用 book_id 关联图书。参数方面要特别注意book.isbn 用 TEXT 而不是 INTEGER因为有些 ISBN 以 0 开头存成数字会丢首位borrow_record 的 return_time 允许为空空值代表这本书还在外面。category 表先建book 表后建否则外键关联会报错。SQLite 默认外键约束是关闭的执行PRAGMA foreign_keys ON;才能生效后面避坑章里会提。2.2 Android 本地存储三选一SharedPreferences、SQLite、Room 的取舍做这个项目之前我先回答了一个问题为什么不用 SharedPreferences也不用 Google 官方推荐的 RoomSharedPreferences 本质是 XML 文件适合保存开关状态和登录 token不适合做条件查询你想查“author 鲁迅”的数据得把整个 XML 读到内存里手动遍历数据一多就会卡。Room 当然更好它有编译期 SQL 校验、支持协程和 Flow但 Room 需要注解处理器依赖很多刚配好 Android Studio 的人卡在 kapt 配置上一报错就是红一整片工程。SQLiteOpenHelper 是 Android 原生提供的数据库帮助类不引入额外依赖SDK 自带的 SQLite 引擎直接可用。取舍参数列在下面。方案依赖成本查询能力升级难度适合场景SharedPreferences无弱只能全量读无概念开关、小数据量 KVSQLiteOpenHelper无强支持 SQL中等需自己写课设、中小型单机应用Room需要 kapt/ksp强低Migration 机制大型项目、团队协作如果是课程设计选 SQLite 的收益比 Room 高一截答辩时老师问起底层原理你能讲清楚 SQLite 的读写过程而不是只会说“Room 帮我封装了”。从后期维护看SQLite 的 SQL 语句可以直接迁移到 MySQL 或 PostgreSQL思维方式是通用的。我一般会直接把 SQLiteOpenHelper 作为首选只有等项目需要多表复杂查询且团队成员都熟 Room 时才换。2.3 SQLiteOpenHelper 的建库建表代码以及版本升级参数DBHelper 是整个系统的地基。这里要强调一个参数version。很多人的数据库版本永远写 1后面加字段时直接改 CREATE TABLE 语句卸载重装才生效这是最大的坑。正确操作是把 version 从 1 升到 2并在 onUpgrade 里补 ALTER TABLE 语句。public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME book_manager.db; private static final int DB_VERSION 1; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE category (...)); db.execSQL(CREATE TABLE book (...)); db.execSQL(CREATE TABLE borrow_record (...)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(ALTER TABLE book ADD COLUMN location TEXT); } if (oldVersion 3) { db.execSQL(CREATE INDEX idx_book_title ON book(title)); } } }onCreate 只在数据库文件第一次创建时执行之后的建表更新全部走 onUpgrade。数据库文件名固定为 book_manager.db存在/data/data/包名/databases/下。super(context, DB_NAME, null, DB_VERSION) 的第三个参数是 CursorFactory传 null 即可表示使用默认工厂。onUpgrade 里每段升级脚本都加了 if 判断保证旧版本从 1 升到 3 时不会遗漏中间步骤。3. 在 Android Studio 里把功能跑通ListView 与 CRUD 的完整调用链3.1 工程结构与 Gradle 配置SDK 版本和依赖参数怎么设下载下来的工程导入 Android Studio 时最容易翻车的不是代码而是 Gradle 版本与本地 SDK 不匹配。这个图书管理系统是老牌的 Android Studio 版本用的还是传统 View 体系没有 Compose。我习惯把 compileSdk 设为 33minSdk 设为 21targetSdk 设为 33这样既能覆盖绝大多数手机又不会碰到太新的权限政策。android { compileSdk 33 defaultConfig { applicationId com.example.bookmanager minSdk 21 targetSdk 33 versionCode 1 versionName 1.0 } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 }minSdk 21 意味着 Android 5.0 以上都能跑覆盖了绝大多数老机型。targetSdk 33 意味着需要处理运行时权限后面导出备份会用到。compileOptions 把 Java 版本固定在 1.8lambda 表达式可用即可不需要升级到 17。这里没有引入 RecyclerView 依赖因为项目原本用 ListView 实现列表我建议初期先沿用等你能熟练改写后再换 RecyclerView 不迟。Android Studio 首次导入工程后如果一直卡在 Gradle sync可以在 gradle-wrapper.properties 里把 distributionUrl 改成你的 Android Studio 自带的 Gradle 版本。3.2 ListView 自定义适配器图书列表最常见写法图书列表展示是用户看到的第一个界面也是“安卓开发如何将搜索到的蓝牙设备显示到 listview 上”这类问题的同款模型。ListView 本身不存数据只负责滚动显示数据源是 Cursor 或 List中间层靠适配器把数据映射到每一项布局上。下面这段是 BookAdapter 的核心逻辑。public class BookAdapter extends BaseAdapter { private ListBook bookList; private LayoutInflater inflater; public BookAdapter(Context context, ListBook bookList) { this.bookList bookList; this.inflater LayoutInflater.from(context); } Override public int getCount() { return bookList null ? 0 : bookList.size(); } Override public Object getItem(int position) { return bookList.get(position); } Override public long getItemId(int position) { return position; } Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { convertView inflater.inflate(R.layout.item_book, parent, false); holder new ViewHolder(); holder.tvTitle convertView.findViewById(R.id.tv_title); holder.tvAuthor convertView.findViewById(R.id.tv_author); holder.tvStock convertView.findViewById(R.id.tv_stock); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } Book book bookList.get(position); holder.tvTitle.setText(book.getTitle()); holder.tvAuthor.setText(book.getAuthor()); holder.tvStock.setText(库存: book.getStock()); return convertView; } static class ViewHolder { TextView tvTitle; TextView tvAuthor; TextView tvStock; } }getCount 决定列表长度getView 里的 convertView 复用是重中之重不复用的话每次滚动都 inflate 一个新布局列表超过 50 条会明显掉帧。ViewHolder 用 setTag 缓存了三个 TextView省去重复 findViewById。position 参数是当前数据下标注意它不是数据库里的 id删除和修改操作要拿 position 去 bookList 取对象再用对象的 id 去操作数据库。TextView 显示价格时如果值是 39.8直接拼字符串会显示 39.8但如果是整数 39SQLite 的 REAL 类型读出来可能是 39.0建议用String.format(%.2f, price)格式化。3.3 新增、修改、删除和借还书背后的完整调用链列表只是读操作真正要跑通的是增删改。新增和修改都离不开 ContentValues它是 SQLite 插入和更新的值载体底层还是键值对但比拼 SQL 字符串安全得多能有效避免 SQL 注入。下面这段是新增图书和删除图书的 DAO 方法。public long insertBook(Book book) { SQLiteDatabase db dbHelper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(title, book.getTitle()); values.put(author, book.getAuthor()); values.put(isbn, book.getIsbn()); values.put(category_id, book.getCategoryId()); values.put(price, book.getPrice()); values.put(stock, book.getStock()); long id db.insert(book, null, values); db.close(); return id; } public int deleteBook(long bookId) { SQLiteDatabase db dbHelper.getWritableDatabase(); int rows db.delete(book, id ?, new String[]{String.valueOf(bookId)}); db.close(); return rows; } public int updateStock(long bookId, int newStock) { SQLiteDatabase db dbHelper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(stock, newStock); int rows db.update(book, values, id ?, new String[]{String.valueOf(bookId)}); db.close(); return rows; }db.insert 的第二个参数 nullColumnHack 传 null 即可只有在 values 为空时才需要指定一个可空列名。delete 和 update 的第三个参数是 whereClause里面用?占位第四参数是占位符的值这是标准的防注入写法不要用字符串拼接方式传书号。每次操作完都必须 db.close()否则会一直占用数据库连接后续操作可能出现 database is locked 异常。借书和还书不能只改图书表的 stock还必须往 borrow_record 里写记录这需要放在一个事务里执行。public void borrowBook(long bookId, String borrower) { SQLiteDatabase db dbHelper.getWritableDatabase(); db.beginTransaction(); try { db.execSQL(UPDATE book SET stock stock - 1 WHERE id ?, new Object[]{bookId}); db.execSQL(INSERT INTO borrow_record(book_id, borrower_name) VALUES(?, ?), new Object[]{bookId, borrower}); db.setTransactionSuccessful(); } catch (Exception e) { e.printStackTrace(); } finally { db.endTransaction(); db.close(); } }beginTransaction 开启事务后如果中途任何一条语句失败catch 里不调用 setTransactionSuccessfulendTransaction 会自动回滚。这样不会出现“库存减了但借阅记录没写”的脏数据。如果项目里还要做还书逻辑就是 return_time 写入当前时间同时 stock 加 1。把 stock 的增减直接写死在 SQL 里而不是先查再改能避免并发修改时的数据不一致。4. 避坑 / 常见问题跑安卓图书管理最容易翻车的五个点4.1 数据库创建成功ListView 却一片空白现象控制台没有报错logcat 也不打印异常Data 目录里能看到 book_manager.db 文件但 ListView 界面就是空的。原因查询用的游标没有调用 moveToFirst或者数据源根本没加载Adapter 拿到的就是一个空 List。还有一个隐蔽点数据库写操作成功了但查询时机在写操作之前。解决查询前检查 cursor.getCount()Adapter 的 setData 之后必须调用 notifyDataSetChanged()。如果是异步查询回主线程后 setAdapter 要放在查询结果返回后执行。4.2 改了表结构不升版本号一启动就崩现象开发阶段给 book 表加了字段重新运行 App 后闪退logcat 里报 no such column。原因数据库文件已存在onCreate 不会重新执行版本号还停留在 1onUpgrade 也没触发。解决每次改表结构必须同时把 DB_VERSION 的数值加 1并在 onUpgrade 里写对应的 ALTER TABLE 语句。注意升级脚本必须是增量式的不能只写一条最新的建表语句否则老用户从 1 升到 2 时会把不该删的字段清掉。4.3 主线程跑大查询数据过千就 ANR现象本地数据库只有两三百条书时很流畅导入一千条后界面卡死过一会儿弹出“系统无响应”。原因getWritableDatabase 和查询全部跑在 UI 线程SQLite 在低端机上每次磁盘 I/O 耗时可能达到几十毫秒叠加 ListView 的滑动加载后就出问题。解决数据量小的时候可以直接在主线程跑超过一千条建议改用线程池或 AsyncTask查询完成后通过 Handler 回主线程更新 UI。至少把搜索和统计这类重查询移到子线程列表的直接查询可以在开发环境中先顶住。4.4 图书标题中文乱码以及 ISBN 变成科学计数法的坑现象从 CSV 批量导入时中文书名变成“???”ISBN 数字显示成 9.78074E11。原因CSV 文件用 GBK 编码读取Android 默认用 UTF-8 解析导致乱码ISBN 在导入时被解析成数字类型存进 DOUBLE 后自动转成科学计数法。解决批量导入统一用 UTF-8 读流new InputStreamReader(new FileInputStream(file), UTF-8)不要用 FileReader。ISBN 在表结构和 Bean 类里都用 String解析时先 trim如果 Excel 导出的内容带了不可见字符需要手动清洗。4.5 目标 SDK 33 后导出的备份文件在手机里看不到现象代码里写了 exportDatabase用 Environment.getExternalStorageDirectory() 拼接路径Toast 提示导出成功但打开文件管理器找不到文件。原因从 Android 10 开始分区存储生效应用不能直接往公共目录写文件getExternalStorageDirectory 访问的是应用限定目录。解决targetSdk 29 以上导出推荐用 MediaStore.Downloads 或 ACTION_CREATE_DOCUMENT。后者弹系统保存框用户自己选择保存位置最省心。这也是我在第五章里给的方案避免 WRITE_EXTERNAL_STORAGE 权限申请一堆但仍旧写不进去的尴尬。5. 进阶给图书管理加一个全文搜索和备份导出的“售后能力”5.1 用 LIKE 还是 FTS4数据量决定策略图书管理系统的搜索框最常见的需求是“按书名模糊查”。数据量在 5000 条以内一个 LIKE 查询完全够用。SELECT * FROM book WHERE title LIKE % || ? || %性能可以接受代码也最好维护。但如果你导入的是学校几万册馆藏LIKE 的隐患就出来了前导通配符会让索引失效全表扫描耗时指数上升。这时我建议建 FTS4 虚拟表用 SQLite 内置的全文索引。CREATE VIRTUAL TABLE book_fts USING fts4( title, author, category, contentbook );创建虚拟表后通过触发器把 book 表的增删改同步到 book_fts查询用 MATCH速度比 LIKE 快一个量级。这里有个坑FTS4 的 content 表是外部内容表必须先有基础表才能建。如果你用的 Android 自带 SQLite 版本较老FTS4 一般已内置而 FTS5 需要看系统版本兼容性不如 FTS4。课设阶段不用追求这个知道边界在哪里就够。5.2 给用户后悔药CSV 导出到 Download 目录本地数据库最大的风险是卸载重装数据全清。所以我会习惯性地在设置页放一个“导出备份”按钮。代码上最省心的不是直接写文件而是用系统文件选择器让用户自己决定存放位置。private void exportDatabase() { File dbFile new File(getDatabasePath(book_manager.db).getPath()); Uri contentUri MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY); ContentValues values new ContentValues(); values.put(MediaStore.Downloads.DISPLAY_NAME, book_manager_backup.csv); values.put(MediaStore.Downloads.MIME_TYPE, text/csv); Uri fileUri getContentResolver().insert(contentUri, values); try { OutputStream os getContentResolver().openOutputStream(fileUri); // 从 SQLite 读出所有 book 表数据按 CSV 格式写入 os } catch (IOException e) { e.printStackTrace(); } }这段代码省去了读写权限申请只要用户在系统选择器里确认保存位置即可。恢复备份时反过来读 CSV逐行解析并插入数据库注意用事务包裹整个恢复过程。我一般会导出 CSV 而不是直接拷贝 .db 文件因为 CSV 能被 Excel 直接打开老师检查也好用户自留也好都更方便。5.3 从安装到验收一次完整的复现清单工程下载后我建议你按下面顺序走一遍而不是直接打开就乱点。首次运行先添加一个分类“文学”再录入三本书其中一本设库存为 2然后发起借书操作去列表确认库存减 1回到详情页把书的库存手动改成 0触发一次借书看它是否允许借出再测搜索框输入书名关键词确认返回结果最后卸载 App 前做一次导出备份重装后导入回来确认数据完整。这一套走完这个项目的安全边界和心理预期也就摸清了。最后说一句我的习惯。第一次做这个项目时我在验证搜索功能时直接点了搜索键结果界面卡住那时我才意识到主线程查询的隐患有多大。从此以后每次写完 SQLite 相关代码我都会强制自己过一遍事务、版本号、游标关闭和主线程耗时这四件事项目翻车率明显下降。这次拆解的 Android 图书管理系统完整代码包括数据库脚本、适配器、增删改查界面和导出备份功能都在工程包内直接导入 Android Studio 就能跑。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网