ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

安卓单机点餐系统完整工程:Java+SQLite实现与踩坑指南

安卓单机点餐系统完整工程:Java+SQLite实现与踩坑指南

简介:这是一份面向安卓初学者的单机点餐系统期末作业资源包,基于Eclipse与SQLite开发,采用单机运行模式,绕开联网接口,重点演示登录、点餐、订单查看等基础业务流程,适合课程设计、毕业设计或自学Android开发时参考。包体共176个文件,约33.53MB,其中包含可安装的apk、java源码、xml界面布局、图片资源及数据库文件,结构清晰便于对照学习和二次修改。压缩包内附有数据库说明文档,账号ZYS/密码123,下载后可直接运行体验。目前已有3133人浏览学习,整体难度较低,功能简洁,非常适合需要快速完成期末作业或快速上手Android项目开发的同学。通过完整源码与运行实例,可帮助理解SQLite存储、Activity界面跳转、ListView列表展示等基础知识。

1. 安卓简易单机点餐系统:一个期末作业被我拆成了完整工程

提到安卓期末作业,十个有八个是点餐系统。可如果只是把一堆控件堆在界面上,数据一关 App 就没了,那这台点餐机顶多算个模型。这个「安卓简易单机点餐系统」的完整工程,走的是原生 Java + SQLite 路线:服务员在界面上选桌号、点菜,下单时把购物车写进本地数据库,结算完还能翻历史订单。它在课程设计的范围内跑通了一个餐厅点餐的最小闭环。适合正在赶期末作业、想搞懂数据库增删改查的安卓新手,也适合想抄一套快速改造成自己毕设的开发者。

2. 技术选型与整体架构:原生 Java + SQLite,单机也能跑通点餐流程

单机点餐系统最大的特点就是没有服务器,所有数据都落在手机本地。这一章先把技术选型和工程结构讲清楚,让你知道这个项目站在什么基础上,后面看代码才不会晕。

2.1 为什么选 Java 和 SQLite,而不是上网络框架

这个系统定位是单机版,菜品、订单、明细都存储在一部手机上。选 SQLite 的理由很直接:它是安卓系统内置的关系型数据库,不需要装服务端,不用联网,课堂演示不会因为网络环境翻车。用 Java 写界面逻辑,是因为期末作业阶段大家对 Kotlin 未必熟悉,Java 的教程最多、报错最好搜,遇到问题不至于卡死。既然数据全在本地,也就没有了登录鉴权和服务器接口,Activity 之间用 Intent 传购物车数据就够了。

这套工程的依赖只有 androidx 兼容包,没有引入第三方网络框架和图片加载库。菜品图片我都没用网络图,直接放资源 drawable,这样 App 在任何环境启动都不会因为图片 URL 失效而白屏。数据库的增删改查全部自己写 SQL,没有用 ORM 框架,把 SQLiteOpenHelper 的用法暴露出来,正好对应课程设计里「学会操作 SQLite」的考核点。和联网点餐系统相比,这里省掉了 Retrofit、OkHttp、JSON 解析那一套,核心逻辑反而更聚焦。

有一种常见做法是把购物车放在 Application 里作为全局变量,这样 Activity 之间就不用传序列化对象。但期末答辩时老师一定会问「要是 Activity 被系统回收了怎么办」。用 Intent 传参会被回收,存 Application 里也可能因为进程被杀而丢。所以我干脆让购物车只活在 MainActivity 的内存里,跳转时用 Bundle 带上一个 ArrayList ,Food 实现了 Serializable,确保订单页面能独立展示。这也解释了为什么订单明细在下单后要立刻落库,而不是回到上一个页面再从内存里捞。

2.2 工程目录与模块划分

打开工程后,主模块的目录长这样:

restaurant/ ├── app/ │ ├── src/main/ │ │ ├── java/com/example/restaurant/ │ │ │ ├── MainActivity.java # 选桌号 + 菜品列表 + 加入购物车 │ │ │ ├── OrderActivity.java # 购物车确认 + 下单 + 结算 │ │ │ ├── db/ │ │ │ │ ├── DBHelper.java # SQLiteOpenHelper,建库建表 │ │ │ │ ├── FoodDao.java # 菜品查询 │ │ │ │ └── OrderDao.java # 订单插入与查询 │ │ │ └── model/ │ │ │ ├── Food.java │ │ │ ├── Order.java │ │ │ └── OrderDetail.java │ │ ├── res/ │ │ │ ├── layout/ │ │ │ │ ├── activity_main.xml │ │ │ │ ├── activity_order.xml │ │ │ │ └── item_food.xml │ │ │ └── drawable/ # 菜品图片 │ │ └── AndroidManifest.xml │ └── build.gradle

