安卓外卖App源码实战:从Spring Boot接口到OkHttp联调

发布时间:2026/10/7 10:52:37
安卓外卖App源码实战:从Spring Boot接口到OkHttp联调
简介一份基于安卓平台的外卖APP开发与设计资源包采用Java语言实现面向毕业设计、课程设计以及Android应用开发学习者提供从项目搭建到功能落地的完整参考。压缩包约17.19MB内含源码、说明文档与演示视频平台未列出文件总数便于对照学习与效果预览目前已有193人学习。项目知识点覆盖Java面向对象、Android SDK及Android Studio集成环境、ViewModel/LiveData/Repository等架构组件并延伸至后端SSM框架、MySQL数据库设计、Retrofit/OkHttp网络通信与地图API接入。同时涉及Material Design界面规范、多线程异步处理、运行时权限管理以及JUnit/Espresso测试调试帮助开发者系统理解Android客户端与后端服务交互的完整链路。通过源码研读、文档解析和演示视频观摩可高效掌握外卖类App的模块拆分、数据流转和功能实现为同类课题开发提供扎实的实践蓝本。1. 一个安卓外卖APP的zip包为什么值得把它跑起来再看源码你手上如果有一个标题叫“基于安卓的外卖APP开发与设计”的Java项目压缩包里面带着源码、说明文档和演示视频大概率第一反应是“又一个毕设项目”。但我想先说一个反直觉的结论这种项目真正难住新人的地方不在安卓界面的布局而在服务端接口怎么约定、下单的订单状态怎么流转、以及安卓端怎么在真实局域网环境里把数据拉通。一个小外卖App把这三层全部走一遍比单独刷一百集Android教程都管用。这套东西适合三类人拿它做毕业设计的学生、想往安卓开发投简历的初级工程师、以及想快速了解一个Java后端接口是怎么被手机App调用的前端开发者。前提是你别只把它当代码压缩包来解压而是当成一条完整的技术链路来复现MySQL建表、Spring Boot起服务、OkHttp发请求、RecyclerView刷列表。下面按我平时接手这类项目的顺序从服务端一路讲到安卓端再落到联调排错和进阶验证。2. 先看服务端外卖App的Java后端怎么搭才算合理拿到项目先别急着打开Android Studio我一般会先把服务端代码翻一遍。原因很简单App端所有页面都是在等接口返回数据接口字段叫什么、返回什么结构决定了你后面写解析时要不要加班。2.1 选型逻辑Spring Boot MyBatis和更轻方案的区别常见的外卖App毕设项目里服务端跑的是Spring Boot加MyBatis配MySQL数据库。这个组合对安卓开发者的Java基础要求不算高你只要会写Controller、Service、Mapper三层再把常用注解认熟剩下的交给框架。它不是性能最优解但胜在生态成熟出任何问题都能在网络上搜到现成的答案。如果你拿到的版本用的是Servlet加JDBC也别急着嫌弃。Servlet项目启动简单不依赖Maven拉一堆包适合把精力放在“接口设计”本身。判断一个服务端代码写得好不好先看数据库脚本。我见过不少翻车现场是订单表里没有订单状态字段或者菜品价格用的是double。基础表结构至少要有这么几张用户表、菜品表、购物车表、订单表、订单明细表。用SQL看更直接CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(32) NOT NULL, password varchar(64) NOT NULL, avatar varchar(255) DEFAULT NULL, address varchar(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dish ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(64) NOT NULL, price decimal(10,2) NOT NULL, image varchar(255) DEFAULT NULL, category varchar(32) DEFAULT NULL, status tinyint(1) DEFAULT 1, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, total_price decimal(10,2) NOT NULL, status tinyint(2) DEFAULT 0 COMMENT 0待支付 1已支付 2配送中 3已完成 4已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, remark varchar(255) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有两个细节值得记一下。价格字段一定用decimal(10,2)而不是double这是做支付相关项目的基本常识double算金额会出现0.01元的误差后面对账有你哭的。订单状态用tinyint加注释比字符串更省空间安卓端拿到数字后自己映射成文字展示。我们约定状态码0到4和后面要讲的接口返回结构是一套配合的逻辑。2.2 订单接口从Controller到事务提交外卖App最核心的接口是下单。很多人把它做成简单的insert语句吃完就出问题——下单要同时写订单表和订单明细表两步必须在一个事务里。用户下单点一次结果只生成了订单主记录明细没写进去订单列表页打开就是一堆没有菜品的空壳。新手最容易犯的错误恰恰是忘记给Service层加Transactional。我习惯把下单接口的代码写成这样它足够简短但包含了核心校验RestController RequestMapping(/order) public class OrderController { Autowired private OrderService orderService; PostMapping(/create) public Result createOrder(RequestBody OrderRequest request) { if (request.getUserId() null || request.getItems().isEmpty()) { return Result.error(用户ID和菜品列表不能为空); } try { Long orderId orderService.createOrder(request); return Result.success(orderId); } catch (Exception e) { return Result.error(下单失败 e.getMessage()); } } }这段代码注释很少是因为逻辑都压在Service层Controller只做参数校验和结果包装。注意PostMapping搭配RequestBody安卓端传JSON用的是POST而不是GET因为下单请求体里带着菜品列表GET根本装不下。OrderRequest里面至少要有userId和items字段items是每个菜品的id和数量。Service层才是重点。createOrder方法里要干三件事计算总价、插入订单主表拿回自增id、再循环插入明细表。这个循环插入不要用for循环加单条insertMyBatis支持批量插入一次insert list性能好很多。事务失效的经典坑有两个一个是方法被同类内部调用Transactional没生效另一个是异常被try catch吞了事务管理器根本不知道出了错。所以我在事务方法里不catch业务异常直接抛出去让全局异常处理器兜底。2.3 接口约定统一返回体和JSON命名安卓端写网络解析最怕什么怕接口每个方法返回的结构都不一样。有的接口直接返对象有的返回一个字符串出错时有的给code、有的给message客户端每接一个接口就要写一套判断逻辑。这个项目如果是多人协作或者从教程里拷来的很容易看到这种混乱局面。解决办法是全局统一返回体所有接口都返回这个结构public class ResultT { private Integer code; // 0成功非0失败 private String message; // 错误信息成功时为空 private T data; // 真正的业务数据 public static T ResultT success(T data) { ResultT r new Result(); r.code 0; r.message ok; r.data data; return r; } public static T ResultT error(String msg) { ResultT r new Result(); r.code 1; r.message msg; return r; } // getter/setter省略 }这个类的价值是给安卓端吃定心丸。客户端永远只认code字段成功走data分支失败走message提示不用在每个回调里再猜返回值的含义。字段命名上要留意一件事后端习惯用Java小驼峰比如createTime而SQL字段是create_time。网上很多教程建议在MyBatis配置文件里开启mapUnderscoreToCamelCase这样属性映射不用写一堆resultMap这个开关默认是开启的但有些老版本项目模板会关掉导致查出来的createTime永远是null。翻车的时候先检查这个配置。日期格式化也要在这一层约定好。默认情况下LocalDateTime序列化出来是一串带T的字符串安卓端直接显示很难看。我一般会在配置里指定格式让接口直接返回“2025-06-18 12:30:00”这种字符串客户端拿过来直接用省得在手机端再写一遍日期转换代码。3. 安卓端的实现顺序先有网络层再谈页面很多从零开始学安卓的人有个习惯先从布局文件开始拖控件。但对于有服务端接口的项目这是本末倒置。页面是死的数据是活的只要接口还没定界面做得再漂亮也是空壳。正确做法是把网络层放在最前面先让App能把服务端的数据拉下来再考虑RecyclerView怎么渲染。3.1 OkHttp统一请求封装与异步解析现在绝大多数Java写的安卓外卖项目用的网络库是OkHttp配Gson。选择它不是因为功能最全而是因为生态稳定、资料多、遇到问题能搜到答案。封装网络层我习惯用一个单例把baseUrl、超时时间、公共header集中管理。核心代码如下public class HttpManager { private static final String BASE_URL http://你的服务器IP:8080/; private static OkHttpClient client; private static Gson gson new Gson(); static { client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .build(); } public static void post(String url, MapString, Object params, Callback callback) { String json gson.toJson(params); RequestBody body RequestBody.create( MediaType.parse(application/json; charsetutf-8), json); Request request new Request.Builder() .url(BASE_URL url) .post(body) .build(); client.newCall(request).enqueue(callback); } }注意POST请求的body要手动指定ContentType为application/json配合服务端的RequestBody使用。如果ContentType是text/plainSpring Boot默认会报415错误这是联调时最高频的一个翻车点。超时时间我设的是10秒。外卖App的列表和下单接口都不应该超过这个时间万一服务端没启动10秒内客户端能收到连接失败的回调给用户一个友好的“网络异常”提示而不是让界面卡住不动。异步用enqueue而不是execute因为网络请求不能跑在主线程Android在3.0以后就已经禁止在主线程做网络访问了很多新手直接把execute放到Activity里一运行就崩溃。3.2 RecyclerView菜品列表和图片问题菜品列表是整个项目里最像“外卖App”的页面。一张菜品卡片要有图片、名字、价格、加入购物车按钮滚动起来不能卡。用RecyclerView是安卓的标准答案三件事布局管理器、适配器、数据集合。适配器的稳定写法是ViewHolder模式onCreateViewHolder里inflate布局onBindViewHolder里set文本和图片。代码结构如下public class DishAdapter extends RecyclerView.AdapterDishAdapter.ViewHolder { private ListDish list; public DishAdapter(ListDish list) { this.list list; } Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_dish, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(ViewHolder holder, int position) { Dish dish list.get(position); holder.name.setText(dish.getName()); holder.price.setText(¥ dish.getPrice()); Glide.with(holder.itemView.getContext()) .load(dish.getImage()) .placeholder(R.drawable.img_default) .into(holder.image); } // ViewHolder类与列表项控件绑定省略 }图片加载直接用Glide它内部处理了缓存和缩放不需要你自己写Bitmap逻辑。placeholder那张默认图很重要开发阶段服务端经常因为图片路径不对返回404Glide会显示占位图至少界面不会崩。图片URL是这条链路上最容易卡住的地方。开发时如果用模拟器服务端返回的图片地址如果是“localhost:8080/upload/x.jpg”在模拟器里是永远加载不出来的因为模拟器里的localhost指向模拟器自己。这个问题放到下一章联调部分细说先知道结论图片不要存数据库的blob字段存服务器磁盘路径或对象存储URL就行。3.3 购物车与下单App端的状态管理购物车是纯客户端状态服务端不管。技术上可以用一个HashMap维护菜品id和数量的映射在点击加号时更新。核心逻辑可以单独抽一个购物车管理类public class CartManager { private MapInteger, Integer items new HashMap(); // dishId - count public void add(int dishId) { items.put(dishId, items.containsKey(dishId) ? items.get(dishId) 1 : 1); } public void remove(int dishId) { if (!items.containsKey(dishId)) return; int count items.get(dishId) - 1; if (count 0) { items.remove(dishId); } else { items.put(dishId, count); } } public ListOrderItemRequest buildOrderItems() { ListOrderItemRequest result new ArrayList(); for (Map.EntryInteger, Integer entry : items.entrySet()) { result.add(new OrderItemRequest(entry.getKey(), entry.getValue())); } return result; } }下单按钮触发时直接调用buildOrderItems拼JSON放进Request然后发到第2.2节那个/order/create接口。有一个细节容易踩坑下单成功之后要立刻清空购物车否则用户切到别的页面回来购物车角标还是红点继续点下单就变成重复提交。如果服务端做了幂等重复提交会被拒但大多数毕设项目没有这个设计清空动作得靠客户端自觉。购物车状态和界面同步的问题简单项目用EventBus或者接口回调都能解决不必为了这个场景上全套LiveData和ViewModel架构。先把链路跑通架构升级是后话。4. 把项目从zip变成能点外卖的App部署与联调实操到了这一步你已经把服务端和安卓端的代码都过了一遍。接下来要做的是把零散代码变成真正能跑起来的系统。这个环节比写代码更考验耐心因为80%的时间会花在“为什么我连不上”“为什么数据是空的”这种问题上。演示视频这时候是最有用的参照物——别一上来就自己瞎调先看视频里的操作流程照着复现一次。4.1 先把数据库和服务端拉起来Maven与SQL脚本执行顺序任何联调都从数据层开始。常见做法是先找到项目里的sql文件在Navicat或命令行里执行把表和初始数据建好。然后打开服务端工程检查application.properties里的数据库连接spring.datasource.urljdbc:mysql://localhost:3306/waimai?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4 spring.datasource.usernameroot spring.datasource.password你的密码这里最容易被卡住的是serverTimezone参数。MySQL 8.x连接串不带这个参数会直接报时区错误加上Asia/Shanghai就不会有8小时的时间偏移问题。建库时注意选utf8mb4而不是utf8不然菜名里的特殊字符会变成问号。数据库就绪后用Maven启动服务端。IDE里打开导入的Maven工程先执行一下clean再执行install最后跑主类。命令行方式也可以但前提是配了Maven环境变量mvn clean package -DskipTests java -jar target/waimai-0.0.1-SNAPSHOT.jar参数说明-DskipTests跳过测试代码避免测试类没配环境导致构建失败java -jar启动的是打包后的可运行jar访问地址默认是本机8080端口。启动后浏览器直接打开http://localhost:8080/dish/list能看到JSON格式的菜品列表就证明服务端OK了。这个验证动作贯穿整篇的核心先确认接口通再去怀疑安卓端的代码。4.2 模拟器联调adb reverse还是10.0.2.2服务端跑起来后第二步是让安卓App连上它。模拟器环境有个容易混的概念模拟器里的localhost是模拟器自己不是你的开发机。访问你电脑上的服务端两个办法二选一。第一个办法是直接用特殊IP 10.0.2.2这是Android模拟器专门给宿主机的保留地址。把baseUrl的IP改成10.0.2.2模拟器里就能访问到电脑上的8080端口。这个办法简单直接但换机器就要改配置而且真机上完全用不了。第二个办法更推荐adb反向代理adb reverse tcp:8080 tcp:8080这条命令把模拟器的8080端口转发到开发机的8080端口。只要开发机上服务端在监听模拟器就能用localhost:8080访问到它。好处是baseUrl不用改保持一致。注意这条命令每次重新连接设备后都要再执行一次如果你发现之前好端端的突然连不上了先检查adb reverse是不是失效了。用模拟器的另一个坑是图片加载。服务端返回的图片URL如果是localhost开头模拟器永远加载不出来原因同上——它是模拟器自己的localhost。解决方案是在服务端存图片时用相对路径安卓端用baseUrl拼完整路径。4.3 真机联调baseUrl、明文传输与局域网访问到了验收阶段很多人会换上真机测一测结果发现App连不上服务端。这里有三层问题要一起解决。第一层是网络地址。真机不能用localhost必须用开发机的局域网IP。查IP在命令行执行ipconfig或ifconfig然后改HttpManager的BASE_URL。如果手机和电脑不在同一个WiFi下这一步怎么改都没用。第二层是Android 9开始的明文HTTP拦截。从Android 9API 28起系统默认禁止App使用明文HTTP协议你访问http://192.168.1.100:8080会直接抛CleartextNotPermitted异常。解决办法是给Manifest加配置application android:usesCleartextTraffictrue ...这个属性表示允许明文流量。本地开发这么干没问题上线前必须走HTTPS或者网络层换成加密连接否则用户的请求内容能被局域网里任意抓包工具直接看到。第三层是防火墙。Windows会让Java进程监听时弹一次防火墙提示如果手滑点了取消真机访问就会被挡住。排查方法很简单同一局域网拿浏览器访问http://开发机IP:8080/dish/list能出JSON说明服务端对局域网开放了再看App的报错到底是什么。5. 避坑清单安卓外卖App联调中最常见的5类翻车下面这几条是从大量同类项目中筛出来的高频问题每一类我都经历至少三四次。按“现象、原因、解决”的方式写方便你对号入座。5.1 POST请求报415错误。现象安卓端点击下单日志里返回HTTP 415服务端接口没有执行。 原因OkHttp请求的ContentType设成了text/plain而服务端用RequestBody接收对象类型不匹配直接被拒。 解决在构建RequestBody时显式指定MediaType.parse(application/json; charsetutf-8)。另外检查一下请求头有没有带Accept字段带错也会出现类似问题。5.2 模拟器能连、真机连不上。现象同一个baseUrl模拟器里一切正常换成手机就被告知连接超时。 原因多半是baseUrl里写的是localhost或10.0.2.2这两个地址在真机上分别指向手机自己和不存在的网段。还有可能是Windows防火墙拦了8080端口。 解决baseUrl统一改成开发机局域网IP防火墙开端口。记住模拟器和真机最好用同一份配置跑通一遍再进入下一步测试。5.3 接口返回的时间字段变成一长串数字。现象订单列表里的时间为“1718700000000”这种东西不是能读的日期格式。 原因服务端用的是java.util.Date直接返回序列化时被转成了毫秒时间戳客户端没做转换。 解决按第2.3节说的在服务端统一把日期格式化成字符串。如果非要传Date类型安卓端拿到毫秒数用SimpleDateFormat手动转也行但每个页面都要写一遍太不划算。5.4 下单成功购物车没清空重复点按钮生成了两笔订单。现象用户提交一次订单数据库里出现两条一模一样的记录。 原因客户端没有在成功回调里清空CartManager或者用户连点两次提交按钮两次POST都成功到达服务端。 解决成功回调里调用cartManager.clear()提交按钮加一个布尔标志位请求期间禁用点击防止重复提交。更稳妥的做法是加按钮倒计时这个在外卖App里也是常见交互。5.5 Glide加载图片一直显示默认图。现象列表加载出来了但图片全是占位图服务端上传目录里明明有文件。 原因最常见的是图片URL是localhost开头的绝对路径或者服务端存的是反斜杠路径Windows环境下特别容易踩。 解决服务端返回相对路径如/upload/xxx.jpg安卓端在Glide的load方法里拼上baseUrl。另外确认一下服务端有没有配置静态资源映射Spring Boot里要写addResourceHandlers把upload目录暴露出来否则浏览器直接访问图片URL也是404。6. 让它不止于课程设计接口验证、缓存与面试表达把这个外卖App当成课程设计做完和把它当成简历里能讲清楚的项目做完是两码事。我做过的项目中最后拉开差距的往往不是功能多而是边界处理细。先做一遍接口回归验证。App端水平有限但接口是用Postman可以逐条验证的。按业务顺序建一个集合注册登录、菜品列表、加购物车、下单、查订单、更新状态。每一条都确认正常返回和异常返回两种情形。比如下单接口传空菜品列表时服务端是否返回了业务错误码而不是500。这个验证表打印出来你就能在第6章自信地向面试官描述接口的健壮性。然后是缓存。列表接口每次打开都请求一次对用户流量和服务端压力都不友好。给OkHttp加一个Cache设置缓存大小和有效期Cache cache new Cache(context.getCacheDir(), 10 * 1024 * 1024); OkHttpClient client new OkHttpClient.Builder() .cache(cache) .build();配合服务端的Cache-Control响应头菜品列表这类低频变更的数据可以做短时缓存用户二次进入时秒开。注意订单接口千万别缓存用户下单后要能看到最新状态。面试时不要复述项目流程讲两个你做过的设计方案。一个是订单金额处理——为什么用BigDecimal而不是double另一个是下单接口的事务和幂等设计——怎么防止用户双击产生两笔订单。这两点问的是同一个东西你是否理解数据准确性和并发下的边界问题。顺着这个方向再往深处讲就自然过渡到Java面试题里常考的ACID、线程安全、索引设计这些主题上去了。最后说一个我自己的教训。早年间做类似的外卖项目全流程跑通后以为万事大吉结果在演示时断了一次网App直接崩溃当场翻车。后来再写App默认都会给网络请求加一个全局异常捕获保证断网时弹Toast而不是崩掉。这个习惯让我在后面所有项目里都少挨了不少骂。你要是有空先把断网、服务器重启、空列表这三种极端情况在这个项目上测一遍改完你会回来感谢这个建议的。希望帮到你。本文还有配套的精品资源点击获取