安卓简易单机点餐系统开发实战:SQLite+RecyclerView全流程解析
2026/9/7 6:03:43 网站建设 项目流程

简介:这是一套基于安卓平台与SQLite数据库开发的简易单机点餐系统,主要面向安卓初学者以及需要完成期末课程设计的在校学生,解决离线环境下餐厅点菜、订单记录等常见需求。整个项目由Eclipse工具构建,实现了登录验证、菜品选择、订单生成与历史查询等核心流程,页面分区清楚,配色自然,交互符合一般点餐场景的使用习惯。压缩包共包含一百七十六个文件,其中以Java源码、XML布局和PNG界面图片为主,同时提供SQLite数据库文件、可安装APK以及少量库依赖,整体大小为三十三点五三兆字节,目录组织清晰,方便按功能模块阅读和二次开发。包内附带数据库说明文档,并内置测试账号,无需额外配置即可直接运行体验,能够帮助初学者理解安卓本地存储、列表展示和基础交互逻辑。目前已有三千一百三十二人学习下载,作为期末作业或入门练手项目具有不错的参考价值。 期末作业发下来,打开题目一看“安卓简易单机点餐系统”,很多同学第一反应是去百度找源码,找到一份改改名字交上去,结果答辩时被老师问两句就露馅了。我在这个题目上折腾了两个星期,踩了不少坑,也把整个系统重构了一遍,最后答辩拿了不错的分数。这篇文章就把我做这个作业的全过程、核心代码、以及答辩时老师真正关心的点整理出来,给正在赶这个作业的同学一个可以“抄作业”但又不至于一眼假的路子。

实话说,这个题目看着简单,但“简易”两个字给了你很大的发挥空间,也给了老师很大的扣分空间。做得太少显得敷衍,做得太深又容易把自己绕进去。这个度怎么拿捏,就是这篇文章要解决的核心问题。

1. 需求拆解:明白期末作业里的“潜台词”

1.1 老师嘴上说的和心里想的

题目写的是“安卓简易单机点餐系统”,关键词拆开看:安卓、单机、点餐。三个词各有各的讲究,“安卓”要求你用安卓原生技术栈,不是网页套壳;“单机”意味着数据不需要远程服务器,SQLite或者文件存储都行;“点餐”则是业务逻辑的核心,菜单展示、添加购物车、下单结算,这三板斧必须有。

但老师不会明说的隐藏要求才是拉开差距的地方。一般来说,一个完整的期末作业要有“数据层”,也就是数据库建表与操作;要有“用户层”,最简单的登录注册也得有;要有“业务层”,菜单列表、购物车、订单生成缺一不可。表现形式上,至少得有两个以上界面,界面之间能跳转,数据能持久化。

如果你只做一个页面把菜列出来然后算个总价,那叫“网页版的填空题”,不是安卓应用。

1.2 技术选型对比,别在起跑线就输了

方案上手难度老师观感期末作业适配度
Java + SQLite + RecyclerView中等正统安卓开发路线最推荐
Kotlin + Room偏高技术新、加分如果课程教过Kotlin可以选
ListView + 文件存储技术太老勉强过关,但容易被追问
WebView套H5明显跑偏不推荐,毫无安卓含量

我选的是Java + SQLite + RecyclerView的组合。Java是课程教的,大家最熟;SQLite是安卓内置数据库,不需要引入任何外部依赖,断网也能跑,完美契合“单机”要求;RecyclerView是现在列表开发的主流组件,用ListView虽然也行,但答辩时老师看到RecyclerView至少觉得你课后用了功。

1.3 功能清单:什么该做,什么不该做

我最后定下来的功能清单是这样的:

  • 登录注册:用户名唯一校验,密码非明文存储(至少做个简单的加密,哪怕是自己写的异或算法)
  • 菜品列表:图片、菜名、价格、分类,用RecyclerView展示
  • 购物车:数量加减、实时算总价
  • 订单提交:生成订单号、保存订单到数据库、清空购物车
  • 历史订单:查看过去下的单,包含订单状态和明细

像服务端同步、优惠券、菜品搜索这些就都没做。“简易”两个字意味着你要控制边界,做多了自己维护不过来,答辩反而被问住。

2. 数据层设计:SQLite建表与预置数据

2.1 四张表打天下

单机应用的SQLite设计,说白了就是在本地模拟一个缩水版的服务端数据库。我设计了四张表:用户表、菜品表、订单表、订单明细表。

