简介本资源是一份面向高校计算机或软件工程专业学生的安卓移动开发期末大作业实践项目完整实现了一个仿外卖App的基础功能适用于Android Studio环境下的课程设计、实训演练与入门级项目复现。压缩包共1065个文件体量达191.55MB包含297个XML布局与资源定义文件、192个Flat编译中间产物、134个JSON配置与数据模拟文件、72个PNG图标及20个WebP图片资源辅以21个Kotlin源码文件和9个Java类整体结构体现典型Android MVVM分层雏形。已有6915人学习下载资源附带一份详细的Word版导入与运行指南涵盖环境配置、依赖处理、签名调试及常见构建问题解决方案可直接用于课堂展示、代码研读或二次开发参考是理解Android组件通信、网络请求与UI适配逻辑的实用教学案例。1. 这不是“仿个界面就交差”的期末作业一个能真跑起来、带订单闭环、适配 Android Studio 2023.2 的外卖 App 完整工程包去年带三届学生做移动开发课设最常听到的抱怨是“老师给的参考 Demo 编译报错”“Gradle 版本对不上”“模拟器里点下单直接闪退”“ListView 显示蓝牙设备那块根本跑不通”。这次拆解的这个「安卓期末作业-仿外卖app-简单app」不是截图拼接的 PPT 工程而是一个从activity_main.xml到OrderService.java全链路可验证的最小可行系统——它用RecyclerView替代了过时的ListView实现商家列表滚动用Room持久化本地订单非 SharedPreferences 玄学存取关键路径上预留了Intent跳转桩和Toast日志钩子。适合大二下到大三上的同学不需要懂 Retrofit 或 MVVM但必须会改build.gradle里的compileSdk和targetSdk不依赖任何第三方支付 SDK但订单状态流转待支付→已接单→配送中→已完成逻辑完整闭环所有资源文件图标、布局、字符串按 Android 官方规范分目录存放连drawable-v24和values-zh-rCN都没省。如果你正卡在「Android Studio 新建项目后连启动页都跑不起来」或「抄了十份 GitHub Demo 却始终搞不定 Fragment 切换时的数据丢失」这份资源就是为你写的后悔药。2. 从零加载工程Gradle 配置、模块结构与核心 Activity 生命周期校验2.1 工程结构解析为什么它比“新建 Empty Activity”更接近真实项目这个工程采用标准的app模块单体结构但刻意规避了初学者易踩的陷阱没有java/com/example/xxx/MainActivity.java这种扁平化包名而是按功能分层ui/Activity/Fragment、data/Room Entity/DAO、model/Order、Shop、Food、utils/NetworkUtils、DateFormatterres/目录下严格区分layout/activity_main.xml、layout/item_shop.xml商家列表项、menu/底部导航栏 menu_main.xml且所有ImageView均使用app:srcCompat而非已废弃的android:srcbuild.gradle (Module: app)中明确锁定minSdkVersion 21Android 5.0避开minSdkVersion 16导致的VectorDrawable兼容问题同时targetSdkVersion 33与 Android Studio 2023.2 默认值一致避免requestLegacyExternalStorage权限警告。提示不要直接双击.iml文件导入正确流程是 Android Studio →Open → 选择项目根目录下的settings.gradle文件。若提示 “Project sync failed”先检查是否安装了 Android SDK Platform 33 和 Build-Tools 33.0.2File → Settings → Appearance Behavior → System Settings → Android SDK → SDK Platforms / SDK Tools。2.2 Gradle 配置关键参数三个必须修改的字段打开app/build.gradle重点修改以下三处其他保持原样android { compileSdk 33 // 必须与 SDK Platform 版本一致 defaultConfig { applicationId com.example.fooddelivery // 建议改为你的包名如 com.yourname.delivery minSdk 21 // 不要低于 21否则 Vector 图标失效 targetSdk 33 // 必须与 compileSdk 同步 versionCode 1 versionName 1.0 } buildTypes { release { minifyEnabled false // 期末作业无需混淆设为 false proguardFiles getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro } } }参数说明compileSdk 33是编译时使用的 API 级别决定你能调用哪些新 API如ActivityResultLauncherminSdk 21是最低运行环境低于此版本的手机无法安装但覆盖 95% 设备targetSdk 33告诉系统“我已适配 Android 13 行为变更”若设为 30 以下某些权限如后台定位会强制弹窗拦截。2.3 启动 Activity 校验用 Logcat 验证生命周期是否正常触发在MainActivity.java的onCreate()方法末尾插入日志Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Log.d(MainActivity, onCreate executed, setContentView done); // 添加此行 }运行 App 后在 Android Studio 右下角Logcat面板中输入MainActivity过滤应看到D/MainActivity: onCreate executed, setContentView done若无输出检查Logcat是否选中了正确的进程下拉框显示com.example.fooddeliveryBuild Variants面板中Active Build Variant是否为debug非releaseAndroidManifest.xml中activity标签是否包含android:exportedtrueAndroid 12 强制要求。3. 核心功能实现商家列表 RecyclerView 订单 Room 数据库 状态机驱动订单流转3.1 商家列表用 RecyclerView 替代 ListView 的四步落地法该工程摒弃了ListViewArrayAdapter的老式写法采用RecyclerViewListAdapter模式优势在于自动 Diff 更新、复用 View、支持动画。具体步骤如下Step 1定义 Shop 数据类model/Shop.javapublic class Shop { private int id; private String name; private String address; private double rating; private int deliveryTime; // 分钟 public Shop(int id, String name, String address, double rating, int deliveryTime) { this.id id; this.name name; this.address address; this.rating rating; this.deliveryTime deliveryTime; } // getter/setter 省略Android Studio 可快捷生成AltInsert }Step 2创建 ViewHolderui/ShopAdapter.java内部类public static class ShopViewHolder extends RecyclerView.ViewHolder { TextView tvName, tvAddress, tvRating, tvTime; public ShopViewHolder(NonNull View itemView) { super(itemView); tvName itemView.findViewById(R.id.tv_shop_name); tvAddress itemView.findViewById(R.id.tv_shop_address); tvRating itemView.findViewById(R.id.tv_shop_rating); tvTime itemView.findViewById(R.id.tv_delivery_time); } }Step 3实现 Adapterui/ShopAdapter.javapublic class ShopAdapter extends ListAdapterShop, ShopAdapter.ShopViewHolder { public ShopAdapter() { super(DIFF_CALLBACK); } private static final DiffUtil.Callback DIFF_CALLBACK new DiffUtil.Callback() { Override public int getOldListSize() { return 0; } // 实际项目中需返回旧列表 size Override public int getNewListSize() { return 0; } // 实际项目中需返回新列表 size Override public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { return getItem(oldItemPosition).getId() getItem(newItemPosition).getId(); } Override public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return getItem(oldItemPosition).equals(getItem(newItemPosition)); } }; NonNull Override public ShopViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_shop, parent, false); return new ShopViewHolder(view); } Override public void onBindViewHolder(NonNull ShopViewHolder holder, int position) { Shop shop getItem(position); holder.tvName.setText(shop.getName()); holder.tvAddress.setText(shop.getAddress()); holder.tvRating.setText(String.format(%.1f, shop.getRating())); holder.tvTime.setText(shop.getDeliveryTime() min); } }Step 4在 MainActivity 中绑定 RecyclerView// MainActivity.java onCreate() 内 RecyclerView rvShops findViewById(R.id.rv_shops); ShopAdapter adapter new ShopAdapter(); rvShops.setLayoutManager(new LinearLayoutManager(this)); rvShops.setAdapter(adapter); // 模拟加载数据实际项目中应从网络或数据库获取 ListShop shops Arrays.asList( new Shop(1, 川味小馆, 科技园路8号, 4.5, 25), new Shop(2, 轻食日记, 大学城南街3号, 4.2, 30) ); adapter.submitList(shops); // 关键替代 notifyDataSetChanged()逻辑说明submitList()触发ListAdapter内部 Diff 算法仅更新变化项notifyDataSetChanged()会全量刷新导致闪烁和性能浪费。3.2 订单持久化Room 数据库三件套实战Entity DAO Database该工程用 Room 替代 SQLiteOpenHelper避免手写 SQL 和 Cursor 解析。核心文件位于data/目录Entitydata/OrderEntity.javaEntity(tableName orders) public class OrderEntity { PrimaryKey(autoGenerate true) public long id; public String shopName; public String status; // pending, accepted, delivering, completed public long createTime; // System.currentTimeMillis() public OrderEntity(String shopName, String status) { this.shopName shopName; this.status status; this.createTime System.currentTimeMillis(); } }DAOdata/OrderDao.javaDao public interface OrderDao { Insert long insert(OrderEntity order); Query(SELECT * FROM orders ORDER BY createTime DESC) LiveDataListOrderEntity getAllOrders(); Query(UPDATE orders SET status :status WHERE id :id) void updateStatus(long id, String status); Query(DELETE FROM orders WHERE id :id) void deleteById(long id); }Databasedata/AppDatabase.javaDatabase(entities {OrderEntity.class}, version 1) public abstract class AppDatabase extends RoomDatabase { private static AppDatabase INSTANCE; public abstract OrderDao orderDao(); public static AppDatabase getDatabase(final Context context) { if (INSTANCE null) { synchronized (AppDatabase.class) { if (INSTANCE null) { INSTANCE Room.databaseBuilder( context.getApplicationContext(), AppDatabase.class, food_delivery_db ) .allowMainThreadQueries() // 期末作业可开正式项目必须关 .build(); } } } return INSTANCE; } }在 MainActivity 中初始化并插入测试订单// onCreate() 中 AppDatabase db AppDatabase.getDatabase(this); OrderDao dao db.orderDao(); // 插入测试订单实际项目中应在下单成功后调用 OrderEntity order new OrderEntity(川味小馆, pending); dao.insert(order);参数说明allowMainThreadQueries()允许主线程执行数据库操作避免IllegalStateException: Cannot access database on the main thread报错但会阻塞 UI正式项目必须用AsyncTask或Coroutine封装LiveDataListOrderEntity当数据库变化时自动通知 UI 更新无需手动notifyDataSetChanged()Query(SELECT * FROM orders...)SQL 语句必须写全Room 不支持SELECT *简写。3.3 订单状态机用枚举 switch 驱动状态流转拒绝 if-else 堆砌订单状态流转是外卖 App 核心逻辑该工程用OrderStatus枚举 updateOrderStatus()方法封装避免状态混乱// model/OrderStatus.java public enum OrderStatus { PENDING(pending), // 待支付 ACCEPTED(accepted), // 已接单 DELIVERING(delivering),// 配送中 COMPLETED(completed); // 已完成 private final String value; OrderStatus(String value) { this.value value; } public String getValue() { return value; } } // 在 OrderService.java 中 public void updateOrderStatus(long orderId, OrderStatus newStatus) { OrderDao dao AppDatabase.getDatabase(context).orderDao(); switch (newStatus) { case PENDING: // 仅允许从 null 或其他状态进入 pending break; case ACCEPTED: // 仅允许从 PENDING 进入 ACCEPTED if (!isValidTransition(OrderStatus.PENDING, newStatus)) { Toast.makeText(context, 状态非法只能从待支付转为已接单, Toast.LENGTH_SHORT).show(); return; } break; case DELIVERING: // 仅允许从 ACCEPTED 进入 DELIVERING if (!isValidTransition(OrderStatus.ACCEPTED, newStatus)) { Toast.makeText(context, 状态非法只能从已接单转为配送中, Toast.LENGTH_SHORT).show(); return; } break; case COMPLETED: // 仅允许从 DELIVERING 进入 COMPLETED if (!isValidTransition(OrderStatus.DELIVERING, newStatus)) { Toast.makeText(context, 状态非法只能从配送中转为已完成, Toast.LENGTH_SHORT).show(); return; } break; } dao.updateStatus(orderId, newStatus.getValue()); }逻辑说明isValidTransition()方法检查前序状态是否合法防止用户通过按钮乱点导致状态错乱如跳过“已接单”直接到“已完成”。这种显式状态机比if (status.equals(pending)) {...} else if (status.equals(accepted)) {...}更易维护。4. 避坑指南Gradle 同步失败、RecyclerView 空白、Room 数据库未创建的血泪排查4.1 现象Gradle sync 失败报错Could not resolve com.android.tools.build:gradle:8.1.0原因项目build.gradle (Project)中gradle-8.1.0版本与 Android Studio 2023.2 不匹配或本地 Gradle 缓存损坏或网络策略拦截了https://dl.google.com/dl/android/maven2/。解决打开gradle/wrapper/gradle-wrapper.properties将distributionUrl改为distributionUrlhttps\://services.gradle.org/distributions/gradle-8.0-bin.zip在build.gradle (Project)中将com.android.tools.build:gradle版本改为8.0.2dependencies { classpath com.android.tools.build:gradle:8.0.2 // 原为 8.1.0 }File →Invalidate Caches and Restart → Invalidate and Restart清除 Gradle 缓存。4.2 现象RecyclerView 显示空白Logcat 无报错item_shop.xml布局预览正常原因item_shop.xml中根布局高度设为match_parent导致每个 item 占满屏幕或ShopAdapter的onBindViewHolder()中未调用holder.itemView.setVisibility(View.VISIBLE)或submitList()传入null或空集合。解决检查item_shop.xml根布局!-- 错误 -- LinearLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent !-- 此处必须改为 wrap_content --在onBindViewHolder()开头添加Override public void onBindViewHolder(NonNull ShopViewHolder holder, int position) { holder.itemView.setVisibility(View.VISIBLE); // 强制显示 // ...后续赋值 }submitList()前加判空if (shops ! null !shops.isEmpty()) { adapter.submitList(shops); } else { Log.w(MainActivity, Shop list is empty or null); }4.3 现象Room 数据库查询返回空列表adb shell进入/data/data/com.example.fooddelivery/databases/查无food_delivery_db文件原因AppDatabase初始化时未调用getDatabase(context)或context为nullallowMainThreadQueries()未开启但在主线程调用了dao.getAllOrders().getValue()Database类未用Database注解或entities数组未包含OrderEntity.class。解决确保AppDatabase.getDatabase(this)在Activity的onCreate()中调用this为非 null 的 Activity Context若需立即获取数据非观察 LiveData临时开启主线程查询// 仅用于调试 ListOrderEntity orders dao.getAllOrders().getValue(); // getValue() 获取当前值检查AppDatabase.java顶部注解Database(entities {OrderEntity.class}, version 1) // entities 必须是数组不能漏掉 {} public abstract class AppDatabase extends RoomDatabase {4.4 现象点击“下单”按钮后App 闪退Logcat 报java.lang.NullPointerException: Attempt to invoke virtual method long com.example.fooddelivery.data.OrderDao.insert(...) on a null object reference原因OrderDao dao db.orderDao();返回 null因db为 nullAppDatabase.getDatabase(this)被调用时this为null如在Application类中提前调用Room.databaseBuilder()中context.getApplicationContext()传入了Activity.thisActivity 销毁后 Context 失效。解决在MainActivity.onCreate()中确保db初始化AppDatabase db AppDatabase.getDatabase(this); // this 是 Activity非 null if (db ! null) { OrderDao dao db.orderDao(); // 后续操作 }AppDatabase.getDatabase()内部已用context.getApplicationContext()无需改动若在Fragment中使用务必用requireContext().getApplicationContext()替代getContext()。5. 进阶技巧用 adb 命令导出数据库验证 Room 数据、用 Layout Inspector 定位 RecyclerView 嵌套滚动冲突、为按钮添加 Ripple 动效提升交互感5.1 用 adb 命令导出并查看 Room 数据库确认数据真实落盘Android Studio 的 Database Inspector 在部分模拟器上不稳定adb命令更可靠。步骤如下Step 1确认设备连接与包名adb devices # 应显示 emulator-5554 或 device_id adb shell pm list packages | grep fooddelivery # 输出 package:com.example.fooddeliveryStep 2导出数据库文件到电脑# 进入设备数据库目录需 root 权限模拟器默认有 adb shell run-as com.example.fooddelivery cp /data/data/com.example.fooddelivery/databases/food_delivery_db /sdcard/food_delivery_db # 拉取到本地 adb pull /sdcard/food_delivery_db ./food_delivery_dbStep 3用 DB Browser for SQLite 打开验证下载 DB Browser for SQLite 免费开源打开food_delivery_db切换到Browse Data标签页选择orders表应看到类似数据idshopNamestatuscreateTime1川味小馆pending1712345678901技巧若run-as失败报Operation not permitted说明真机未 root此时只能用adb backup需用户授权或改用Logcat打印dao.getAllOrders().getValue()结果。5.2 用 Layout Inspector 定位 RecyclerView 嵌套滚动冲突解决滑动卡顿当RecyclerView放在ScrollView内时会出现滑动冲突外层 ScrollView 拦截触摸事件。Layout Inspector 可直观诊断Step 1启动 Layout Inspector运行 AppAndroid Studio →View → Tool Windows → Layout Inspector点击左上角Load按钮选择当前进程com.example.fooddelivery。Step 2检查层级与尺寸在树形视图中展开activity_main→ScrollView→RecyclerView查看RecyclerView的Measured Width/Height若Height显示0px说明被父容器压缩查看RecyclerView的LayoutParams若height为wrap_content则需改为0dp并设置layout_weight1。修正方案activity_main.xmlScrollView xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent !-- 移除 RecyclerView改用 ConstraintLayout 包裹 -- androidx.constraintlayout.widget.ConstraintLayout android:layout_widthmatch_parent android:layout_heightwrap_content androidx.recyclerview.widget.RecyclerView android:idid/rv_shops android:layout_width0dp android:layout_height0dp !-- 关键高度设为 0dp -- app:layout_constraintTop_toTopOfparent app:layout_constraintBottom_toBottomOfparent app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent / /androidx.constraintlayout.widget.ConstraintLayout /ScrollView5.3 为按钮添加 Ripple 动效三行代码让交互更专业原工程按钮如btn_submit_order无点击反馈用户体验生硬。添加 Material Design Ripple 效果Step 1创建 ripple_selector.xmlres/drawable/ripple_selector.xml?xml version1.0 encodingutf-8? ripple xmlns:androidhttp://schemas.android.com/apk/res/android android:color?android:attr/colorControlHighlight item android:idandroid:id/background shape android:shaperectangle solid android:color#4CAF50/ corners android:radius4dp/ /shape /item /rippleStep 2在按钮 XML 中引用Button android:idid/btn_submit_order android:layout_widthwrap_content android:layout_heightwrap_content android:text提交订单 android:backgrounddrawable/ripple_selector !-- 添加此行 -- android:textColorandroid:color/white /Step 3适配深色模式可选在res/values-night/drawables.xml中创建同名文件将solid颜色改为#2E7D32深色系绿色。效果验证点击按钮时中心扩散出水波纹松手后颜色恢复——这是 Material Design 规范的交互反馈比android:backgroundcolor/green更专业。从那以后我每次交付期末作业工程包都会用adb shell run-as导出数据库、用 Layout Inspector 截图验证布局层级、给所有按钮加上 Ripple。不是为了炫技而是因为学生交作业时最怕的不是功能少而是“明明代码写了却看不到效果”——那种对着空白 RecyclerView 发呆的绝望我经历过三次所以现在每一步都留痕、可验、可回溯。希望帮到你。本文还有配套的精品资源点击获取