
简介面向安卓初学者的期末大作业参考项目以仿外卖App为核心功能覆盖首页商家列表、商品分类、购物车、下单结算等常见业务场景适合移动开发课程设计、期末考核或入门练手。压缩包内共有1065个文件包含297个xml布局文件、192个flat构建缓存、134个json数据文件、72个png图片资源以及kotlin/java源码、dex/class编译产物和APK安装包等整体约191.55MB工程结构完整。通过学习本包可掌握Android Studio项目导入与Gradle构建流程、页面布局编写、列表数据绑定、资源文件组织及APK打包发布等实操技能附带的安卓导入Word文档还能帮助快速完成开发环境准备与项目运行。该资源已有6900余人学习下载可作为期末突击参考、功能模块拆分或二次开发基础。1. 仿外卖App这套期末作业直接拿去改值不值期末大作业压在头上安卓方向最常碰到的题目就是「仿外卖App」——功能不难但页面多、状态杂从列表到购物车再到下单每一步都在卡人。这套资源是Android Studio写的简单版仿外卖App代码量不大包体结构清晰刚好卡在期末答辩要求的那条线上有界面、有交互、有数据、有本地存储。适合零基础赶工的同学也适合想快速套框架改功能的移动开发新手。先说结论值得下但别拿到手就直接双击运行里面好几个坑我后面一个个拆开讲。2. 先读懂项目骨架四大组件、页面流转与资源文件2.1 MainActivity与Fragment切换底部导航栏怎么搭很多新手拿到安卓项目第一反应是找MainActivity然后直接点运行发现界面跟自己想象的不一样。这套仿外卖App的入口是MainActivity但主界面一般是「底部导航 多个Fragment」的经典结构首页、菜单、订单、我的四个页签对应四个FragmentActivity只负责承载和切换。我一般会这样用FragmentTransaction做底部导航切换public class MainActivity extends AppCompatActivity { private HomeFragment homeFragment; private MenuFragment menuFragment; private OrderFragment orderFragment; private MineFragment mineFragment; private Fragment currentFragment; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); homeFragment new HomeFragment(); menuFragment new MenuFragment(); orderFragment new OrderFragment(); mineFragment new MineFragment(); switchFragment(homeFragment); } private void switchFragment(Fragment target) { if (currentFragment target) return; FragmentTransaction transaction getSupportFragmentManager().beginTransaction(); if (!target.isAdded()) { transaction.add(R.id.fragment_container, target); } else { transaction.show(target); } if (currentFragment ! null) { transaction.hide(currentFragment); } currentFragment target; transaction.commit(); } }这里有几个关键点。第一用hide/show而不是replace是为了保留Fragment实例状态避免每次切换都重新加载列表数据第二fragment_container是主布局里的FrameLayout在activity_main.xml里定义宽高都设成match_parent第三currentFragment初始值是null第一次调用switchFragment时hide(null)不会崩这是FragmentTransaction对空引用的天然容忍不用刻意判空。如果你在资源里看到的是RadioGroup RadioButton做底部导航也没问题那是更原始但期末作业更常见的写法。好处是不依赖Material库RadioButton的android:buttonnull配合drawable选择器就能做出选中变色效果坏处是RadioGroup的选中状态和Fragment切换状态容易不同步快速连点两个页签可能出现空白帧。所以我改造时倾向于用BottomNavigationViewmenu文件里定义四个item的id和icon代码里监听选中事件状态同步是控件自己保证的少操一份心。2.2 菜品列表页RecyclerView与Adapter的完整代码仿外卖App的核心页面是菜品列表一般长这样顶部店铺信息下面一列菜品卡片每行显示菜名、价格、月售和「加入购物车」按钮。列表用RecyclerView实现数据模型叫Dish字段包括name、price、imageRes、monthSales。先看Dish.javapublic class Dish { private String name; private double price; private int imageRes; // 本地图片资源ID private int monthSales; // 月售份数 public Dish(String name, double price, int imageRes, int monthSales) { this.name name; this.price price; this.imageRes imageRes; this.monthSales monthSales; } public String getName() { return name; } public double getPrice() { return price; } public int getImageRes() { return imageRes; } public int getMonthSales() { return monthSales; } }然后是决定列表能不能正常显示的DishAdapterpublic class DishAdapter extends RecyclerView.AdapterDishAdapter.ViewHolder { private ListDish dishList; private OnItemClickListener listener; public DishAdapter(ListDish dishList) { // 防止外部传入null导致崩溃 this.dishList dishList ! null ? dishList : new ArrayList(); } public void setOnItemClickListener(OnItemClickListener listener) { this.listener listener; } Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { // false: 先不挂载交给RecyclerView统一管理 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 dishList.get(position); holder.tvName.setText(dish.getName()); holder.tvMonthSales.setText(月售 dish.getMonthSales() 份); holder.tvPrice.setText(¥ dish.getPrice()); // 本地资源ID加载不涉及异步图片请求 holder.ivImage.setImageResource(dish.getImageRes()); holder.itemView.setOnClickListener(v - { if (listener ! null) { listener.onItemClick(dish); } }); } Override public int getItemCount() { return dishList.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView tvName, tvMonthSales, tvPrice; ImageView ivImage; ViewHolder(View itemView) { super(itemView); tvName itemView.findViewById(R.id.tv_name); tvMonthSales itemView.findViewById(R.id.tv_month_sales); tvPrice itemView.findViewById(R.id.tv_price); ivImage itemView.findViewById(R.id.iv_image); } } public interface OnItemClickListener { void onItemClick(Dish dish); } }几个参数值得注意。LayoutInflater.inflate的第三个参数传false意思是先不添加到父容器避免条目重复挂载holder.itemView是条目根布局点击事件绑在整条上这样老师问「列表能不能点击跳详情」你能答上来图片用setImageResource因为资源包里的图是本地drawable不需要Glide这也是「简单app」的定位顺便避开了网络图片加载的OOM。很多人在这步翻车findViewById的id写错或者item_dish.xml里控件类型不是TextView。运行后列表空白先看Logcat有没有NullPointerException十有八九是tv_month_sales这个id对不上。2.3 gradle配置与资源文件里那几个老手才注意的细节压缩包里应该有完整目录app/src/main/java、app/src/main/res、build.gradle、settings.gradle。期末作业最容易翻车的是build.gradle版本配置AS升级频繁Gradle版本和SDK版本对不上就同步失败。一个比较稳的组合是这样android { compileSdkVersion 33 defaultConfig { applicationId com.example.foodorder minSdkVersion 21 targetSdkVersion 33 versionCode 1 versionName 1.0 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.9.0 implementation androidx.recyclerview:recyclerview:1.3.0 }compileSdkVersion 33对应Android 13minSdkVersion 21覆盖Android 5.0以上设备期末演示的模拟器大概率落在这个范围targetSdkVersion 33让系统按Android 13的行为标准约束你比如分区存储、通知权限。有个典型坑如果把targetSdkVersion升到30以上旧代码里直接访问公共目录的写法会被拒报Permission denied。期末作业根本不需要写公共目录临时数据放SharedPreferences就够了具体方案第3章会讲。资源文件方面图片放drawable或mipmapmipmap放应用图标drawable放页面图片和选择器。很多模板的图标是.png直接拷进drawable的也可以换成VectorDrawable.xml矢量图体积小且缩放不模糊。如果改过ic_launcher记得AndroidManifest.xml里的android:icon要指向mipmap/ic_launcher否则应用没有图标桌面上显示默认机器人头答辩时很掉价。项目目录大致长这样拿到资源先对照一遍app/src/main/ ├── java/com/example/foodorder/ │ ├── MainActivity.java │ ├── model/Dish.java │ ├── adapter/DishAdapter.java │ ├── manager/CartManager.java │ └── fragment/ ├── res/ │ ├── layout/activity_main.xml │ ├── layout/item_dish.xml │ ├── drawable/ │ └── values/strings.xml └── AndroidManifest.xml3. 从点餐到下单购物车逻辑与数据持久化3.1 购物车数据结构为什么我选HashMap而不是List购物车是仿外卖App里唯一有点业务逻辑的地方也是答辩时老师最喜欢追问的模块。很多人第一反应是用ListDish存点一次加一个对象清空就clear()。但问题在于你怎么知道某个菜加了几份如果列表里有五份宫保鸡丁你总不能存五个对象然后挨个删。我用HashMapDish, Integer键是菜品引用值是数量。public class CartManager { private static volatile CartManager instance; private MapDish, Integer cartMap; private CartManager() { cartMap new HashMap(); } // 双重锁单例保证全局只有一份购物车 public static CartManager getInstance() { if (instance null) { synchronized (CartManager.class) { if (instance null) { instance new CartManager(); } } } return instance; } public void add(Dish dish) { int count cartMap.containsKey(dish) ? cartMap.get(dish) : 0; cartMap.put(dish, count 1); } public void reduce(Dish dish) { if (!cartMap.containsKey(dish)) return; int count cartMap.get(dish); if (count 1) { cartMap.remove(dish); // 数量减到0直接移除 } else { cartMap.put(dish, count - 1); } } public int getCount(Dish dish) { return cartMap.containsKey(dish) ? cartMap.get(dish) : 0; } public double getTotalPrice() { double total 0; for (Map.EntryDish, Integer entry : cartMap.entrySet()) { total entry.getKey().getPrice() * entry.getValue(); } return total; } public MapDish, Integer getCartMap() { return cartMap; } }这个单例用了双重锁保证并发环境下只创建一次实例。add里containsKey判断后再取值比get返回null再处理更直观reduce里数量减到0直接remove避免脏数据残留。getTotalPrice遍历整个Map算总额注意这里用double会有精度隐患下一节专门说。这套设计在答辩时很好讲单例保证全局一份购物车、Map保证菜品唯一性、Integer字段对应加减按钮的数量状态。老师追问「为什么不用List」你就答「List无法表达同一菜品加多份的数量语义」直接命中。3.2 下单页金额计算double精度坑与格式化如果购物车里价格直接相加会出现¥29.999999999这种尴尬数字。原因是浮点数用二进制表示十进制小数有精度误差。外卖App的金额建议用BigDecimal结算或者用整数「分」存储。public String formatPrice(double price) { BigDecimal bd BigDecimal.valueOf(price); bd bd.setScale(2, RoundingMode.HALF_UP); return ¥ bd.toPlainString(); }更稳妥的做法是让CartManager直接返回BigDecimalpublic BigDecimal getTotalPriceBig() { BigDecimal total BigDecimal.ZERO; for (Map.EntryDish, Integer entry : cartMap.entrySet()) { BigDecimal itemPrice BigDecimal.valueOf(entry.getKey().getPrice()); BigDecimal itemCount BigDecimal.valueOf(entry.getValue()); total total.add(itemPrice.multiply(itemCount)); } return total.setScale(2, RoundingMode.HALF_UP); }两个细节。第一BigDecimal.valueOf(double)比new BigDecimal(double)安全后者保留了浮点数的完整二进制值会生成一长串尾数第二setScale(2, RoundingMode.HALF_UP)是四舍五入到小数点后两位正好对应人民币的「分」。如果数据模型里价格直接存int单位是分那根本没有精度问题total priceFen * count后再转字符串插小数点就行。答辩时跟老师说「我用整数分计算避免浮点丢精度」绝对加分。下单页还有一个常见问题「加入购物车」按钮的数字角标。在DishAdapter里维护一个MapDish, Integer实时刷新角标或者在Activity里注册一个回调接口每次add或reduce后重刷列表。资源包里的简单做法是直接notifyDataSetChanged()数据量小不会卡顿期末场景完全够用。3.3 SharedPreferences持久化重启App不丢单内存里的Map在App被杀掉就没了。期末作业不要求做数据库但「重启后购物车还在」这种功能做好了能跟水作业拉开差距。最轻量的方案是SharedPreferences把购物车转成JSON字符串存起来下次启动再解析回来。public void saveCart(Context context) { JSONArray jsonArray new JSONArray(); for (Map.EntryDish, Integer entry : cartMap.entrySet()) { try { JSONObject obj new JSONObject(); obj.put(name, entry.getKey().getName()); obj.put(price, entry.getKey().getPrice()); obj.put(count, entry.getValue()); jsonArray.put(obj); } catch (JSONException e) { e.printStackTrace(); } } SharedPreferences sp context.getSharedPreferences(cart_data, Context.MODE_PRIVATE); sp.edit().putString(cart_json, jsonArray.toString()).apply(); }对应的读取public void loadCart(Context context) { SharedPreferences sp context.getSharedPreferences(cart_data, Context.MODE_PRIVATE); String json sp.getString(cart_json, ); if (json.isEmpty()) return; try { JSONArray jsonArray new JSONArray(json); for (int i 0; i jsonArray.length(); i) { JSONObject obj jsonArray.getJSONObject(i); String name obj.getString(name); double price obj.getDouble(price); int count obj.getInt(count); // 关键从全局菜品库按名称找回同一个Dish对象 Dish dish dishRepository.findByName(name); if (dish ! null) { cartMap.put(dish, count); } } } catch (JSONException e) { e.printStackTrace(); } }容易被忽略的点SharedPreferences适合存小数据购物车最多几十条没问题但如果你把整个菜单几百个菜塞进去启动会卡。另外apply()是异步写入commit()是同步写入期末场景用apply足够不挡UI线程。最隐性的问题是注释里那行——持久化时存的是菜名恢复时必须从全局菜品库里找回同一个Dish对象否则CartManager里存了「同名但不同引用」的对象后续加减操作匹配不上列表勾选状态不同步。这个坑我第一次写的时候踩了折腾了半个下午才想明白。调用时机放在onStop和onCreateOverride protected void onStop() { super.onStop(); CartManager.getInstance().saveCart(this); } Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); CartManager.getInstance().loadCart(this); }onStop在App进入后台或被杀前通常都会调用onCreate里恢复就能保证「下次打开还在」。如果你把保存逻辑写在onDestroy里经常会被系统跳过因为onDestroy不保证执行——这也是一个经典的玄学问题数据明明保存了为什么重启丢了一半。答案就是不要依赖onDestroy用onStop甚至onPause。4. 避坑与常见问题从导入到答辩的六个翻车现场4.1 Gradle同步失败仓库下架与阿里云镜像现象打开项目后Gradle一直转圈然后报Could not resolve com.android.support:appcompat-v7或Could not find com.google.android.material:material:1.4.0。原因Gradle依赖要从仓库拉取旧模板写的是jcenter()而JCenter已经停止服务另外google()仓库如果网络不稳定也会拉取失败。解决在settings.gradle和build.gradle里把仓库换成阿里云镜像repositories { google() maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/gradle-plugin } }settings.gradle里还要确认dependencyResolutionManagement配置没有禁用自定义仓库。新版AS默认在settings.gradle统一管理仓库加进去后重新Sync。如果还失败检查JDK版本——新版AS要求JDK 17老项目用JDK 8会报UnsupportedClassVersionError到File Project Structure SDK Location里指定JDK路径即可。4.2 模拟器请求本地服务器失败明文流量被拦现象App在模拟器里能打开但HttpURLConnection请求http://10.0.2.2:8080模拟器访问本机服务器的IP时抛CLEARTEXT communication to 10.0.2.2 not permitted by network security policy。原因Android 9API 28开始系统默认禁止明文HTTP流量只允许HTTPS。期末作业的本地接口基本都是HTTP必须显式放开。解决在AndroidManifest.xml的application标签里加一行application android:usesCleartextTraffictrue ...usesCleartextTraffictrue是全局放开适合开发场景如果只想对特定域名放开需要写network_security_config.xml但期末作业全局放开就够了。加了这行后记得Clean Project再重新运行别只点绿色的Run有时候资源没刷新白折腾。4.3 RecyclerView图片错乱复用机制与加载时序现象快速滑动菜品列表某个位置的图片变成上一个位置的或者闪现一下再变回来。原因RecyclerView会复用滑出屏幕的ViewHolderonBindViewHolder被回调时设置新图片的代码跑得比旧图片的异步加载慢于是旧图先显示在错误的位置上。如果代码里混了Glide、用了Drawable缓存、或者手动做AsyncTask加载就会踩到这个经典错乱问题。解决如果使用Glide在onBindViewHolder里先clear再loadGlide.with(holder.ivImage.getContext()) .load(dish.getImageUrl()) .apply(new RequestOptions().placeholder(R.drawable.ic_default)) .into(holder.ivImage);更干脆的方法给ImageView打Tag判断当前item的Tag和图片地址是否一致不一致就丢弃这次加载结果。本地图片场景下我直接在Adapter构造时把Drawable预加载一遍onBind直接setImageDrawable从根上避开线程问题。资源包里如果只有本地图把setImageResource换掉那行就够。4.4 Activity没注册导致闪退Manifest核查清单现象点击图标启动App闪一下直接回到桌面Logcat报java.lang.RuntimeException: Unable to start activity ... java.lang.ClassNotFoundException。原因新建的Activity或FragmentActivity忘了注册到AndroidManifest.xml特别是自己新增了「订单详情页」或「登录页」写了startActivity(new Intent(this, OrderDetailActivity.class))但Manifest里没有对应activity标签。解决检查Manifest典型清单长这样application android:labelstring/app_name ... activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity activity android:name.OrderDetailActivity / /applicationMainActivity必须带MAIN和LAUNCHER两个项这是启动入口其他页面只需要android:name。android:exportedtrue是API 31后的强制要求不写的话安装时直接报INSTALL_PARSE_FAILED_MANIFEST_MALFORMED。所以期末作业里每个Activity都补上android:exported省得不同系统版本之间翻车。4.5 真机调试连不上开发者选项与USB配置现象手机插上USB线AS里显示No devices或者一直停在Waiting for target device to come online。原因没开启开发者选项或者USB连接模式选了「仅充电」驱动也没装好。解决先到「设置 → 关于手机 → 连续点击版本号7次」打开开发者选项然后进「开发者选项」打开USB调试和USB安装插线后把USB模式切到「传输文件」手机弹「允许USB调试吗」对话框时点允许并勾选「始终允许」。最后在AS的Run下拉框选设备列表里还是没有就去SDK Manager SDK Tools勾选Google USB Driver装驱动。Windows上这个最常出问题macOS基本即插即用。连上后优先用真机演示而不是模拟器真机加载速度和触摸反馈都更稳定答辩时不会卡在模拟器启动上。4.6 targetSdkVersion升级存储权限翻车从读写公共目录到分区存储现象App里有一处订单保存功能把JSON文件写到/sdcard/Download/在targetSdkVersion 28的老项目里能直接写升级后运行时直接报Permission denied或者写入后文件管理器里看不到。原因Android 10API 29开始强制Scoped Storage分区存储App不能随意访问公共目录READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限形同虚设。解决期末作业根本不需要写公共目录。把临时订单数据存到getExternalFilesDir()或直接存SharedPreferences完全不需要存储权限。如果代码里确实有旧逻辑先批量改成// getExternalFilesDir: App专属外部目录免权限 File dir context.getExternalFilesDir(null); File file new File(dir, order_ System.currentTimeMillis() .json);以上六条避坑记录基本覆盖导入、网络、列表、清单、真机和存储。遇到其他问题流程都一样先看Logcat定位到哪一行抛异常再往上查调用链别急着整个项目重来。5. 改出差异感三个让老师眼前一亮的低成本增强5.1 AlertDialog清空购物车确认很多模板购物车页的「清空」按钮直接clear()看着很简陋。加一个确认弹窗交互完整度立刻不一样btnClearCart.setOnClickListener(v - { new AlertDialog.Builder(this) .setTitle(清空购物车) .setMessage(确定要把所有菜品移除吗) .setPositiveButton(再想想, null) .setNegativeButton(确定清空, (dialog, which) - { CartManager.getInstance().getCartMap().clear(); cartAdapter.notifyDataSetChanged(); tvTotalPrice.setText(¥0.00); }) .show(); });这里正负按钮的措辞是我故意反过来的正按钮「再想想」对应setPositiveButton实际语义是「不执行」负按钮「确定清空」才执行删除。这样把破坏性操作放在习惯的右侧提升操作安全。如果你不习惯这种正反颠倒也可以把正按钮写成「确定」负按钮写「取消」但务必保持语义清晰。答辩演示时弹窗一出来点头交互的完成度就立住了。5.2 用SharedPreferences记住登录态在登录页勾选「记住我」后把登录状态存进SharedPreferences下次打开App直接进首页。模拟器演示时老师不用每次等手动输入账号密码。// 登录成功时 SharedPreferences prefs getSharedPreferences(user_prefs, MODE_PRIVATE); prefs.edit().putBoolean(is_login, true).apply(); // 启动页/首页判断 boolean isLogin prefs.getBoolean(is_login, false); if (!isLogin) { startActivity(new Intent(this, LoginActivity.class)); finish(); }// 退出登录时 prefs.edit().putBoolean(is_login, false).apply();注意is_login只是方便演示的标志位千万别把密码明文存进去。期末答辩老师如果追问「用户密码怎么保存」你要回答「应该做加密或存Token」而不是「存在SharedPreferences里」。就算代码里确实只存了标志位也要说出安全设计思路这是加分项。5.3 空购物车占位图与Toast反馈把边界情况做完整界面里最常见的翻车是购物车空了页面还挤着一堆数据或者点「下单」没反应。一个完整的交互应该有边界反馈。购物车为空时显示空状态布局下单成功弹Toast提示「订单已提交」代码大概是public void updateCartUI() { int itemCount CartManager.getInstance().getCartMap().size(); if (itemCount 0) { llEmptyCart.setVisibility(View.VISIBLE); // 空态占位图文案 rvCart.setVisibility(View.GONE); btnCheckout.setEnabled(false); } else { llEmptyCart.setVisibility(View.GONE); rvCart.setVisibility(View.VISIBLE); btnCheckout.setEnabled(true); } }btnCheckout.setOnClickListener(v - { if (CartManager.getInstance().getCartMap().isEmpty()) { Toast.makeText(this, 购物车还是空的, Toast.LENGTH_SHORT).show(); return; } // 这里拼接订单摘要并提交 Toast.makeText(this, 订单已提交预计30分钟送达, Toast.LENGTH_SHORT).show(); });这套逻辑加起来不超过三十行但把「空态」「可用性」「成功反馈」全部补齐。期末作业的评分维度经常有一条「交互完整性」这三十行就是拿满分的入场券。顺带在item_dish.xml里把「加入购物车」按钮的id暴露出来在Adapter里单独给它绑点击事件这样itemView的点击可以留给「跳转详情」互不干扰。5.4 启动页SplashScreen三行代码撑起App门面启动页是很多模板没有的加一个成本极低观感提升明显。不需要第三方库一个Handler就能实现public class SplashActivity extends AppCompatActivity { Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_splash); new Handler(Looper.getMainLooper()).postDelayed(() - { startActivity(new Intent(this, MainActivity.class)); finish(); }, 2000); // 延迟2秒进入主界面 } }记得在AndroidManifest.xml里把启动入口从MainActivity换成SplashActivity并把SplashActivity的theme设置成全屏主题避免启动时露出默认标题栏。postDelayed的时长建议1.5到2秒太短看不清logo太长让人烦躁。这个增强单独拿出来讲老师会觉得你连启动体验都考虑到了。6. 答辩前必做的三件事从全流程跑通到演示录像先说第一件把完整流程在真机或模拟器上通跑三遍以上。流程是启动App → 首页加载 → 进入菜单列表 → 添加三个菜到购物车 → 减一个菜 → 打开购物车 → 清空 → 重新加菜 → 下单 → 弹出成功Toast → 关闭App重开 → 购物车恢复。这十一小步每一步都要有视觉反馈任何一步卡住就在答辩前改完不要在答辩现场调试现场调试等于把打分权交给运气。第二件把核心代码读熟。老师不会问每一行但一定会问「列表用的什么控件」或者「购物车怎么存数据的」。你要能指着DishAdapter.java说出onCreateViewHolder创建视图、onBindViewHolder绑定数据、getItemCount返回数量。背不出源码没关系把流程理顺画出「Activity → Fragment → Adapter → 数据模型」的调用关系问答环节就不会冷场。第三件录一份演示视频。用手机自带录屏或AS模拟器的Record Screen按钮把上面十一小步操作录下来同时配音说关键操作。这样即使答辩时电脑连不上投影、模拟器起不来你也可以直接播放录像。这招我从一次现场翻车中学到——那次老师电脑没有HDMI转接头我的项目完全没演示成全靠提前录好的视频过了关。从那以后我每次提交安卓期末作业都强制走一遍通跑流程 → 读熟核心代码 → 录演示视频三件事一个不能少。希望帮到你。本文还有配套的精品资源点击获取