-- 用户表 CREATE TABLE user ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password TEXT NOT NULL ); -- 菜品表 CREATE TABLE menu ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL NOT NULL, image TEXT, category TEXT, description TEXT ); -- 订单表 CREATE TABLE orders ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_no TEXT NOT NULL, user_id INTEGER, total_price REAL NOT NULL, create_time TEXT NOT NULL, FOREIGN KEY (user_id) REFERENCES user(id) ); -- 订单明细表 CREATE TABLE order_item ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, menu_id INTEGER NOT NULL, menu_name TEXT NOT NULL, price REAL NOT NULL, count INTEGER NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id) );

注意订单明细表里冗余存了一份menu_nameprice,这是有意为之的。因为菜品将来可能改价或者改名,订单属于历史数据,必须保留下单那一刻的快照。这个细节我答辩时主动提了一句,老师点了点头。

2.2 数据库帮助类的正确写法

SQLiteOpenHelper是安卓封装好的数据库助手类,核心就是onCreateonUpgrade两个回调。很多同学的代码里只写了onCreateonUpgrade直接空着,这其实是个隐患。假如你后期改了表结构,App升级时数据库没同步,直接崩。

public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "ordering.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 user (...)"); db.execSQL("CREATE TABLE IF NOT EXISTS menu (...)"); db.execSQL("CREATE TABLE IF NOT EXISTS orders (...)"); db.execSQL("CREATE TABLE IF NOT EXISTS order_item (...)"); // 预置菜单数据 initMenuData(db); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 开发期最简单的处理方式:删表重建 db.execSQL("DROP TABLE IF EXISTS user"); db.execSQL("DROP TABLE IF EXISTS menu"); db.execSQL("DROP TABLE IF EXISTS orders"); db.execSQL("DROP TABLE IF EXISTS order_item"); onCreate(db); } }

2.3 菜品数据从哪里来

单机应用没有网络接口,菜品数据有两个来源:一是首次创建数据库时插入,二是直接打包一个已经写好的DB文件放到assets目录。我推荐后者,因为图片资源和菜品数据可以提前整理好,但对学生来说操作复杂度高一些。

我用的是第一种方案,在onCreate里调用一个initMenuData(db)方法,把菜品写死在代码里。菜品图片放在drawable目录,数据库里存图片资源名,而不是存Bitmap二进制。读取时通过getResources().getIdentifier()拿到资源ID,再加载。这个方案的好处是数据库文件小,图片交系统管理,不会出现大字段导致的内存问题。

3. 菜单列表与购物车:核心交互的实现思路

3.1 界面结构:一页还是两页

我考虑过两种方案:一种是一个Activity里用Fragment切换不同页面;另一种是底部导航加多Activity。最后选了前者,底部三个Tab:菜单、订单、我的,用Fragment实现。这样做的好处是底部导航常驻,用户在各个页面间切换体验流畅,而且代码组织上更接近现在主流App的结构。

Fragment事务切换的代码不复杂,但有一个坑要提醒:切换时用add而不是replacereplace会销毁再创建Fragment,状态丢失,比如菜单列表滑到一半的位置、购物车里的数据可能就没了。add配合hideshow,Fragment的实例一直活着,数据自然就保住了。这个方法在答辩时也是个加分回答点。

3.2 RecyclerView适配器与购物车的联动

RecyclerView的Adapter写起来不难,难点在于“数量变化怎么通知界面刷新”。我的做法是用一个HashMap<Integer, Integer>当作购物车容器,key是菜品ID,value是购买数量。

