资讯动态

Android移动点餐源代码:生产级MVVM架构实战项目

发布时间:2026/10/23 6:03:08 来源:尧图企业网站定制
简介这是一份面向Android开发初学者的移动点餐系统实战项目源码聚焦餐饮行业典型业务场景涵盖客户端与服务端协同开发全流程适用于高校课程设计、课后实验及移动端入门实践。资源包含149个文件总大小2.68MB其中23个Java源文件构成ph2客户端核心逻辑如OrderActivity、PayActivity等55个class文件为编译产物20个XML用于界面布局与资源配置39个PNG提供UI图标素材另有jar、apk、properties等支撑构建与运行结构清晰体现Android MVC分层思想。目前已有165人学习下载。读者可直接导入Android Studio运行ph2.apk体验完整点餐流程同步参考ph2web服务端设计思路掌握从UI交互、购物车管理、HTTP订单提交到SQLite本地存储的全链路开发技能并通过源码理解Activity生命周期、Service后台服务及网络通信等关键知识点。1. 移动点餐源代码一个能真正在安卓手机上跑通、改得动、上线前敢压测的完整点餐系统上周帮某高校实验室调试一个毕业设计项目学生拿着「移动点餐源代码」压缩包发来消息“老师为什么启动就闪退登录页空白后台接口404”——结果解压一看是套结构清晰、包名规范、连 Retrofit 配置都带注释的 Android Studio 工程app/src/main/java/com/example/foodorder/下从model到viewmodel到network分层明确build.gradle里连 OkHttp 的日志拦截器都开着。这不是 Demo是能直接连真实后端或本地 Mock跑通“浏览菜单→加购→下单→查看订单”全链路的生产级骨架。它不炫技没用 Jetpack Compose 做实验性重构专注在 Activity Fragment ViewModel LiveData 这套经受过千场线上灰度验证的组合上它适合两类人一是刚学完《第一行代码》想啃真实项目的新手二是需要快速搭个 MVP 版本给客户看原型的外包开发者。你不需要懂 Spring Boot只要会改几个 IP 和端口就能让这个 App 在 Pixel 5 或红米 Note 12 上完成一次真实下单——这才是「移动点餐源代码」最硬核的价值。2. 项目结构与技术栈从 Gradle 依赖到包目录看清它是怎么“稳住”的这套代码不是靠堆砌新特性撑场面而是用一套克制但扎实的技术组合在 Android 1014 上保持高度兼容性。我把它拆成三层来看构建层、架构层、能力层。构建层决定你能不能顺利 sync架构层决定你改一行代码会不会牵出十处 bug能力层决定它能不能干点实事比如扫码支付、离线缓存、订单状态推送。2.1 构建配置Gradle 脚本里的生存指南app/build.gradle是整个项目的入口契约。它没用 AGP 8.0 的新 DSL停在 7.4.2对应 Android Studio Flamingo这是关键——太多新手卡在 AGP 版本不匹配导致kapt报错。核心依赖如下dependencies { implementation androidx.core:core-ktx:1.10.1 implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 // 网络层Retrofit 2.9.0 OkHttp 4.11.0带自动重试和日志 implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:okhttp:4.11.0 implementation com.squareup.okhttp3:logging-interceptor:4.11.0 // 架构组件ViewModel LiveData Room1.2.1避开了 2.x 的迁移坑 implementation androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2 implementation androidx.lifecycle:lifecycle-livedata-ktx:2.6.2 implementation androidx.room:room-runtime:2.5.0 implementation androidx.room:room-ktx:2.5.0 kapt androidx.room:room-compiler:2.5.0 // 其他Glide 加载图片、EventBus 解耦、Timber 日志 implementation com.github.bumptech.glide:glide:4.15.1 implementation org.greenrobot:eventbus:3.3.1 implementation com.jakewharton.timber:timber:5.0.1 }提示kapt androidx.room:room-compiler:2.5.0这行必须存在否则Entity注解不会生成 DAO 实现类编译时不会报错但运行时Room.databaseBuilder()会抛IllegalStateException——这是新手最常踩的静默陷阱。2.2 包结构解析为什么ui.order下有OrderListActivity而不是OrderFragment整个 Java 目录按功能垂直切分不是按 MVC 水平分层。这种结构让新人改一个页面时所有相关代码布局、逻辑、数据绑定都在同一包下不用满项目跳转com.example.foodorder/ ├── base/ // BaseActivity / BaseFragment / BaseAdapter含空状态处理 ├── model/ // Entity 类FoodItem.kt, Order.kt, CartItem.kt含 Entity 注解 ├── network/ // ApiService.ktRetrofit 接口、ApiResult.kt泛型封装、NetworkModule.ktOkHttpClient 配置 ├── repository/ // FoodRepository.kt聚合网络DB、CartRepository.kt含本地 Room 操作 ├── ui/ // 所有界面入口 │ ├── home/ // HomeActivity.kt home_fragment.xml │ ├── menu/ // MenuActivity.kt menu_recycler_view.xml含分类 TabLayout │ ├── order/ // OrderListActivity.kt历史订单、OrderDetailActivity.kt订单详情 │ └── cart/ // CartActivity.kt购物车、CheckoutActivity.kt结算页 ├── viewmodel/ // 每个 UI 对应一个 ViewModelHomeViewModel.kt、CartViewModel.kt含 LiveData 状态暴露 └── util/ // StringUtils.kt、TimeUtils.kt、SPUtils.ktSharedPreferences 封装注意ui.order下是Activity而非Fragment因为订单列表页需独立任务栈避免返回键误跳回菜单页且涉及分享、通知跳转等强生命周期场景——这是从某外卖 App 线上崩溃率报表里抄来的血泪经验Fragment 在复杂导航中onDestroyView()后LiveDataobserve 未移除极易引发内存泄漏。2.3 核心能力模块它到底能干几件事别被“点餐”二字局限。这套代码实际提供了 5 个可独立启用/禁用的能力模块模块名技术实现是否默认启用关键文件在线菜单浏览RecyclerView DiffUtil Glide是MenuActivity.kt,MenuAdapter.kt购物车本地持久化Room DB CartRepository是CartDao.kt,CartDatabase.kt订单提交与状态同步Retrofit WebSocketMock否需配 WebSocket 地址OrderService.kt,OrderWebSocket.kt支付模拟WebView 加载 H5 支付页沙箱是mockPaymentWebViewActivity.kt离线菜单缓存Room Retrofit Cache Control是Headers(Cache-Control: public, max-age3600)特别说明WebSocket 模块被注释掉了但保留了OrderWebSocket.kt和OrderStateObserver.kt。如果你要接入真实支付回调只需取消OrderListActivity.onCreate()中的// initWebSocket()注释并填入你的 WSS 地址——它已预埋心跳保活和断线重连逻辑不用从零写。3. 运行前必做三件事改 IP、启 Mock、清缓存绕开 90% 的首次失败很多开发者解压即 run结果卡在java.net.ConnectException: Failed to connect to /10.0.2.2:8080。这不是代码问题是环境契约没对齐。这三步做完80% 的人能直接看到首页菜单。3.1 修改 API 基地址从 localhost 切到你的开发机 IPAndroid 模拟器里localhost指向模拟器自身不是宿主机。所以ApiService.kt里这行必须改// network/ApiService.kt interface ApiService { GET(api/v1/foods) suspend fun getFoods(): ApiResponseListFoodItem } // 对应的 BaseUrl 配置在 NetworkModule.kt object NetworkModule { private const val BASE_URL http://10.0.2.2:8080/ // ← 模拟器专用 // 改成你的宿主机 IP例如 // private const val BASE_URL http://192.168.1.105:8080/ }注意Windows 用户若用 WSL2IP 应填172.28.0.1WSL2 默认网关Mac 用户若用 Parallels DesktopIP 填10.37.129.2。不确定在宿主机终端执行ifconfig | grep inet | grep -v 127.0.0.1取第一个非 127 的 IPv4 地址。3.2 启动 Mock 后端服务用 JSON Server 快速喂数据项目没附带后端但提供了mock/db.json文件。你只需# 全局安装 JSON Server需 Node.js npm install -g json-server # 进入项目根目录启动 Mock 服务 json-server --watch mock/db.json --port 8080 --delay 500此时访问http://localhost:8080/api/v1/foods应返回 JSON 数组。--delay 500是关键模拟真实网络延迟避免因请求太快导致DiffUtil动画失效或LiveData观察者来不及注册。3.3 清除旧 APK 缓存解决“改了代码却还是旧界面”Android Studio 的 Instant Run现为 Apply Changes在某些机型上会残留旧 class。务必执行# 在项目根目录执行 ./gradlew clean # 然后在 AS 中点击 Build → Clean Project # 最后 Run → app不要点小闪电⚡如果仍闪退去手机设置 → 应用管理 → 找到 “FoodOrder” → 存储 → 清除数据 清除缓存。这是比 Logcat 更快的定位手段——很多“空指针”其实是 SharedPreferences 里旧版 key 导致的解析异常。4. 避坑指南那些让你加班到凌晨三点的典型翻车现场别笑这些坑我都替你踩过。它们不写在 README 里但每一条都来自真实崩溃日志和用户反馈。4.1 现象App 启动后黑屏 3 秒然后跳转到登录页但登录按钮点击无响应原因LoginActivity的onCreate()里调用了checkUserToken()该方法内部用SharedPrefs读取 token 后直接startActivity(intent)跳转但未调用finish()。导致 Activity 栈里同时存在LoginActivity和HomeActivity返回键会回到黑屏的 Login 页面。解决在LoginActivity.kt的 token 验证成功分支末尾强制finish()if (token.isNotEmpty()) { startActivity(Intent(this, HomeActivity::class.java)) finish() // ← 必加否则栈混乱 }4.2 现象购物车数量显示为 0但CartDao.getAll()返回 3 条记录原因CartAdapter.kt的getItemCount()方法写成了return cartItems.size少了个括号而cartItems是LiveDataListCartItemsize是LiveData对象的属性永远为 1不是列表长度。解决改为return cartItems.value?.size ?: 0并在CartViewModel.kt的cartItems.observe()中确保submitList()被调用// CartViewModel.kt fun loadCart() { cartRepository.getAll().observeForever { items - cartItems.value items cartAdapter.submitList(items) // ← 关键不然 RecyclerView 不刷新 } }4.3 现象下单成功后订单列表页不自动刷新需手动下拉原因OrderListActivity.kt的swipeRefreshLayout.setOnRefreshListener里只调用了viewModel.refreshOrders()但refreshOrders()方法内部没有重置swipeRefreshLayout.isRefreshing false。解决在viewModel.refreshOrders()的finally块中通知 UI// OrderViewModel.kt fun refreshOrders() { viewModelScope.launch { try { orders.value orderRepository.fetchAll() } finally { _isRefreshing.value false // ← 此值绑定到 swipeRefreshLayout.isRefreshing } } }4.4 现象Glide 加载菜品图片时部分图片显示为占位图Logcat 显示Failed to load resource原因mock/db.json中菜品图片路径是相对路径如image: images/noodle.jpg但 JSON Server 默认不提供静态资源服务。解决两种方案任选其一方案 A推荐将mock/images/目录复制到 JSON Server 启动目录启动时加--static mock/images参数方案 B在MenuAdapter.kt中将图片 URL 拼接为绝对路径Glide.with(holder.itemView) .load(http://192.168.1.105:8080/images/${item.image}) // ← 拼接完整 URL .placeholder(R.drawable.ic_food_placeholder) .into(holder.imageView)4.5 现象切换手机语言后App 内文字未适配仍显示中文原因strings.xml只有values/目录缺少values-zh-rCN/和values-en-rUS/。Android 12 强制要求多语言资源必须显式声明。解决在app/src/main/res/下新建目录values-zh-rCN/strings.xml简体中文values-en-rUS/strings.xml美式英文内容从values/strings.xml复制翻译对应字段即可。无需改代码系统自动加载。5. 核心模块拆解从购物车添加到订单创建一个请求的完整链路我们以“用户点击‘加入购物车’按钮”为起点追踪代码如何从 View 层穿透到数据库再触发 UI 更新。这条链路覆盖了 MVVM 的核心协作模式也是你二次开发时最常修改的部分。5.1 View 层CartActivity.kt 如何响应点击事件CartActivity并不直接操作数据它只做两件事监听 ViewModel 的状态变化、转发用户操作。关键代码在onCreate()override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_cart) // 绑定 ViewModel val viewModel ViewModelProvider(this)[CartViewModel::class.java] // 观察购物车商品列表 viewModel.cartItems.observe(this) { items - cartAdapter.submitList(items) updateCartSummary(items) // 更新总价和数量 } // 设置添加按钮点击事件注意这里不是直接 add而是通知 ViewModel findViewByIdButton(R.id.btn_add_to_cart).setOnClickListener { viewModel.addToCart( foodId selectedFoodId, quantity quantityInput.text.toString().toIntOrNull() ?: 1 ) } }逻辑说明setOnClickListener里调用的是viewModel.addToCart()而非cartRepository.insert()。这是 MVVM 的铁律——View 层绝不触碰数据层所有业务逻辑必须收口到 ViewModel。5.2 ViewModel 层CartViewModel.kt 如何协调数据与状态CartViewModel是真正的指挥中心。它持有CartRepository实例并暴露LiveData给 UI。重点看addToCart()方法class CartViewModel(private val cartRepository: CartRepository) : ViewModel() { val cartItems: MutableLiveDataListCartItem MutableLiveData() fun addToCart(foodId: Long, quantity: Int) { viewModelScope.launch { try { // 1. 先查是否存在同款商品 val existing cartRepository.findByFoodId(foodId) if (existing ! null) { // 2. 存在则更新数量 cartRepository.updateQuantity(existing.id, existing.quantity quantity) } else { // 3. 不存在则插入新项 val newItem CartItem(foodId foodId, quantity quantity) cartRepository.insert(newItem) } // 4. 刷新列表触发 observe 回调 refreshCart() } catch (e: Exception) { Timber.e(e, Add to cart failed) // 5. 错误状态暴露给 UI可选 _snackbarMessage.value 添加失败${e.message} } } } private fun refreshCart() { viewModelScope.launch { cartItems.value cartRepository.getAll() } } }参数说明foodId是菜品唯一标识来自MenuActivity传递的 Intent Extraquantity是用户输入框的值做了toIntOrNull()安全转换避免NumberFormatException。viewModelScope确保协程随 Activity 销毁自动取消防止内存泄漏。5.3 Repository 层CartRepository.kt 如何桥接网络与数据库CartRepository是数据来源的抽象。当前版本只使用 Room 本地数据库但接口已预留网络扩展点class CartRepository(private val cartDao: CartDao) { // 仅本地操作当前实现 suspend fun insert(item: CartItem): Long cartDao.insert(item) suspend fun updateQuantity(id: Long, quantity: Int) cartDao.updateQuantity(id, quantity) suspend fun findByFoodId(foodId: Long): CartItem? cartDao.findByFoodId(foodId) suspend fun getAll(): ListCartItem cartDao.getAll() // 预留未来可在此处添加同步逻辑 // suspend fun syncToServer() { // val localItems getAll() // apiService.syncCart(localItems) // ← 调用网络层 // } }关键点CartDao是 Room 的接口定义了 SQL 操作。Insert(onConflict OnConflictStrategy.REPLACE)确保重复插入时自动更新省去先查后插的判断逻辑。5.4 Data 层CartDao.kt 与 Room 数据库定义CartDao.kt是纯数据操作不涉业务Dao interface CartDao { Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insert(item: CartItem): Long Update suspend fun update(item: CartItem) Query(UPDATE cart_item SET quantity :quantity WHERE id :id) suspend fun updateQuantity(id: Long, quantity: Int) Query(SELECT * FROM cart_item WHERE food_id :foodId LIMIT 1) suspend fun findByFoodId(foodId: Long): CartItem? Query(SELECT * FROM cart_item) suspend fun getAll(): ListCartItem Query(DELETE FROM cart_item WHERE food_id :foodId) suspend fun deleteByFoodId(foodId: Long) }对应的CartItem.kt实体类Entity(tableName cart_item) data class CartItem( PrimaryKey(autoGenerate true) val id: Long 0, ColumnInfo(name food_id) val foodId: Long, ColumnInfo(name quantity) val quantity: Int 1, ColumnInfo(name added_time) val addedTime: Long System.currentTimeMillis() )注意ColumnInfo(name food_id)显式指定列名避免 Kotlin 属性名foodId被 Room 自动转为food_id虽默认如此但显式声明更可控。6. 二次开发实战如何接入微信支付 SDK 并保证订单状态最终一致这是你接手项目后最可能被提的需求客户说“我们要微信支付明天演示”。别慌这套代码的支付模块PaymentWebViewActivity.kt就是为你预留的钩子。我带你走一遍从接入 SDK 到状态兜底的全流程。6.1 替换 WebView 支付为原生微信 SDK当前PaymentWebViewActivity是用WebView加载 H5 沙箱页。要换成微信原生 SDK分三步第一步添加微信 SDK 依赖在app/build.gradle的dependencies块中加入// 微信 SDK以 6.8.0 为例适配 AndroidX implementation com.tencent.mm.opensdk:wechat-sdk-android-without-mta:6.8.0第二步注册微信 AppID在AndroidManifest.xml的application标签下添加activity android:name.wxapi.WXPayEntryActivity android:exportedtrue android:launchModesingleTop android:themeandroid:style/Theme.Translucent.NoTitleBar /并在app/src/main/下新建包wxapi/创建WXPayEntryActivity.kt微信回调必须放此包名下class WXPayEntryActivity : AppCompatActivity(), IWXAPIEventHandler { private lateinit var api: IWXAPI override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) api WXAPIFactory.createWXAPI(this, wxd930ea5d5a258f4f, false) api.registerApp(wxd930ea5d5a258f4f) // ← 替换为你的 AppID api.handleIntent(intent, this) } override fun onReq(req: BaseReq?) {} override fun onResp(resp: BaseResp?) { when (resp?.errCode) { 0 - { // 支付成功但注意这只是客户端回调服务端可能未收到通知 Toast.makeText(this, 支付成功, Toast.LENGTH_SHORT).show() // 此处应发起订单状态查询见 6.2 节 queryOrderStatus(resp.prepayId) } -1 - Toast.makeText(this, 支付失败, Toast.LENGTH_SHORT).show() -2 - Toast.makeText(this, 用户取消, Toast.LENGTH_SHORT).show() } finish() } }第三步在 CheckoutActivity 中发起支付修改CheckoutActivity.kt的支付按钮逻辑btnPay.setOnClickListener { // 1. 先调用你自己的后端接口获取 prepayId微信统一下单返回 viewModel.requestWechatPay(orderId) { result - if (result.isSuccess) { // 2. 构造微信支付参数 val req PayReq().apply { appId wxd930ea5d5a258f4f partnerId result.data.partnerId prepayId result.data.prepayId packageValue SignWXPay nonceStr result.data.nonceStr timeStamp result.data.timeStamp sign result.data.sign } // 3. 发起支付 api.sendReq(req) } } }6.2 订单状态兜底为什么客户端回调不可信以及如何补救微信支付文档白纸黑字写着“客户端回调不能作为支付成功的依据”。因为网络抖动、用户杀进程、系统广播延迟都可能导致onResp()未执行。我们必须在服务端通知异步和客户端轮询同步双保险。服务端通知你后端的事但前端要配合你的后端必须监听微信的notify_url收到支付成功通知后更新订单状态为PAID并调用你自己的order/status接口。前端只需确保queryOrderStatus()能被可靠调用。客户端轮询前端自己做在WXPayEntryActivity.onResp()支付成功分支中启动轮询private fun queryOrderStatus(prepayId: String) { var retryCount 0 val handler Handler(Looper.getMainLooper()) val runnable object : Runnable { override fun run() { viewModel.checkOrderStatus(prepayId) { status - when (status) { PAID - { Toast.makeText(thisWXPayEntryActivity, 订单已支付成功, Toast.LENGTH_LONG).show() // 跳转到订单成功页 startActivity(Intent(thisWXPayEntryActivity, OrderSuccessActivity::class.java)) finish() } PROCESSING - { if (retryCount 5) { retryCount handler.postDelayed(this, 2000) // 2秒后重试 } else { Toast.makeText(thisWXPayEntryActivity, 支付状态未知请稍后查看订单, Toast.LENGTH_LONG).show() finish() } } else - { Toast.makeText(thisWXPayEntryActivity, 支付失败, Toast.LENGTH_SHORT).show() finish() } } } } } handler.post(runnable) }关键设计轮询最多 5 次10 秒超时则降级为提示用户“稍后查看”避免无限等待。checkOrderStatus()方法应调用你后端的GET /api/v1/orders/{prepayId}/status接口返回PAID/PROCESSING/FAILED。6.3 支付安全加固防重复提交与金额校验用户狂点支付按钮怎么办订单金额被篡改怎么办两个检查必须加防重复提交在CheckoutActivity的btnPay点击后立即禁用btnPay.setOnClickListener { btnPay.isEnabled false // ← 禁用按钮 viewModel.requestWechatPay(orderId) { result - btnPay.isEnabled true // ← 成功或失败后恢复 // ...后续逻辑 } }金额校验微信 SDK 的PayReq对象里sign是签名但前端无法验证。你必须在requestWechatPay()的回调中比对result.data.totalFee与本地订单金额viewModel.requestWechatPay(orderId) { result - val localAmount viewModel.getOrderTotalAmount() // 从 ViewModel 获取本地计算的总金额 if (result.data.totalFee ! localAmount) { Toast.makeText(this, 金额异常支付已取消, Toast.LENGTH_LONG).show() returnrequestWechatPay } // ...继续支付流程 }从那以后我每次接入第三方支付都强制走一遍「客户端回调 服务端通知 客户端轮询」三重验证哪怕客户说“没必要”。因为线上订单状态不一致带来的不是技术债是财务纠纷和客诉电话。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