资讯动态

安卓单机点餐系统开发:SQLite数据库设计与实现全解析

发布时间:2026/10/19 8:39:21 来源:尧图企业网站定制
简介一份面向安卓初学者的单机点餐系统期末作业工程采用Eclipse开发数据库使用SQLite无需联网即可完成登录、点餐、查看订单等基本功能系统内置测试账号登录后即可体验完整流程适合计算机专业学生作为课程设计或期末项目参考。压缩包为RAR格式共176个文件总体积33.53MB文件类型包括PNG界面图片、JAVA源码、XML布局文件、CLASS编译文件、JAR依赖库、DB数据库文件以及可直接安装的APK安装包并附有数据库说明文档目录结构清晰便于按需查看与修改。已有3133人学习下载通过这份项目可以获得完整源码、安装包、数据库文件和界面素材既能对照理解安卓基础控件的使用也能直接修改功能并生成自己的期末作业对学习SQLite操作和单机应用开发有实际帮助。1. 安卓单机点餐系统期末作业的完整落地方案做Android期末项目最怕什么不是不会写代码而是写出来的东西要么功能太简陋撑不起“系统”二字要么复杂度失控改到崩溃。这套单机点餐系统正好卡在中间基于SQLite做本地存储覆盖菜单浏览、购物车管理、订单生成、历史查询的完整闭环核心代码量控制在800行左右一两天能跑通但又足够撑起课程设计或期末答辩的“工作量展示”。你不用联网、不用搭服务器、不用处理并发所有数据落在手机本地数据库里拿到源码改一改就能交。这套资源适合几类人被要求做“安卓应用类课程设计”但还没定题的学生已经写完界面但不知道订单数据怎么设计的初学者以及想快速看懂SQLite增删改查如何嵌入Activity生命周期的开发者。它解决的不是“会不会写Android”而是“如何在有限时间内交付一个逻辑完整、可演示、可扩展的课设项目”。下面从数据库设计到踩坑修复按我实际拆解这个项目的顺序逐一展开。2. SQLite数据层设计四张表如何撑起单机版点餐闭环2.1 表结构与字段取舍为什么不用一张大表很多第一次做点餐系统的同学会试着把菜单、购物车、订单全塞进一个表里字段多到没法看。这套项目里用了四张表各管一段menu_item存菜品基础信息cart存当前正在点的购物车orders存订单主表order_detail存订单明细。拆开的好处是购物车清空不影响订单历史订单表能独立做状态统计。CREATE TABLE menu_item ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL NOT NULL, category TEXT DEFAULT 未分类, image_res INTEGER DEFAULT 0, is_available INTEGER DEFAULT 1 ); CREATE TABLE cart ( id INTEGER PRIMARY KEY AUTOINCREMENT, menu_id INTEGER NOT NULL, name TEXT NOT NULL, price REAL NOT NULL, quantity INTEGER DEFAULT 1 ); CREATE TABLE orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, table_no TEXT DEFAULT A01, total_price REAL NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime(now, localtime)), status INTEGER DEFAULT 0 ); CREATE TABLE order_detail ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, menu_id INTEGER NOT NULL, name TEXT NOT NULL, price REAL NOT NULL, quantity INTEGER NOT NULL );menu_item里的image_res字段存的是drawable资源的整型ID这是单机版的关键思路不上传图片文件直接用R.drawable.xxx的int值入库避免Bitmap序列化的麻烦。is_available是个软删除标记下架菜品不用物理删除只在查询时过滤。orders表的created_at用SQLite内置的datetime函数生成本地时间比在Java代码里拼字符串更可靠时区问题由SQLite直接处理。2.2 购物车与订单的关联逻辑购物车表和订单明细表结构几乎一样区别在于cart是临时态order_detail是持久态。下单动作本质上是“把cart的数据复制到order_detail再清空cart”——这个场景很多教程讲不清楚学生容易做成“把购物车数据改名成订单”导致清购物车时订单历史也跟着没了。-- 插入订单主表后拿到自增ID INSERT INTO orders (table_no, total_price, status) VALUES (A01, 45.5, 0); -- 用 last_insert_rowid() 获取刚才的订单号 INSERT INTO order_detail (order_id, menu_id, name, price, quantity) SELECT last_insert_rowid(), menu_id, name, price, quantity FROM cart; -- 下单成功后清空购物车 DELETE FROM cart;逻辑说明这三步必须在同一个事务里执行否则会出现“订单主表插入了但明细没插进去”的脏数据。GO. Android中SQLite的事务要么自己写beginTransaction要么用SQLiteDatabase提供的runInTransaction方法。至于为什么不在Java层做for循环逐条插入——你会在下一条命令里看到用SQL子查询一次完成的写法既减少代码量也避免多表操作时忘记同步。参数说明last_insert_rowid()是SQLite特有的函数只取当前连接最近一次INSERT的自增ID。如果你用ContentValues先插入orders再想拿ID就需要用Cursor query一下比这个SQL写法绕很多。建议App层直接用sqlite的execSQL配合bindArgs别套ContentValues代码逻辑反而更清晰。2.3 DBHelper写法的边界别把数据库操作堆在Activity里这套项目里DBHelper继承SQLiteOpenHelperonCreate里一次性执行上述建表语句onUpgrade里做了最简处理——DROP掉所有表再重建。这算偷懒但可接受的做法因为课设阶段不存在“用户升级App保留数据”的需求重点是演示数据库被正确创建和初始化。public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME restaurant.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 IF NOT EXISTS menu_item (...)); db.execSQL(CREATE TABLE IF NOT EXISTS cart (...)); db.execSQL(CREATE TABLE IF NOT EXISTS orders (...)); db.execSQL(CREATE TABLE IF NOT EXISTS order_detail (...)); initMenuData(db); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(DROP TABLE IF EXISTS menu_item); db.execSQL(DROP TABLE IF EXISTS cart); db.execSQL(DROP TABLE IF EXISTS orders); db.execSQL(DROP TABLE IF EXISTS order_detail); onCreate(db); } private void initMenuData(SQLiteDatabase db) { // 预置6~8条菜品方便演示 db.execSQL(INSERT INTO menu_item (name, price, category, image_res) VALUES (宫保鸡丁, 28.0, 热菜, R.drawable.dish1 )); db.execSQL(INSERT INTO menu_item (name, price, category, image_res) VALUES (番茄蛋汤, 12.0, 汤类, R.drawable.dish2 )); } }代码逻辑说明execSQL里直接拼接R.drawable的int值因为它在编译期就是个常量不会产生注入问题。但你在自己写的时候要注意一个反模式——很多初学者会在onCreate里先判断表是否存在再去插入数据在SQLiteOpenHelper里这是多余的因为onCreate只在首次创建数据库时执行一次不存在重复insert的问题。参数说明DB_VERSION初始设为1。如果你后期在onCreate里加了表记得把版本号1否则Android认为数据库没变化不会执行onUpgrade新表就永远不会建出来。这是排查“为什么我加了表但运行时没有”的最常见原因。2.4 预置图片资源的坑image_res存的是Int值把菜品图片以int形式存数据库属于取巧方案。好处是不用处理文件流数据库直接存数字List列表加载图片时用Picasso或Glide传入这个int即可坏处是如果你在别的设备上跑R.drawable的常量值可能不同——实际上同一个APK在不同设备上R值恒定所以这个坑其实不存在。public ListMenuItem getMenuList() { ListMenuItem list new ArrayList(); SQLiteDatabase db getReadableDatabase(); Cursor cursor db.rawQuery(SELECT id, name, price, category, image_res FROM menu_item WHERE is_available 1, null); while (cursor.moveToNext()) { MenuItem item new MenuItem(); item.setId(cursor.getInt(cursor.getColumnIndexOrThrow(id))); item.setName(cursor.getString(cursor.getColumnIndexOrThrow(name))); item.setPrice(cursor.getDouble(cursor.getColumnIndexOrThrow(price))); item.setCategory(cursor.getString(cursor.getColumnIndexOrThrow(category))); item.setImageRes(cursor.getInt(cursor.getColumnIndexOrThrow(image_res))); list.add(item); } cursor.close(); db.close(); return list; }用getColumnIndexOrThrow而不用getColumnIndex好处是列名写错时直接抛异常方便定位问题而不是返回-1再越界崩溃。Cursor用完必须close否则多次打开数据库会触发“Unable to open database file”或者内存泄漏。db.close()在单机小项目里问题不大但如果你在Activity里频繁打开数据库建议用单例DBHelper不要让每个页面都new一个。还有一点要注意SQLiteDatabase在跨线程使用时不安全如果你在子线程查库务必用同一个helper实例且不要在主线程和子线程同时拿同一个db对象执行写操作。3. 核心业务代码拆解点餐、结算、订单查询三个模块的实现路径3.1 菜单列表的加载与点击RecyclerView绑定SQLite数据菜单列表用RecyclerView承载Adapter接收DBHelper查询返回的List。关键点是Adapter构造时不直接做数据库查询而是把数据作为参数传进来让Adapter纯粹做视图绑定职责分离。public class MenuAdapter extends RecyclerView.AdapterMenuAdapter.ViewHolder { private ListMenuItem data; private OnItemClickListener listener; public MenuAdapter(ListMenuItem data) { this.data data; } Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_menu, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(ViewHolder holder, int position) { MenuItem item data.get(position); holder.nameTv.setText(item.getName()); holder.priceTv.setText(¥ item.getPrice()); holder.imageView.setImageResource(item.getImageRes()); holder.itemView.setOnClickListener(v - { if (listener ! null) listener.onItemClick(item); }); } Override public int getItemCount() { return data null ? 0 : data.size(); } }代码逻辑说明图片加载直接用setImageResource因为image_res本身就是R.drawable的int值不需要任何图片加载库。如果你准备的图片在res/drawable里可以这么存如果图片是网络URL或者文件路径就不适合这个方法了需要把image_res改成字符串字段存URL或路径再引入Glide加载。参数说明item_menu布局里建议用LinearLayout或CardView包裹一个ImageView和两个TextView。价格显示用¥ price的字符串拼接虽然简单但能避免浮点显示精度问题——比如28.0会显示成28.0如果你想要“¥28”就得用DecimalFormat处理。3.2 添加购物车与数量校验点菜的核心操作是往cart表插数据或更新数量。同一个菜点了两次正常逻辑应该是把quantity加1而不是插两条记录。这个逻辑不复杂但代码写起来要看清楚public void addToCart(MenuItem item) { SQLiteDatabase db getWritableDatabase(); // 先查购物车是否已有同款菜 Cursor cursor db.rawQuery( SELECT id, quantity FROM cart WHERE menu_id ?, new String[]{String.valueOf(item.getId())}); if (cursor.moveToFirst()) { int cartId cursor.getInt(0); int oldQty cursor.getInt(1); db.execSQL(UPDATE cart SET quantity ? WHERE id ?, new Object[]{oldQty 1, cartId}); } else { db.execSQL(INSERT INTO cart (menu_id, name, price, quantity) VALUES (?, ?, ?, ?), new Object[]{item.getId(), item.getName(), item.getPrice(), 1}); } cursor.close(); db.close(); }这里用了占位符?传参数而不是拼接字符串这是一个好习惯。虽然菜单数据内部可控不拼字符串可以避开转义单引号之类的麻烦尤其是菜品名里有可乐鸡翅这种不带引号没事但万一用户自定义菜品里有引号就会SQL注入或者SQL语句异常——写SQLite操作时统一用绑定参数你后面排查问题会少很多。逻辑说明先SELECT再UPDATE或INSERT是经典的“检查-更新”模式。这个操作在单机单用户情况下安全但如果未来做成多人联网版这地方就需要加事务和锁否则并发下数量会错乱——课设阶段不用担心答辩时你可以主动提这个升级点反而加分。3.3 结算逻辑事务、总价计算与购物车清空结算是最容易出逻辑错的环节。有些学生先清空购物车再拿空购物车去算总价结果订单金额永远是0。套项目的标准顺序是先从cart表聚合查询总价再插入orders表然后把cart明细复制到order_detail最后才DELETE FROM cart。这四步任何一步失败订单状态就不完整。db.beginTransaction(); try { // 1. 计算总价 Cursor totalCursor db.rawQuery(SELECT SUM(price * quantity) FROM cart, null); double total 0; if (totalCursor.moveToFirst()) { total totalCursor.getDouble(0); } totalCursor.close(); // 2. 插入订单主表 db.execSQL(INSERT INTO orders (table_no, total_price, status) VALUES (?, ?, 0), new Object[]{tableNo, total}); // 3. 复制明细到订单表 db.execSQL(INSERT INTO order_detail (order_id, menu_id, name, price, quantity) SELECT last_insert_rowid(), menu_id, name, price, quantity FROM cart); // 4. 清空购物车 db.execSQL(DELETE FROM cart); db.setTransactionSuccessful(); } finally { db.endTransaction(); }时间点说明INSERT INTO order_detail里用last_insert_rowid()取到的正是上一步INSERT的order_id这个过程必须发生在同一个事务里。如果你拆成三次execSQL且不包事务任何一次中途崩溃都会造成订单表有记录但明细表没有或者购物车清了但订单没建。参数说明table_no可以做成EditText让用户填也可以默认“A01”最后在收银台界面允许修改。total_price在orders表里存REAL类型注意SQLite的浮点计算会有精度误差比如19.9加0.1可能变成20.0或20.000000000000004。课设阶段可以用DecimalFormat在UI层做四舍五入但如果你追求严谨应该把价格字段从REAL改成以“分”为单位的INTEGER这是生产系统的标准做法。3.4 订单历史查询列表与状态展示订单查询模块用两个界面还是用一个界面——这是很多初学者纠结的点。这套资源的做法是一个订单列表Activity点击一条订单后弹AlertDialog展示明细不额外建第二个Activity省代码又能说明白数据关系。public void showOrderDetail(long orderId) { SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.rawQuery(SELECT name, price, quantity FROM order_detail WHERE order_id ?, new String[]{String.valueOf(orderId)}); StringBuilder sb new StringBuilder(); double total 0; while (cursor.moveToNext()) { String name cursor.getString(0); double price cursor.getDouble(1); int qty cursor.getInt(2); sb.append(name).append( x).append(qty).append( ¥) .append(price * qty).append(\n); total price * qty; } cursor.close(); // 用AlertDialog展示sb.toString()和总价 }这段代码的核心思路是一行Cursor对应一个菜品明细拼成字符串方便展示同时累加总价做二次校验。这样即使orders表里的total_price被误改你也能通过明细重新算一遍真实金额算是一个简单的数据一致性校验手段。分页查询如果订单数量大了比如超过50条建议在查询orders时LIMIT 20 OFFSET 0或在ListView/RecyclerView里做滚动加载。期末作业不需要但面试时你主动提到这个优化点会让人觉得你不是“为了交作业而写”。3.5 菜品上下架的简单实现is_available标志位实战菜单管理页做两个操作添加新菜INSERT和修改售卖状态UPDATE is_available。不需要DELETE物理删除因为已有历史订单引用了menu_id物理删除会导致订单明细“查无此菜”页面显示空白。is_available0表示下架在查询菜单列表时加过滤条件即可。public void toggleAvailability(int menuId) { SQLiteDatabase db getWritableDatabase(); db.execSQL(UPDATE menu_item SET is_available CASE WHEN is_available 1 THEN 0 ELSE 1 END WHERE id ?, new Object[]{menuId}); db.close(); }这里用SQLite的CASE语法实现“一次UPDATE完成取反”不用先查再改少一次I/O且不会出现并发问题。单机版可能感觉不到差别但这个写法放在生产环境也是正确的。新菜添加页面就两个输入框名字、价格、一个下拉框选分类和一个图片选择。图片部分用固定几个drawable资源让用户挑不用相册导入原理和前面image_res存Integer一样可以省掉运行时权限申请的麻烦。如果你想让项目显得更完整可以加一个“上架商品数量统计”的TextView查SELECT COUNT(*) FROM menu_item WHERE is_available 1就能显示。4. 避坑指南单机点餐项目最常见的8个翻车现场4.1 数据库文件没生成运行时“no such table: menu_item”现象App安装后一打开菜单页就崩溃Logcat报android.database.sqlite.SQLiteException: no such table: menu_item。原因DB_VERSION没改或者数据库之前已经创建过但表没建对。最常见的场景是第一次代码里建表语句写错了修复后没有卸载App直接跑——数据库文件已经存在SQLiteOpenHelper的onCreate不会再次执行表依然是错的。解决卸载App重装或者开发阶段每次改表结构都调高DB_VERSION触发onUpgrade里DROP表重建。我一般开发时直接把onUpgrade改成“DROP所有表onCreate”上线前再改成真正的迁移脚本课设阶段无脑重建就是最快的调试方式。4.2 购物车点两次同款菜出现两条重复记录而不是合并数量现象UI上购物车列表出现两行“可乐鸡翅”数量都是1而不是一行数量2。原因没有按menu_id检查购物车中是否已有该菜品直接无条件INSERT。解决严格按3.2节中的写法先SELECT再UPDATE或者用SQL语句INSERT OR UPDATE。SQLite原生支持“INSERT ... ON CONFLICT(menu_id) DO UPDATE SET quantity quantity 1”加上一条UNIQUE约束能一步解决但很多教材没讲这个语法。不一定非要炫技改这个但至少先查再插的逻辑必须写对。4.3 订单金额变成0.00现象结算完成订单列表里显示的total_price为0.00但点开明细里菜品价格都对。原因执行顺序错了。先DELETE了cart再执行SELECT SUM(price*quantity) FROM cart此时cart已经空了SUM永远为NULL。解决结算逻辑严格按3.3节的顺序。先查再插再清事务包裹。如果你之前已经写反了改顺序就行。4.4 图片不显示ImageView空白现象菜单列表文字正常但图片区域是空白的没有崩溃。原因一种可能是image_res在数据库里存的是0ImageView.setImageResource(0)不会崩溃但也不显示任何图片。另一种可能是R.drawable的常量在数据库里存的时候是对的但后来你删了drawable里的某张图运行时引用已不存在的资源ID会崩溃。解决把menu_item表里image_res字段的默认值从0改为某个确定存在的资源ID比如R.drawable.default_food。添加菜品页面提供的图片选项只从现有drawable资源里选禁止用户输入任意int。4.5 数据的表结构改了旧数据错乱现象原本menu_item只有5个字段后来加了is_available字段但运行时报“no such column: is_available”。原因数据库文件是旧的没触发onCreate。解决开发阶段把DB_VERSION往上加触发onUpgrade的DROP重建即可。这个坑的本质和4.1一样关键在于记住“SQLiteOpenHelper只在版本号变化时执行onUpgrade”。4.6 主线程访问数据库导致卡顿现象菜单数据只有几十条但列表滑起来掉帧结账时界面卡一两秒。原因所有数据库操作都在UI主线程执行。虽然单机版数据量小不至于ANR但在低端机上确实有感知。解决简单做法是保持现状课设答辩不会有人拿低端机测你的性能如果你想展示工程能力用ExecutorService异步查询结果通过runOnUiThread回调更新Adapter。这里不建议引RxJava或协程课设项目引入太复杂反而会把自己绕晕。4.7 SQLite数据库文件锁SQLiteDatabaseLockedException现象快速点击“结算”按钮两次第二次点击直接崩溃报SQLiteDatabaseLockedException。原因第一次结算的事务还没释放第二次点击又发起写操作SQLite单文件锁冲突。所有订单操作都在主线程有可能前一个execSQL还没跑完后一个就进来了。解决按钮加防重复点击——在onClick里先setEnabled(false)结算成功后再setEnabled(true)简单粗暴但好用。更好的做法是结算动作发起后显示ProgressDialog直到事务完成再关闭。4.8 模拟器上正常真机上数据库路径找不到现象在模拟器调试一切正常换到真机安装后查询不到任何数据。原因模拟器和真机的包名相同但数据目录不同如果你代码里写死了数据库路径比如直接用/data/data/包名/database/restaurant.db模拟器的路径可能因为权限问题不同。更常见的是你从Evernote或博客抄了一段用Context.getDatabasePath()的代码但传入的context是Application还是Activity不对导致路径解析偏差。解决统一用SQLiteOpenHelper的构造方法传DB_NAME不要自己拼绝对路径。这样系统会帮忙把数据库放到应用专属的databases目录不需要你关心具体位置。提示以上8个坑是这套项目从能跑到能交之间的所有主要关卡。多数崩溃类问题卸载重装就能恢复数据错乱类问题需要清空App数据再启动这两个操作要记熟。5. 这套资源的扩展路径从课设到简历项目的三个升级方向5.1 加一个顾客端/服务员端切换登录角色当前版本是单机单角色所有操作都在一个界面里完成。如果改成“启动时选择服务员/顾客”只需在入口Activity加一个角色选择然后按角色控制可见的Tab。数据表层面不需要改动因为这个改动只是UI层的路由切换orders表加一个staff_name字段记录是谁处理的订单就能演示“多角色操作留痕”的概念。这个升级成本低、展示效果好期末答辩时“业务逻辑”评分项会明显提高。5.2 订单状态机从0到3的状态流转当前orders表的status只有0和1两个值你可以扩展为0-待支付1-已支付2-制作中3-已完成。在订单列表页做“状态更新”按钮通过UPDATE orders SET status status 1来实现简单的状态递进。注意做边界控制status为3时不能再加UI上禁用按钮。这个模块的价值在于让学生理解“表的字段不只是存储还包含业务状态”——面试时你能把这个讲清楚比顺一遍“我会SQLite”有说服力得多。5.3 数据导出把订单历史写成CSV文件单机App无法联网但“导出数据”是一个让项目看起来更完整的功能。在订单列表页增加一个“导出”按钮使用OpenFileHelper写入一个CSV文件把orders表和order_detail表的查询结果拼接成逗号分隔的文本。这个功能涉及Android存储权限Android 10以上需要MANAGE_EXTERNAL_STORAGE或使用MediaStore课设阶段可以在导出成功后用ShareCompat调起系统分享面板避免动态权限申请。核心代码大概30行但带来的“工程完整性”观感提升非常明显。public void exportOrdersToCsv(Context context) { ListString lines new ArrayList(); lines.add(订单号,桌号,金额,时间); Cursor cursor db.rawQuery(SELECT id, table_no, total_price, created_at FROM orders, null); while (cursor.moveToNext()) { String line cursor.getInt(0) , cursor.getString(1) , cursor.getDouble(2) , cursor.getString(3); lines.add(line); } cursor.close(); // 用 ShareCompat 调系统分享选择邮件或备忘录保存 Intent shareIntent new Intent(Intent.ACTION_SEND); shareIntent.setType(text/csv); shareIntent.putExtra(Intent.EXTRA_TEXT, TextUtils.join(\n, lines)); context.startActivity(Intent.createChooser(shareIntent, 导出订单)); }这里的参数说明示例中没有真正写文件而是直接把CSV内容通过Intent传递好处是不需要存储权限。如果你想存成真实文件再分享需要FileProvider并且AndroidManifest里要配置provider节点。课设阶段用这种方式最快答辩时你可以补充说明真实保存方案的设计思路而不是真的去实现。这三个扩展方向有一个共同特点都是基于现有表结构做增量改动不需要推翻重写。所以拿到这套资源后不要急着改UI换主题先把基础跑通了再挑一个方向做定制“工作量”和“创新点”自然就立体起来了。6. 单元测试与数据验证用SQLite内存库配合Robolectric跑通逻辑这个技巧是我自己拆课设项目时养成的习惯不依赖Android模拟器直接用JVM跑Robolectric测试验证数据库相关代码的正确性。Robolectric允许你在普通的JUnit测试里使用Android API包括SQLiteOpenHelper。测试时数据库文件写到临时目录测试结束自动清理不会污染你手机里已装App的真实数据。RunWith(RobolectricTestRunner.class) public class MenuRepositoryTest { private DBHelper helper; Before public void setUp() { helper new DBHelper(RuntimeEnvironment.getApplication()); } Test public void addToCart_shouldIncreaseQuantityWhenSameItemAddedTwice() { MenuItem item new MenuItem(); item.setId(1); item.setName(宫保鸡丁); item.setPrice(28.0); helper.addToCart(item); helper.addToCart(item); SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.rawQuery(SELECT quantity FROM cart WHERE menu_id 1, null); cursor.moveToFirst(); int quantity cursor.getInt(0); cursor.close(); assertEquals(2, quantity); } }逻辑说明这个测试第一次跑如果通过说明addToCart的“先查再更新”逻辑没问题。如果失败错误信息会告诉你quantity是1还是3直接定位bug。Robolectric的测试速度比模拟器快一个数量级一个测试类几十个用例几十秒跑完比每次改完代码都上车实测高效太多。验证数据完整性的另一个方法在App内部写一个隐藏的“开发者模式”——点击版本号5次弹出DebugActivity里面展示当前数据库所有表的内容。界面用简单的TextView拼查询结果即可。这个功能平时隐藏答辩前演示用比在Logcat里翻日志更直观。参数说明Robolectric对SQLite的支持相当完善但要注意它默认用的SQLite是Android的Java层实现不是原生C层性能比真机低只适合逻辑验证不适合压测。如果遇到“SQLiteException: no such function: last_insert_rowid”之类的报错检查你用的Robolectric版本是否过旧4.x及以上版本都支持。从那以后我每次拿到课设项目源码都会先把数据库层相关的代码用Robolectric跑一遍只测增删改查和事务界面部分一律不测。这样做的好处是上线演示翻车概率最高的数据模块在写代码阶段就能被测试拦截住而不是等你站在答辩台上才发现“购物车数量不对”这种尴尬问题。数据层稳了界面就只是皮怎么改都不慌。希望这套思路对你梳理自己的点餐项目也有帮助。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