public class MenuAdapter extends RecyclerView.Adapter<MenuAdapter.ViewHolder> { private List<Menu> menuList; private Map<Integer, Integer> cartMap; // menuId -> count private OnCartChangeListener listener; @Override public void onBindViewHolder(@NonNull ViewHolder holder, int position) { Menu menu = menuList.get(position); holder.tvName.setText(menu.getName()); holder.tvPrice.setText("¥" + menu.getPrice()); holder.tvCount.setText(String.valueOf(cartMap.getOrDefault(menu.getId(), 0))); holder.btnAdd.setOnClickListener(v -> { cartMap.put(menu.getId(), cartMap.getOrDefault(menu.getId(), 0) + 1); notifyItemChanged(position); if (listener != null) listener.onCartChanged(getTotalCount(), getTotalPrice()); }); holder.btnMinus.setOnClickListener(v -> { int count = cartMap.getOrDefault(menu.getId(), 0); if (count <= 0) return; if (count == 1) { cartMap.remove(menu.getId()); } else { cartMap.put(menu.getId(), count - 1); } notifyItemChanged(position); if (listener != null) listener.onCartChanged(getTotalCount(), getTotalPrice()); }); } public interface OnCartChangeListener { void onCartChanged(int totalCount, double totalPrice); } }

这里需要注意notifyItemChanged(position)只刷新当前项,比notifyDataSetChanged()高效,而且不会让列表滚动位置跳动。这个细节我在开发过程中被坑过,一开始用notifyDataSetChanged,每次点按钮列表都会跳回顶部,体验很差,后来改成局部刷新才解决。

3.3 购物车数据存在哪里

购物车数据我放在Fragment里的一个静态Map中(或者直接存在当前Fragment的字段里),因为单机应用不需要服务端保存购物车,用户杀掉App购物车清空也说得过去。不过如果要从购物车跳转到确认订单页面,这个数据要跨界面传递,我推荐三种方式:通过接口回调、通过Application持有、通过Intent传递序列化对象。这个项目我用的是Application里存一份全局购物车Map,简单直接,Activity和Fragment都能拿得到。

4. 订单结算与历史订单:细节才是拿分点

4.1 金额计算别用double

这个是真坑。Java里double做减法会有精度问题,比如0.1 + 0.2的结果是0.30000000000000004。做点餐系统,结算总价算错了,哪怕差一分钱,演示的时候都极其尴尬。

正确姿势是用BigDecimal

private double calculateTotal(Map<Integer, Integer> cartMap, List<Menu> menuList) { BigDecimal total = BigDecimal.ZERO; for (Menu menu : menuList) { int count = cartMap.getOrDefault(menu.getId(), 0); if (count > 0) { BigDecimal price = BigDecimal.valueOf(menu.getPrice()); total = total.add(price.multiply(BigDecimal.valueOf(count))); } } return total.setScale(2, RoundingMode.HALF_UP).doubleValue(); }

数据库里存REAL类型够用了,但界面上显示的格式化用String.format("%.2f", price)或者DecimalFormat,保证显示两位小数。

4.2 生成订单号与事务处理

订单号我用时间戳加随机数生成,简单且能保证不重复。下单的核心操作包括:插入订单主表记录、插入订单明细表多条记录、清空购物车。这三个操作必须放在一个事务里,保证要么全部成功,要么全部失败。不然就会出现订单主表有记录,明细表空着的情况——我在开发时真的遇到过,最后查了半天,发现是明细插入失败但主表已经提交了。

SQLiteDatabase db = dbHelper.getWritableDatabase(); db.beginTransaction(); try { long orderId = db.insert("orders", null, orderValues); for (OrderItem item : itemList) { db.insert("order_item", null, itemValues); } db.setTransactionSuccessful(); } finally { db.endTransaction(); }

事务处理是数据库操作的高频考点,答辩时老师几乎必问“怎么保证数据一致性”,能把事务讲明白,这个项目就已经成功一大半了。

4.3 历史订单页面的两级展示

订单记录我用了一个两级的展示方式:上半部分是订单卡片,显示订单号、下单时间、总金额、状态;点击卡片后弹出明细对话框,展示这个订单包含哪些菜品、各自什么价格、数量多少。

这里有一个小技巧:订单列表用orders表的数据,不关联查询菜品明细,因为明细放在order_item表里,数量多的话联表查询会有性能问题。等用户点击某条订单时才去查对应的明细,用WHERE order_id = ?条件即可。数据量小,单机应用压根不用考虑性能优化,但代码结构上清晰,老师看着舒服。

4.4 空购物车与重复下单的防御

这些边界情况虽然不起眼,但都是期末作业里的“安全网”。我在提交订单的按钮里加了购物车空判断,购物车为空时Toast提示,不让走订单流程。登录页对空输入做了校验,密码错误也有独立提示。这些代码量不大,但能明显提升项目的完整度。很多同学只写了顶层流程,没写防御逻辑,一演示给老师看,空购物车也能下单成功,当场就被扣分了。

5. 真机运行时的坑与界面上容易被忽略的细节

5.1 横竖屏切换数据丢失

模拟器默认竖屏,但老师可能会旋转屏幕检查适配。如果不处理,Activity重新创建,界面上的状态全没了。最简单的方案是在AndroidManifest.xmlactivity标签里加一行:

android:configChanges="orientation|screenSize|keyboardHidden"

然后在Activity里重写onConfigurationChanged方法,什么都不用做。这个方案不是最优雅的,但应付期末作业足够了。更优雅的方案是使用onSaveInstanceState保存数据,但那个要写一堆序列化代码,学生项目没必要,性价比太低。

5.2 图片加载的两种思路

菜品图片我前面提到用drawable资源名存数据库。加载时用getIdentifier方式:

int resId = context.getResources().getIdentifier(imageName, "drawable", context.getPackageName()); if (resId != 0) { holder.ivImage.setImageResource(resId); } else { holder.ivImage.setImageResource(R.drawable.default_food); }

这样比在代码里写一堆if-else或者switch-case要简洁得多。还有一种思路是先把图片放进rawassets目录,读取时转Bitmap。但要注意:Bitmap不经压缩直接显示,内存一高就容易OOM。用Glide加载本地资源也行,但期末作业没必要引第三方库,老师还会问你为什么用Glide。

5.3 图标、应用名、包名这些小门面

期末作业最亏的扣分点,一个是App名字还是默认的app_name,一个是图标还是安卓默认的小机器人。改起来其实很简单:strings.xml里改app_name,图标在mipmap目录替换。我有个同学功能做得不错,就因为这俩没改,被老师说“态度不认真”,分数直接降了一个档。界面配色也不要全用系统的默认白底黑字,自己定一个主色调,按钮圆角、卡片阴影这些用drawable自定义一下,观感立刻不一样。

5.4 模拟器选择与真机调试建议

期末演示一般用模拟器。Android Studio自带的模拟器启动慢、占内存大,如果电脑配置一般,建议用Genymotion或者直接在手机上调试。真机调试时打开“开发人员选项”里的“USB调试”,用数据线连电脑就能直接跑。注意:不同的手机厂商进入开发者模式的方式不一样,一般是连点“版本号”七次。这个环节看似简单,但我见过不少人卡在这里,还有USB驱动装不上的,提前准备好会比较从容。

6. 答辩现场:老师最常问的问题和应对思路

6.1 高频问题Top 5

