
简介这是一份使用Android Studio开发的订餐应用完整源码项目采用Material Design设计语言界面风格贴近安卓5.0之后内置应用适合安卓初学者、移动开发课程设计或毕业设计作为参考。压缩包共包含690个文件体积30.69MB内容以Java源码、XML布局、Gradle构建配置为主同时提供APK安装包、DEX与CLASS编译产物以及PNG、JPG图片等资源目录结构完整可直接导入常见安卓开发环境运行与二次开发。目前已有13187人学习下载。功能模块覆盖用户注册登录、首页美食列表、详情页可折叠标题栏、购物车添加与长按删除、提交订单、个人中心订单管理及通过其他应用分享软件等交互流程接近真实应用。对于希望系统学习安卓项目分层、界面设计与业务逻辑实现的读者这套源码提供了完整可运行的实战样例从编译配置到资源文件一应俱全也可以作为课程设计答辩或上线项目的前期基础。1. 先搭骨架订餐系统的模块拆分与数据表设计最近接触到的很多课程设计、期末项目里“Android Studio实现订餐系统”出现频率极高。这个题目看起来简单真动手做了才知道问题往往不是不会写界面而是需求边界没划清楚导致代码越写越乱最后数据库表都不够用。我的建议是动手之前先把模块拆干净。一个单机版本地数据库版订餐系统核心只需要四个模块——用户模块、菜单模块、购物车模块、订单模块。用户模块负责登录注册和登录态存储菜单模块负责展示菜品分类与列表购物车模块处理加菜、减菜和数量同步订单模块负责把购物车内容固化成订单记录并展示历史订单。至于商家后台、支付接口、实时配送状态这些除非题目明确要求否则第一版统统不要碰先把主链路跑通后面有余力再扩展。模块定好之后数据库设计是整件事的定盘星。我第一次做这个项目时就是吃了没建明细表的亏直接在订单表里存了一个拼接的菜品名字符串后面想统计哪个菜卖得多完全没法查。正确的做法是至少建五张表用户表、菜品表、购物车表、订单表、订单明细表。订单明细表特别关键它的作用是把一次性订单拆成多行菜品记录这样既能对账又能做销量统计。我建议的字段结构大致是这样表名关键字段说明useruserId, username, password, phone用户名要加唯一约束foodfoodId, name, price, category, imageName图片用资源名或本地路径别直接存BitmapcartcartId, userId, foodId, quantity按用户隔离购物车数据orderorderId, userId, totalPrice, orderTime, statusstatus用0/1/2表示待处理/已确认/已完成order_detaildetailId, orderId, foodId, quantity, priceprice存下单时的价格快照这里解释一下为什么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而非KotlinRoom的注解处理器要换成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的类型定为ListMenuFood这样每次刷新时既能显示菜品信息又能叠加购物车数量。右侧Button的点击回调用一个接口传到Activity层处理interface OnAddCartListener { fun onAddClick(foodId: Long, position: Int) }数据刷新优先用DiffUtil。订餐列表经常出现用户取消搜索关键词后要恢复全量列表购物车数量变化后仅更新对应item这类局部更新需求notifyDataSetChanged()会闪屏且浪费资源。DiffUtil能自动比对新旧列表只刷新变化项。代码不复杂实现四个方法即可class FoodDiff : DiffUtil.ItemCallbackMenuFood() { 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里写一个FoodDaoDao interface FoodDao { Query(SELECT * FROM food ORDER BY category, foodId) fun getAllFood(): FlowListFood }返回FlowListFood而不是ListFood意味着数据库里菜品数据一旦变化菜单列表会自动刷新。比如后面做了管理员新增菜品功能前台界面根本不用手动调用刷新方法数据流自动推送这是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拿到购物车里的MapfoodId, quantity后遍历当前列表更新cartCount再submitList一份新列表给DiffUtil。这样无论用户从哪个页面返回菜单上的×N角标永远是对的。5. 历史订单与状态展示列表卡片的设计取舍订单界面比较容易被忽略因为功能上就是显示几条记录没多少复杂度。但做得糙和做得靠谱差别体现在两个细节上。第一个细节是订单卡片的信息层次。一单记录里至少有订单号、下单时间、总金额、状态、明细摘要五类信息。你不能把它们平铺成五行文字用户看不过来。我的做法是卡片标题行放订单号和状态标签副行放下单时间底部用共3件商品合计¥76.50收尾。状态标签用不同颜色区分——待处理灰色、已确认蓝色、已完成绿色。这种信息架构不需要复杂布局一个LinearLayout嵌几个TextView就能实现但体验差距很明显。第二个细节是订单明细是否要展开。第一版我只显示了共3件商品用户根本记不清自己点了什么。后来我加了一个可展开区域默认折叠点击订单卡片展开显示明细列表。实现方式不需要嵌套RecyclerView直接在卡片ViewGroup里动态添加行TextView即可数据量小单订单最多几十行动态添加足够流畅还能避免嵌套滚动冲突。每行格式是菜品名 × 数量金额对齐到右侧。订单列表的数据加载和菜单页一样走FlowListOrder监听数据库变化。状态流转这块如果是带用户确认收货功能的版本就在订单卡片上加一个确认收货按钮点击后更新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.encodingUTF-8一旦多人协作或者换电脑编译时注释和字符串里的中文就可能变成乱码。这个事属于平时不觉得、出问题想骂人的类型提前在配置里写死最保险。第四个坑是数据库版本升级。Room默认如果改了Entity结构但version没从1升到2App运行起来会直接抛IllegalStateException提示你需要迁移。开发阶段最简单粗暴的方法是卸载重装但如果是已经交作业或者演示版数据就没了。规范做法是写Migration对象并在addMigrations里注册。初学阶段至少做到改表必升version然后写个空迁移或手动create重建避免运行期崩溃。最后一个建议给项目加一个模拟数据预填充功能。在DatabaseCallback的onCreate里插入十几条菜品数据省去每次运行都要手动到数据库里塞数据的时间。这个操作只需要写一次后面所有页面测试都不用愁没数据。关于订餐系统我差不多就分享这些。这个项目看着基础但把模块拆分、数据同步、列表刷新这些基本功练扎实了后面去做电商类、内容类的Android项目都能直接迁移。如果你正在写这个项目不妨先把我说的数据表结构和购物车同步方案搭好再开始写界面。本文还有配套的精品资源点击获取