news 2026/10/7 10:59:26

仿外卖App安卓期末项目:从架构到避坑的完整改造指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
仿外卖App安卓期末项目:从架构到避坑的完整改造指南

简介:面向安卓初学者的期末大作业参考项目,以仿外卖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」的经典结构,首页、菜单、订单、我的四个页签对应四个Fragment,Activity只负责承载和切换。

我一般会这样用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:button="@null"配合drawable选择器就能做出选中变色效果;坏处是RadioGroup的选中状态和Fragment切换状态容易不同步,快速连点两个页签可能出现空白帧。所以我改造时倾向于用BottomNavigationView,menu文件里定义四个item的id和icon,代码里监听选中事件,状态同步是控件自己保证的,少操一份心。

2.2 菜品列表页:RecyclerView与Adapter的完整代码

仿外卖App的核心页面是菜品列表,一般长这样:顶部店铺信息,下面一列菜品卡片,每行显示菜名、价格、月售和「加入购物车」按钮。列表用RecyclerView实现,数据模型叫Dish,字段包括name、price、imageRes、monthSales。

先看Dish.java:

public 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; } }

然后是决定列表能不能正常显示的DishAdapter:

public class DishAdapter extends RecyclerView.Adapter<DishAdapter.ViewHolder> { private List<Dish> dishList; private OnItemClickListener listener; public DishAdapter(List<Dish> 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 13,minSdkVersion 21覆盖Android 5.0以上设备,期末演示的模拟器大概率落在这个范围;targetSdkVersion 33让系统按Android 13的行为标准约束你,比如分区存储、通知权限。有个典型坑:如果把targetSdkVersion升到30以上,旧代码里直接访问公共目录的写法会被拒,报Permission denied。期末作业根本不需要写公共目录,临时数据放SharedPreferences就够了,具体方案第3章会讲。

资源文件方面,图片放drawable或mipmap,mipmap放应用图标,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.xml

3. 从点餐到下单:购物车逻辑与数据持久化

3.1 购物车数据结构:为什么我选HashMap而不是List

购物车是仿外卖App里唯一有点业务逻辑的地方,也是答辩时老师最喜欢追问的模块。很多人第一反应是用List<Dish>存,点一次加一个对象,清空就clear()。但问题在于:你怎么知道某个菜加了几份?如果列表里有五份宫保鸡丁,你总不能存五个对象然后挨个删。我用HashMap<Dish, Integer>,键是菜品引用,值是数量。

public class CartManager { private static volatile CartManager instance; private Map<Dish, 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.Entry<Dish, Integer> entry : cartMap.entrySet()) { total += entry.getKey().getPrice() * entry.getValue(); } return total; } public Map<Dish, 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直接返回BigDecimal:

public BigDecimal getTotalPriceBig() { BigDecimal total = BigDecimal.ZERO; for (Map.Entry<Dish, 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里维护一个Map<Dish, Integer>实时刷新角标,或者在Activity里注册一个回调接口,每次add或reduce后重刷列表。资源包里的简单做法是直接notifyDataSetChanged(),数据量小,不会卡顿,期末场景完全够用。

3.3 SharedPreferences持久化:重启App不丢单

内存里的Map在App被杀掉就没了。期末作业不要求做数据库,但「重启后购物车还在」这种功能,做好了能跟水作业拉开差距。最轻量的方案是SharedPreferences,把购物车转成JSON字符串存起来,下次启动再解析回来。

public void saveCart(Context context) { JSONArray jsonArray = new JSONArray(); for (Map.Entry<Dish, 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和onCreate:

@Override 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 9(API 28)开始,系统默认禁止明文HTTP流量,只允许HTTPS。期末作业的本地接口基本都是HTTP,必须显式放开。

解决:在AndroidManifest.xml的<application>标签里加一行:

<application android:usesCleartextTraffic="true" ...>

usesCleartextTraffic="true"是全局放开,适合开发场景;如果只想对特定域名放开,需要写network_security_config.xml,但期末作业全局放开就够了。加了这行后记得Clean Project再重新运行,别只点绿色的Run,有时候资源没刷新,白折腾。

4.3 RecyclerView图片错乱:复用机制与加载时序

现象:快速滑动菜品列表,某个位置的图片变成上一个位置的,或者闪现一下再变回来。

原因:RecyclerView会复用滑出屏幕的ViewHolder,onBindViewHolder被回调时,设置新图片的代码跑得比旧图片的异步加载慢,于是旧图先显示在错误的位置上。如果代码里混了Glide、用了Drawable缓存、或者手动做AsyncTask加载,就会踩到这个经典错乱问题。

解决:如果使用Glide,在onBindViewHolder里先clear再load:

Glide.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:label="@string/app_name" ...> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name=".OrderDetailActivity" /> </application>

MainActivity必须带MAIN和LAUNCHER两个项,这是启动入口;其他页面只需要android:name。android:exported="true"是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 10(API 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转接头,我的项目完全没演示成,全靠提前录好的视频过了关。从那以后我每次提交安卓期末作业都强制走一遍:通跑流程 → 读熟核心代码 → 录演示视频,三件事一个不能少。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 10:57:49

DDR5输入测量标准JESD79-5第8章深度解析:AC/DC Level本质与工程落地

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 10:56:20

LAMMPS avechunk大文件高效解析与科研绘图实战

简介&#xff1a;本资源是一套专为材料科学与计算物理研究者设计的Lammps分子动力学大数据后处理与科研绘图自动化工具集&#xff0c;面向需高效处理超10GB avechunk输出文件的科研人员&#xff0c;显著缓解传统工具在加载、切分与可视化大尺寸轨迹数据时的性能瓶颈。压缩包共3…

作者头像 李华
网站建设 2026/10/7 10:56:01

第092篇 协程异常处理:从 try-catch 到 CoroutineExceptionHandler

协程里最贵的 bug 不是崩溃,是"静默地不干活"。上一节把 Job 的取消传播讲清了,本节顺着异常这条线往下走:异常从协程抛出后经过哪些环节、到哪儿被接住、CancellationException 为什么要特殊对待、以及 CoroutineExceptionHandler 究竟在什么位置生效。这题在面试…

作者头像 李华
网站建设 2026/10/7 10:55:41

Altium Designer网络管理器:PCB后期GND/VCC批量优化实战

1. 为什么网络管理器是PCB后期优化的隐藏利器画过几块板子的人大概都有这种体会&#xff1a;原理图阶段网络命名随意&#xff0c;GND写成GND_1、GND_2&#xff0c;VCC分成VCC_3V3、VCC_5V、VCC_MAIN&#xff0c;等到PCB布局布线快收尾了&#xff0c;突然发现电源网络需要统一加…

作者头像 李华
网站建设 2026/10/7 10:55:07

两阶段鲁棒优化在多微网容量配置与联合调度中的建模与仿真

多微网联合调度这两年确实是个热门方向&#xff0c;但真正能把“两阶段鲁棒优化”和“容量配置”串起来做仿真的人并不算多。原因倒也不复杂&#xff0c;这类问题涉及的不确定集构建、min-max-min三层结构求解、以及投资层与运行层的变量耦合&#xff0c;每一个环节都足够劝退一…

作者头像 李华
网站建设 2026/10/7 10:54:37

流处理工程化实战:从Flink与Spark Streaming到窗口状态管理

打开招聘网站搜大数据岗位&#xff0c;十个里有八个要求熟悉流处理框架&#xff0c;不管你是做平台开发、数据清洗还是实时大屏&#xff0c;绕不开Spark Streaming或者Flink。但很多人学流处理是割裂的——今天看个Kafka入门的博客&#xff0c;明天抄一段WordCount代码&#xf…

作者头像 李华