这个目录没有做复杂的 MVVM 分层,因为单机点餐的数据量级小,逻辑全在 Activity 里也完全跑得开。我把数据库访问单独抽成 dao 包,是为了让下一个开发者一进来就知道哪些类是干数据活的。model 包里的 Food、Order、OrderDetail 只是承载字段的普通 JavaBean,对应三张表。文件职责可以看这张表:

文件 / 类职责
DBHelper.java继承 SQLiteOpenHelper,负责建库、建表、升级
FoodDao.java查询菜品列表,按分类筛选
OrderDao.java插入订单主表 + 明细表,查询历史订单
MainActivity.java展示菜品,维护选中的购物车
OrderActivity.java展示购物车,下单并结算

2.3 单机架构下的数据流

当服务员在界面点菜时,数据的流向是这样的:MainActivity 先从 FoodDao 读菜品列表并展示到 ListView;用户点某道菜时,系统把菜名、单价、数量放进一个购物车集合;点「去结算」跳到 OrderActivity 时,Intent 把购物车对象序列化后传过去;OrderActivity 拿到数据后,调用 OrderDao 的 insertOrderWithDetails 插入一条订单,同时插入该订单下的所有明细,并且用事务保证两步都成功。

因为单机不需要考虑并发,SQLite 的读写锁在这个场景下完全够用。服务端点餐系统要考虑的订单丢失、数据同步、断网补偿,在这里可以直接省略。这个架构的好处就是让课程设计的重点落在 SQL 和界面跳转上,对新手友好。如果你之后要改成多人点餐,只要把 FoodDao 和 OrderDao 的数据库实现替换成网络请求,Activity 层的购物车逻辑基本可以原封不动地复用。

3. 数据库设计与核心模块拆解:从建表到订单落库的完整实现

这一章是整个工程的心脏,我把建表、菜品查询、下单写入三块代码都拎出来过一遍。每一段代码后面都会说清楚它做了什么、参数怎么改。

3.1 SQLite 表结构:菜品表、订单表、订单明细表