  • “为什么用SQLite而不用文件存储?”这个问题比较好答。SQLite支持结构化查询、事务、索引,数据操作更规范,而且安卓系统原生支持,不需要额外成本。文件存储适合读写的场景简单、没有复杂查询关系的数据,而点餐系统的用户、菜品、订单之间是有关系的,用数据库更合适。

  • “购物车数据怎么保存?杀进程之后还在吗?”说实话,这个在我的项目里进程杀掉就清空了。可以补一个思路:把购物车存到SQLite或者SharedPreferences,启动时恢复。如果要加,建议存SQLite里的一个cart表,或者更精简的做法是SharedPreferences存JSON。

  • “下单时如果插入订单主表成功了,但明细插入失败了怎么办?”这就把话题引导到你用了事务上,把自己写在4.2节的代码讲一遍,老师会觉得你考虑得很周全。

  • “菜品数据写死在代码里,如果要新增一个菜品怎么操作?”老实说最简单的方式是改initMenuData重新装DB,或者做一个“菜品管理”的界面。期末作业做到新增菜品界面就有点超纲了,你可以说“生产环境下菜品数据应由服务端下发,本题因为是单机系统,所以采用预置数据方案”。

  • “密码安全吗?”如果明文存储,肯定不要撒谎。诚实的回答是“这里采用了简单的加密处理,并非明文存储”,如果你只做了异或,就说“对于课程设计这种场景,加密强度够用了,生产环境会用MD5加盐或BCrypt”。

6.2 演示顺序怎么排

这一步非常关键。很多人一上去就从头演示,结果刚进入菜单列表就卡住了,自己手忙脚乱。我建议按这个顺序来:

  • 先展示App整体界面,说明底部有三个Tab,快速带过菜单页
  • 重点演示点餐流程:加几个菜、减一个、看总价变化、提交订单
  • 切到历史订单页,展示刚下的单出现在列表里
  • 最后演示登录注册逻辑,退出再登录,验证数据的持久性

先走主流程再走分支逻辑,即使中间出了状况,老师已经看到了核心功能,印象分已经拿到手了。

写在最后的几个小建议

做完这个项目最大的感受是:期末作业不是功能越多越好,而是“核心流程完整、细节处理到位、技术选型讲得出理由”。数据用SQLite做了持久化,购物车和订单有完整的状态流转,Adapter封装和Fragment管理用的是当前主流写法,防御性判断和异常处理覆盖了关键路径,这些才是真正让老师给你打高分的点。

再提醒一个事,代码里的命名规范。虽然期末作业不要求像企业级代码那样讲究,但类名用驼峰、变量命名有含义、关键逻辑写注释,这种好习惯会贯穿项目,答辩时翻代码给老师看也会更自信。如果你现在还在赶工,优先保证主流程跑通,然后再回头处理这些细节点,但千万不要觉得无所谓就完全不处理。我见过一群人项目跑得通但代码乱成一团被老师吐槽“这是给自己挖坑”,所以随手收拾一下,不亏的。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询