简介:这是一份使用Android Studio开发的订餐应用完整源码,项目采用Material Design设计语言,界面风格贴近安卓5.0之后内置应用,适合安卓初学者、移动开发课程设计或毕业设计作为参考。压缩包共包含690个文件,体积30.69MB,内容以Java源码、XML布局、Gradle构建配置为主,同时提供APK安装包、DEX与CLASS编译产物以及PNG、JPG图片等资源,目录结构完整,可直接导入常见安卓开发环境运行与二次开发。目前已有13187人学习下载。功能模块覆盖用户注册登录、首页美食列表、详情页可折叠标题栏、购物车添加与长按删除、提交订单、个人中心订单管理及通过其他应用分享软件等,交互流程接近真实应用。对于希望系统学习安卓项目分层、界面设计与业务逻辑实现的读者,这套源码提供了完整可运行的实战样例;从编译配置到资源文件一应俱全,也可以作为课程设计答辩或上线项目的前期基础。
1. 先搭骨架:订餐系统的模块拆分与数据表设计
最近接触到的很多课程设计、期末项目里,“Android Studio实现订餐系统”出现频率极高。这个题目看起来简单,真动手做了才知道,问题往往不是不会写界面,而是需求边界没划清楚,导致代码越写越乱,最后数据库表都不够用。
我的建议是:动手之前先把模块拆干净。一个单机版(本地数据库版)订餐系统,核心只需要四个模块——用户模块、菜单模块、购物车模块、订单模块。用户模块负责登录注册和登录态存储;菜单模块负责展示菜品分类与列表;购物车模块处理加菜、减菜和数量同步;订单模块负责把购物车内容固化成订单记录并展示历史订单。至于商家后台、支付接口、实时配送状态这些,除非题目明确要求,否则第一版统统不要碰,先把主链路跑通,后面有余力再扩展。
模块定好之后,数据库设计是整件事的定盘星。我第一次做这个项目时就是吃了没建明细表的亏:直接在订单表里存了一个拼接的菜品名字符串,后面想统计"哪个菜卖得多"完全没法查。正确的做法是至少建五张表:用户表、菜品表、购物车表、订单表、订单明细表。订单明细表特别关键,它的作用是把一次性订单拆成多行菜品记录,这样既能对账,又能做销量统计。
我建议的字段结构大致是这样:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| user | userId, username, password, phone | 用户名要加唯一约束 |
| food | foodId, name, price, category, imageName | 图片用资源名或本地路径,别直接存Bitmap |
| cart | cartId, userId, foodId, quantity | 按用户隔离购物车数据 |
| order | orderId, userId, totalPrice, orderTime, status | status用0/1/2表示待处理/已确认/已完成 |
| order_detail | detailId, orderId, foodId, quantity, price | price存下单时的价格快照 |
这里解释一下为什么order_detail里要再存一份price:菜品价格是会变的,如果只关联foodId,等你回头看三个月前的订单,价格早就对不上了。把下单那一刻的价格快照存下来,订单金额才具备追溯意义。这个设计是业务上常见的"快照"思路,技术上没什么难度,但不做后面一定后悔。
建表语句我用的是Room框架的Entity写法,Room是Google在SQLite之上的官方ORM推荐库,省去了手写SQLiteOpenHelper的一大堆样板代码。对订餐系统这种中小体量项目来说,Room的编译期校验、LiveData支持和数据库版本升级机制都够用,不需要上GreenDAO或者其他重框架。
2. 工程配置:Android Studio依赖准备与ViewBinding开启
环境这块,很多人卡在第一步,所以我把要点单独拎出来说。Android Studio安装本身不复杂,下载Android Studio稳定版后一路默认下一步即可。装完之后记得在SDK Manager里勾选所需的Android SDK版本,国内网络环境下建议启用系统自带的代理设置,不然SDK组件经常下载失败。模拟器方面,不要一上来就建大屏高分辨率的虚拟设备,比如Pixel 6 Pro这类,会很卡。选一个中低分辨率、API 30左右的镜像,例如Pixel 2的AVD配置,跑订餐系统绰绰有余。
新建项目时语言建议选Kotlin,最低SDK版本设为API 24或API 26就行。API 24能覆盖绝大多数模拟器和真机,太低反而要处理运行时权限和系统适配的兼容分支,没必要。
依赖方面,按照我常用的版本组合,在build.gradle.kts模块级文件里加上:
implementation("androidx.recyclerview:recyclerview:1.3.2") implementation("androidx.room:room-runtime:2.6.1") kapt("androidx.room:room-compiler:2.6.1") implementation("com.github.bumptech.glide:glide:4.16.0")如果项目用Java而非Kotlin,Room的注解处理器要换成annotationProcessor,这点别搞混。Glide是图片加载库,订餐系统里的菜品图直接放在drawable或mipmap目录,用Glide一行代码就能异步加载并自动做内存缓存,比手写BitmapFactory处理OOM问题省心得多。
ViewBinding我强烈建议开启。在android块中加入:
buildFeatures { viewBinding = true }开完之后,每个布局文件会自动生成对应的Binding类,Activity和Fragment里就不用写findViewById了。订餐系统里页面多、控件多,少写几十行样板代码不说,还能避免空引用崩溃。很多教程还在用老式findViewById,能跑,但新项目真没必要。
3. 菜单列表页:从数据加载到RecyclerView适配器实现
菜单列表是整个App最先见人的界面,也是RecyclerView用得最重的页面。RecyclerView本身不难,但有几个点处理不好列表就会显得很业余:一是item布局的复用优化,二是数据更新时的局部刷新,三是列表项里按钮事件的回调设计。
先看item布局。我的item_food.xml里放了三样东西:左侧一张菜品图,中间两行文本(菜名和价格),右侧一个"加入购物车"的Button。注意价格显示建议保留两位小数,String.format("%.2f", price),不然菜品价格是整数时只显示一个"25",观感很不统一。
然后是Adapter。这里有一个关键决策:Adapter持有的数据源究竟放Food实体,还是放一个带购物车数量的"UI模型"?我第一次实现时只放了Food,结果界面上要显示"当前已加入购物车3份"就傻了,又加了一张Map来映射菜品ID和数量,绕了一圈。正确做法是定义一个MenuFood类:
data class MenuFood( val food: Food, var cartCount: Int = 0 )Adapter的类型定为List<MenuFood>,这样每次刷新时既能显示菜品信息,又能叠加购物车数量。右侧Button的点击回调用一个接口传到Activity层处理:
interface OnAddCartListener { fun onAddClick(foodId: Long, position: Int) }数据刷新优先用DiffUtil。订餐列表经常出现"用户取消搜索关键词后要恢复全量列表""购物车数量变化后仅更新对应item"这类局部更新需求,notifyDataSetChanged()会闪屏且浪费资源。DiffUtil能自动比对新旧列表,只刷新变化项。代码不复杂,实现四个方法即可:
class FoodDiff : DiffUtil.ItemCallback<MenuFood>() { override fun areItemsTheSame(oldItem: MenuFood, newItem: MenuFood) = oldItem.food.foodId == newItem.food.foodId override fun areContentsTheSame(oldItem: MenuFood, newItem: MenuFood) = oldItem == newItem }然后调用adapter.submitList(newList),剩下的交给DiffUtil处理。
菜单数据从哪来?Room里写一个FoodDao:
@Dao interface FoodDao { @Query("SELECT * FROM food ORDER BY category, foodId") fun getAllFood(): Flow<List<Food>> }返回Flow<List<Food>>而不是List<Food>,意味着数据库里菜品数据一旦变化,菜单列表会自动刷新。比如后面做了"管理员新增菜品"功能,前台界面根本不用手动调用刷新方法,数据流自动推送,这是Room和LiveData/Flow配合最爽的地方。Activity里用lifecycleScope.launch收集Flow,再映射成MenuFood列表提交给Adapter。
4. 购物车联动与订单生成:数量同步和状态流转的细节
这节的坑最多,我踩过的几个必须写出来。
第一个坑是"菜单页的加入购物车按钮"和"购物车页的数量加减"不在同一个页面,如何保证数量同步。我的方案是:所有购物车操作都走一个CartRepository单例,内部用Room的CartDao做数据读写,操作完通过LiveData把最新购物车数据发给所有观察者。菜单页只做一件事:调用cartDao.addOrUpdate(userId, foodId, quantity)。购物车页面观察同一个LiveData,数据一变界面就刷新。这样两个页面解耦,但数据永远一致。
addOrUpdate这个SQL值得单独一说,因为多数人第一次都会写成先查再插,分两步也就算了,还有并发风险。Room里可以直接写成:
INSERT INTO cart(userId, foodId, quantity) VALUES(:userId, :foodId, 1) ON CONFLICT(userId, foodId) DO UPDATE SET quantity = quantity + 1只要在cart表中给(userId, foodId)建联合唯一索引,这条SQL就能在点击瞬间幂等地完成"没有则插入,有则加一",不需要查一次数据库,效率也高。注意ON CONFLICT语法要SQLite 3.24以上才支持,Room内部自带的SQLite版本满足条件,直接放心用。
购物车列表的item布局,右侧是"减号-数量-加号"。减号按钮在数量为1时要置灰或者点击后删除该条记录,我建议直接删除记录,语义更清晰。数量和总价的计算不需要存数据库,购物车页面拿到列表后遍历累加即可,订单表里存的是最终确认的总价,购物车表本身只管过程数据,不存冗余金额。
第二个坑是订单生成。很多新人直接把Cart表里当前用户的所有菜品遍历插入order_detail,然后清空cart,逻辑听起来没问题,但漏了一点:购物车表里记录的"当前数量"在做遍历时应该以界面展示的最终数据为准,一旦用户操作过快,列表项的quantity和数据库不同步,订单金额就算错了。稳妥做法是:下单按钮点击后,重新从CartDao按userId查一次最新数据,基于这份数据生成订单,而不是依赖界面上的Adapter数据。这个习惯很重要——界面数据只能用来展示,业务计算要走数据库最新值。
订单生成建议放在一个OrderRepository里,用RoomDatabase.withTransaction包裹,确保"插入订单表、插入明细表、清空购物车"三步要么全部成功,要么全部回滚。代码结构大致是:
roomDb.withTransaction { orderDao.insertOrder(order) orderDetailDao.insertAll(details) cartDao.clearByUser(userId) }购物车里一份菜都没加就点下单,Toast提示"购物车为空",按钮在购物车为空时直接置灰更合理。还有一单只允许一个用户操作,用户Id从登录态中拿,登录态我用SharedPreferences存一个currentUserId,简单够用。
第三个坑是“返回菜单页继续加菜”。用户从菜单页去购物车页下单,下单成功后返回,会发现菜单页的cartCount还停留在旧值。原因很简单,菜单页Adapter持有的MenuFood.cartCount是内存里的副本,并没有监听购物车变化。解决方法是:菜单页也订阅CartRepository的LiveData,拿到购物车里的Map<foodId, quantity>后,遍历当前列表更新cartCount,再submitList一份新列表给DiffUtil。这样无论用户从哪个页面返回,菜单上的"×N"角标永远是对的。
5. 历史订单与状态展示:列表卡片的设计取舍
订单界面比较容易被忽略,因为功能上就是显示几条记录,没多少复杂度。但做得糙和做得靠谱,差别体现在两个细节上。
第一个细节是订单卡片的信息层次。一单记录里至少有订单号、下单时间、总金额、状态、明细摘要五类信息。你不能把它们平铺成五行文字,用户看不过来。我的做法是:卡片标题行放"订单号"和"状态标签",副行放"下单时间",底部用"共3件商品,合计¥76.50"收尾。状态标签用不同颜色区分——待处理灰色、已确认蓝色、已完成绿色。这种信息架构不需要复杂布局,一个LinearLayout嵌几个TextView就能实现,但体验差距很明显。
第二个细节是订单明细是否要展开。第一版我只显示了"共3件商品",用户根本记不清自己点了什么。后来我加了一个可展开区域:默认折叠,点击订单卡片展开,显示明细列表。实现方式不需要嵌套RecyclerView,直接在卡片ViewGroup里动态添加行TextView即可,数据量小(单订单最多几十行),动态添加足够流畅,还能避免嵌套滚动冲突。每行格式是"菜品名 × 数量",金额对齐到右侧。
订单列表的数据加载和菜单页一样,走Flow<List<Order>>监听数据库变化。状态流转这块,如果是带"用户确认收货"功能的版本,就在订单卡片上加一个"确认收货"按钮,点击后更新status字段。如果是单纯展示型的毕业设计,状态字段可以在本地模拟:下单时设为0,写一个延时逻辑或者手动触发设为1,也可以让老师在数据库里手动改。别在这个功能上过度设计,除非题目明确要求。
6. 调试时期最容易踩的坑:模拟器、图片和数据库版本
最后分享几个我实际调试中反复踩的坑,每一个都是真实花费过时间的。
模拟器启动慢、反应卡,这是订餐系�统开发时最高频的问题。我的建议是AVD配置里把分辨率降到720x1280,不要开Google Play镜像(带Play服务的镜像更重),用普通的AOSP镜像即可。开发阶段如果只需要测试不涉及地图推送,完全够用。另外模拟器默认的图形加速在某些电脑上会崩,把Graphics设为Software虽然画质降了,但稳定多了。跑起来之后如果列表滚动掉帧,多半不是模拟器问题,而是RecyclerView item里用嵌套布局太多,尽量控制布局层级在三层以内。
第二个坑是Glide加载本地资源图时,图片不显示但也不报错。这个问题一般出在URI格式上。如果菜品图放在drawable目录,正确用法是:
Glide.with(context) .load(resources.getIdentifier(imageName, "drawable", packageName)) .into(ivFoodImage)不要把imageName当成字符串直接传进去,Glide虽然能处理R.drawable.xxx的int形式,但传字符串时它是按URL或文件路径解析的,本地资源名不会自动匹配。类似的坑还有真机调试时图片比例不统一导致拉伸变形,用centerCrop配合固定宽高能解决。
第三个坑是中文乱码。新版Android Studio默认UTF-8编码基本没问题,但有些环境下项目的gradle.properties没设置:
org.gradle.jvmargs=-Dfile.encoding=UTF-8一旦多人协作或者换电脑,编译时注释和字符串里的中文就可能变成乱码。这个事属于"平时不觉得、出问题想骂人"的类型,提前在配置里写死最保险。
第四个坑是数据库版本升级。Room默认如果改了Entity结构,但version没从1升到2,App运行起来会直接抛IllegalStateException提示你需要迁移。开发阶段最简单粗暴的方法是卸载重装,但如果是已经交作业或者演示版,数据就没了。规范做法是写Migration对象并在addMigrations里注册。初学阶段至少做到:改表必升version,然后写个空迁移或手动create重建,避免运行期崩溃。
最后一个建议,给项目加一个模拟数据预填充功能。在DatabaseCallback的onCreate里插入十几条菜品数据,省去每次运行都要手动到数据库里塞数据的时间。这个操作只需要写一次,后面所有页面测试都不用愁没数据。
关于订餐系统,我差不多就分享这些。这个项目看着基础,但把模块拆分、数据同步、列表刷新这些基本功练扎实了,后面去做电商类、内容类的Android项目都能直接迁移。如果你正在写这个项目,不妨先把我说的数据表结构和购物车同步方案搭好,再开始写界面。
本文还有配套的精品资源,点击获取