数据库入口是 DBHelper,它负责创建数据库和升级版本。下面是完整的建表代码:

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 food(" + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT NOT NULL," + "price REAL NOT NULL," + // 单价,单位元 "category TEXT)"); // 分类,如热菜、主食、饮料 db.execSQL("CREATE TABLE orders(" + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + "table_no INTEGER NOT NULL," + // 桌号 "total_price REAL NOT NULL," + "create_time TEXT NOT NULL)"); // 下单时间,存 yyyy-MM-dd HH:mm:ss db.execSQL("CREATE TABLE order_detail(" + "_id INTEGER PRIMARY KEY AUTOINCREMENT," + "order_id INTEGER NOT NULL," + "food_name TEXT NOT NULL," + "unit_price REAL NOT NULL," + "count INTEGER NOT NULL)"); // 插入演示菜品数据,第一次启动就有菜可点 db.execSQL("INSERT INTO food(name, price, category) VALUES('宫保鸡丁', 28.0, '热菜')"); db.execSQL("INSERT INTO food(name, price, category) VALUES('酸辣土豆丝', 12.0, '热菜')"); db.execSQL("INSERT INTO food(name, price, category) VALUES('米饭', 2.0, '主食')"); db.execSQL("INSERT INTO food(name, price, category) VALUES('可乐', 5.0, '饮料')"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } }

DBHelper 是数据库的门面。DB_NAME 定义库文件名,DB_VERSION 是版本号,之后只要改这个数字让手机上旧库调用 onUpgrade。onCreate 在三张表建好后插入演示菜品,这样第一次启动就有数据可以点菜。orders 和 order_detail 之间通过 order_id 建立关联,靠这个字段能反查某个订单点了哪些菜。价格字段这里用了 REAL 存浮点,演示没问题,如果要商用建议改成 INTEGER 存「分」,避免精度问题,这点在避坑章节会细说。

3.2 菜品查询与点餐列表加载

菜品查询被封装在 FoodDao 里,MainActivity 只需要调用一行就能拿到列表。先看 FoodDao:

public class FoodDao { private DBHelper helper; public FoodDao(Context context) { helper = new DBHelper(context); } public List<Food> getAllFoods() { List<Food> list = new ArrayList<>(); SQLiteDatabase db = helper.getReadableDatabase(); Cursor cursor = db.rawQuery("SELECT * FROM food ORDER BY category, _id", null); while (cursor.moveToNext()) { Food food = new Food(); food.setId(cursor.getInt(cursor.getColumnIndexOrThrow("_id"))); food.setName(cursor.getString(cursor.getColumnIndexOrThrow("name"))); food.setPrice(cursor.getDouble(cursor.getColumnIndexOrThrow("price"))); food.setCategory(cursor.getString(cursor.getColumnIndexOrThrow("category"))); list.add(food); } cursor.close(); return list; } }

getAllFoods 用 rawQuery 执行原生 SQL,Cursor 是结果集,moveToNext 每次移动一行。getColumnIndexOrThrow 拿到列索引而不是写死下标,以后改表字段顺序代码也不会炸。注意用完后要 close() 释放 Cursor,否则反复打开 Activity 会造成内存泄漏。getReadableDatabase 和 getWritableDatabase 在这个场景下没差别,查数据用前者语义更清晰。

然后是 MainActivity 里的加载代码:

ListView listView = findViewById(R.id.lv_food); FoodDao dao = new FoodDao(this); List<Food> foods = dao.getAllFoods(); FoodAdapter adapter = new FoodAdapter(this, foods); listView.setAdapter(adapter);

写完 Adapter 后,setAdapter 就会把数据渲染出来。FoodAdapter 的 getView 里 inflate item_food.xml,把菜名和价格填到两个 TextView。这个项目用 ListView 而不是 RecyclerView,一是期末作业没引入复杂依赖,二是 ListView 的 Item 点击事件用 setOnItemClickListener 处理,对新手更友好。如果你想改成 RecyclerView,后面二次开发章节会给你方向。

3.3 下单逻辑:购物车明细写入订单

下单是整个系统最关键的一步,要同时写订单主表和订单明细表。我在 OrderDao 里用事务保证原子性:

public long insertOrderWithDetails(int tableNo, List<Food> cart, double totalPrice) { SQLiteDatabase db = helper.getWritableDatabase(); db.beginTransaction(); try { ContentValues orderValues = new ContentValues(); orderValues.put("table_no", tableNo); orderValues.put("total_price", totalPrice); orderValues.put("create_time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") .format(new Date())); long orderId = db.insert("orders", null, orderValues); for (Food food : cart) { ContentValues detailValues = new ContentValues(); detailValues.put("order_id", orderId); detailValues.put("food_name", food.getName()); detailValues.put("unit_price", food.getPrice()); detailValues.put("count", food.getCount()); db.insert("order_detail", null, detailValues); } db.setTransactionSuccessful(); return orderId; } finally { db.endTransaction(); } }

这个方法用了事务。先插入订单主表拿到自增 orderId,再循环把购物车里的每道菜写入明细表。beginTransaction 开始事务,setTransactionSuccessful 标记成功,endTransaction 在 finally 里确保无论成没成功都结束事务。只要中间任何一条 insert 失败,整单都会回滚,不会出现订单主表有数据、明细丢失的半截订单。参数 tableNo 是服务员选的桌号,cart 是购物车列表,totalPrice 提前算好传入。明细表里存了 food_name 和 unit_price 的快照,这样以后菜品涨价,历史订单仍然显示下单时的价格。

计算总价的逻辑我放在 MainActivity:遍历购物车,对每道菜的 price * count 累加。因为浮点计算会有精度偏差,我在结账入口用 BigDecimal 四舍五入到两位小数,再跳转到 OrderActivity。跳转代码如下:

double total = 0; for (Food f : cart) { total += f.getPrice() * f.getCount(); } BigDecimal bd = new BigDecimal(total).setScale(2, RoundingMode.HALF_UP); total = bd.doubleValue(); Intent intent = new Intent(MainActivity.this, OrderActivity.class); intent.putExtra("table_no", tableNo); intent.putExtra("total_price", total); intent.putExtra("cart", (ArrayList<Food>) cart); startActivity(intent);

putExtra 的第一个参数是键,第二个是值。table_no 是整数,total_price 是 double,cart 是 ArrayList 。因为 Food 实现了 Serializable,整个 ArrayList 可以直接序列化进 Intent,Bundle 的容量也足够放下一个餐厅的菜品数量。OrderActivity 拿到这些数据后展示在购物车界面,等服务员点「确认下单」,就调用刚才的 insertOrderWithDetails,下单成功 Toast 提示并 finish 返回主界面清空购物车。

4. 项目运行与打包:从导入 Android Studio 到安装到手机

代码拆完了,得让它先跑起来。这一章我按自己平时带新人跑项目的顺序写,跟着做基本能一次成功。

4.1 导入项目与 Gradle 配置

解压工程包后,打开 Android Studio 选择 File -> Open,定位到工程根目录,等 Gradle 同步完成。如果电脑上没装对应版本的 SDK,同步会报错,根据提示点安装缺失的 SDK 即可。这个工程用的 Gradle 配置是常见的配置块:

android { compileSdk 33 defaultConfig { applicationId "com.example.restaurant" minSdk 21 targetSdk 33 versionCode 1 versionName "1.0" } }

compileSdk 决定能用哪个版本的 API,minSdk 21 表示 Android 5.0 以上的设备都能跑,targetSdk 33 对应 Android 13 的运行行为。如果你的机器没有装 SDK 33,把 compileSdk 改成你本机已装的版本,同时 targetSdk 保持一致,代码里没有用到高版本专属 API,降级完全没问题。

4.2 模拟器运行与真机调试

推荐先用模拟器跑通逻辑。在 Device Manager 里建一台 Pixel 模拟器,系统镜像选 API 30 左右就行,启动后直接点 Run 按钮,App 会自动安装并打开。第一次启动后应该能看到菜品列表,点几个菜选桌号,结算后去 data/data/com.example.restaurant/databases 下查一下 restaurant.db,确认数据真的落库了。

真机调试也一样简单:手机开启开发者选项里的 USB 调试,插上电脑,adb 能看到设备:

adb devices adb install -r app-debug.apk

第一次插上手机会弹授权提示,记得点允许。如果 adb devices 看不到设备,多半是驱动问题,Windows 下装一下厂商 USB 驱动,Mac 和 Linux 一般免驱。真实设备上跑数据库读写,比模拟器更贴近演示场景。

4.3 生成 APK 与期末演示

如果只是课堂演示,debug 包直接连电脑跑就行。但交作业或发给别人装,需要生成独立 APK。流程是 Build -> Generate Signed Bundle / APK -> 选 APK -> Create new keystore,填上密码和别名,签名后生成 release 包。

生成 release 包时有个小坑:签名文件的密码和别名一定要截图存好,后面二次修改再打包要用同一个签名,否则装不上旧设备。演示前先在模拟器上完整走一遍点餐流程,确认数据正常。老师如果问数据库在哪,用 Android Studio 的 Device File Explorer 找到 databases 目录,导出 restaurant.db,用 SQLite 工具打开给他看表结构和数据,这一手很容易加分。

5. 常见问题与避坑:这几个坑我写期末作业时全踩过

跑通是一回事,从跑通到能演示、能答辩又是另一回事。这一章列了五个真实踩过的坑,每一条都按现象、原因、解决写清楚,遇到问题直接对号入座。

5.1 安装后闪退:数据库升级忘加旧表字段

现象:第一次运行正常,改了表结构后再次安装,启动直接闪退,日志里报 "table orders has no column named remark"。

原因:数据库文件已经存在,但 DB_VERSION 没改,onUpgrade 空实现,新字段根本没有加进去。

解决:每次改动表结构,都要把 DB_VERSION 加一,并在 onUpgrade 里写清楚迁移逻辑:

@Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion < 2) { db.execSQL("ALTER TABLE orders ADD COLUMN remark TEXT"); } }

这样老用户升级时只会执行增量 SQL,而不是把整库重建。开发期图省事可以卸载重装,但演示和交付前一定要把升级路径测一遍。

5.2 ListView 不刷新:Adapter 还是旧数据

现象:下单成功后回到主页面,菜品列表没变化,购物车角标还是原来的数量。

原因:数据库里的数据变了,但 ListView 的 Adapter 持有的 List 还是旧对象,没有通知刷新。

解决:在数据变更后主动调用 adapter.notifyDataSetChanged()。MainActivity 在 onActivityResult 里收到下单成功的标志后,清空购物车集合,重新 setAdapter,保证每次回到主界面都读到最新状态。新手经常忘了这一行,结果界面看起来像「坏掉了」,其实就是缺一个刷新通知。

5.3 总价对不上:double 精度误差

现象:点了一份 28 元的宫保鸡丁和一份 2 元的米饭,总价不是 30,而是 29.999999999999996。

原因:二进制浮点精度问题,double 在计算和存储尾数时会丢失精度。

解决:金额计算统一走 BigDecimal,展示统一走 DecimalFormat。更彻底的做法是数据库价格字段改成 INTEGER 存「分」,订单明细里的 unit_price 也存分,只在界面转换成元。我从那以后只要是跟钱有关的字段,一律按分存储,算完后除以 100,彻底绕开浮点坑。

5.4 模拟器打字崩溃:输入法兼容问题

现象:在模拟器的 EditText 里输入中文菜名,瞬间崩掉,log 里满屏 "Unable to start activity ComponentInfo"。

原因:部分模拟器镜像自带的键盘和中文输入法有兼容问题,输入过程触发资源加载失败。

解决:这个项目本来不需要手动输入,桌号用下拉框选,菜品用 ListView 点选,能把 EditText 整个省掉。如果一定要输入备注,建议在真机上调试,或给模拟器装第三方中文输入法。在答辩现场我吃过这个亏,之后凡是演示用的模拟器,我都提前把输入环节全部改成选择交互。

5.5 真机安装失败:签名冲突

现象:adb install 报 INSTALL_FAILED_UPDATE_INCOMPATIBLE,手机上显示「未安装」。

原因:设备上已经装了一个同包名但签名不同的版本,比如之前用调试签名装过,现在又用另一个 debug 签名装。

解决:先卸载旧的再装:

adb uninstall com.example.restaurant adb install app-debug.apk

如果你接的是别人的项目,还要检查 applicationId 是否冲突,改包名要连带改 AndroidManifest 里所有 activity 的完整路径。这个坑在交作业前的最后一天特别容易遇到,因为那会大家都会拿别人的 APK 互相装。

6. 二次开发:把这套系统改造成能交差的完整功能

基础版本能跑通,但离「优秀作业」和真实餐厅需求还差一点。这一章给你三个改造方向,按优先级来,改完就是一套拿得出手的完整系统。

6.1 加入数量加减与购物车持久化

现在的购物车是点一次加一道菜,一个菜想加三份得点三次,体验很差。我把 FoodAdapter 改成维护一个 Map<餐品Id, 数量>,每次点击加减按钮都更新这个 Map,同时刷新 Item 上的数量 TextView。结算时直接遍历 Map 生成购物车列表,统一传给 OrderDao。这样改动只涉及 Adapter 和 MainActivity,数据库层完全不用动。

6.2 用 RecyclerView 替代 ListView

答辩时老师可能会问「为什么还用 ListView」。你可以提前把菜品列表换成 RecyclerView,FoodAdapter 改成 RecyclerView.Adapter 结构,ViewHolder 用标准的 itemView 模式。RecyclerView 对点击事件的处理更灵活,后面要加长按删除、滑动修改数量都很顺手。用 GridLayoutManager 还能把菜品变成九宫格样式,视觉上更像个点餐平板。

6.3 订单历史查询与状态管理

加一个订单历史页面,从 orders 表按 create_time 倒序查出来,点击某条订单跳到详情页,用 order_id 去 order_detail 表查明细。如果想让流程更完整,给 orders 表加一个 status 字段,区分「已下单」和「已完成」,在 OrderActivity 里加一个状态更新按钮,就成了一个带订单流转的点餐闭环。这个改造有一个隐藏考点:让老师看到你已经会处理表间关联和状态枚举,比堆一堆 UI 特效有用得多。

从那以后我每次拿到数据库相关的安卓项目,都强制先画一遍字段关系表,再动手写 DAO,最后才碰 Activity。数据层稳了,上层怎么改都不慌。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进