尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android Studio练手项目:从零搭建点菜系统全流程解析

Android Studio练手项目:从零搭建点菜系统全流程解析 要说Android Studio练手项目点菜系统绝对是我见过最实在的选择之一。我最初接触这个项目的时候学完Android基础语法正处在一个尴尬阶段——笔记看了不少小demo也写了几个可一旦要自己从零搭一个完整App脑子里还是一团浆糊。后来把点菜系统完整做下来一遍整个Android开发的脉络一下子通了这才明白为什么那么多培训机构会把点餐/点菜当成结课项目。这个系统的妙处在于它刚好踩在入门和真实项目的分界线上。登录、菜品列表、购物车、下单、订单管理——功能看起来不少但每块逻辑的边界都足够清晰。做的时候你可以只用一个Activity加几个布局硬怼也可以按标准MVP/MVC结构分层丰俭由人。更重要的是整套流程覆盖了Android开发日常最常打交道的那些能力SQLite数据库、RecyclerView列表、页面跳转与数据回传、状态持久化。跑通它你就把Android App从写代码到装进手机的完整链路走了一遍。下面我把这个项目从设计到打包的完整思路拆开讲包括我实际开发中踩过的坑和最后补上的细节希望能给正在找项目练手的朋友一些能直接用的参考。1. 点菜系统的项目边界为什么这样设计最合适1.1 功能清单与开发技能对照开始写代码之前搞清楚这个系统到底要做什么、每个功能对应Android开发的哪个知识点比急着敲键盘重要得多。我把整个项目拆成五个模块每个模块都有明确的学习目标。登录/用户模块练SharedPreferences轻量存储和Intent页面跳转。这里不用真的做注册流程写死一个默认账号就行重点是学会把用户状态保存下来这个思路以后做记住密码自动登录都是同一套逻辑。菜品列表模块练RecyclerView Adapter的完整使用流程以及ImageView加载本地图片资源。菜品一般分凉菜、热菜、主食、汤类几个分类很多人会在这里加一个分类筛选这时可以顺手练一下Fragment ViewPager2不过初版不建议加列表能正常滑动、点击没Bug就够了。购物车模块练数据状态管理。选完菜之后购物车的增删改查、数量加减、总价实时刷新这一块是整个项目里最容易出Bug的地方但也是最有价值的部分。订单模块练SQLite数据库增删改查。下单之后订单存进本地数据库历史订单列表从库里读出来展示订单状态在待上菜—制作中—已完成之间流转。结算模块练金额计算和数据传递。购物车总价汇总、单个菜品小计、下单时把购物车数据整体传到订单确认页这里会用到BigDecimal处理金额也会用到Serializable或者Bundle传对象。1.2 为什么选择本地无服务端架构很多人看到成品项目四个字第一反应是这不就是没后台的阉割版吗说实话初版点菜系统我确实不建议引入后端。原因是初学者在服务端上踩的坑远比功能本身多。网上那些成品点菜系统项目大多数是纯本地架构Android Studio SQLite ListView/RecyclerView。这个设计不是偷懒而是刻意选择。它的好处很直接拿到项目就能跑不依赖服务器、不需要配环境网络请求、JSON解析、接口联调这些后端知识完全不涉及。学习曲线被压得非常平缓你可以把全部注意力放在界面和本地的数据流上。我用过一次带Bmob后端云的点菜项目结果半天时间花在初始化SDK、处理回调线程、适配不同机型网络权限上真正的点菜逻辑反而没写几行。那之后我就明白了学习型项目的第一目标是跑通而不是完整。等本地版做熟了再往上面接一套Spring Boot后端、把SQLite换成MySQL那是从会做App到会做产品的下一步。顺带一提本地架构还有个好处装到手机上展示的时候完全不受网络状态影响拿到餐馆现场演示也稳。很多毕业设计答辩现场翻车一半是网络问题一半是模拟器性能问题纯本地版可以一次性避开前者。2. 项目结构拆解从目录设计到数据表设计2.1 目录结构怎么组织才能不粘在一起我第一次写这个项目的时候所有类都堆在一个包下面MainActivity三千行改一个Bug翻半天。第二次重写就按界面层—适配层—数据层—模型层四层拆开了清爽很多。下面这个目录结构可以直接照抄OrderSystem/ ├── app/ │ ├── src/main/ │ │ ├── java/com/example/ordersystem/ │ │ │ ├── activity/ # 界面层每个页面一个Activity │ │ │ │ ├── LoginActivity.java │ │ │ │ ├── MainActivity.java │ │ │ │ ├── MenuActivity.java │ │ │ │ ├── CartActivity.java │ │ │ │ ├── OrderConfirmActivity.java │ │ │ │ └── OrderHistoryActivity.java │ │ │ ├── adapter/ # RecyclerView的Adapter │ │ │ │ ├── MenuAdapter.java │ │ │ │ └── CartAdapter.java │ │ │ ├── model/ # 实体类 │ │ │ │ ├── FoodItem.java │ │ │ │ ├── CartItem.java │ │ │ │ └── OrderBean.java │ │ │ ├── db/ # 数据库操作 │ │ │ │ ├── DBHelper.java │ │ │ │ ├── FoodDao.java │ │ │ │ └── OrderDao.java │ │ │ └── utils/ # 金额计算等工具类 │ │ │ └── PriceUtils.java │ │ ├── res/ │ │ │ ├── layout/ │ │ │ ├── drawable/ # 菜品图片和按钮背景 │ │ │ └── values/ │ └── build.gradle这个结构的好处是界面层只负责展示和点击事件所有数据操作都通过Activity调DAO去完成。比如MenuActivity里点加购按钮就是FoodDao.addToCart(food)页面本身不需要知道数据库是怎么写的。后期想加网络请求只需要把DAO里的实现换成HTTP调用界面完全不用动。这就是分层最直接的收益也是判断一个项目写得好不好的初筛标准。2.2 三张表搞定所有数据数据库设计是点菜系统的地基。我用的SQLite表结构设计成三张够用且清晰。很多成品项目的核心也是这三张表。表名字段说明food菜品表id, name, price, category, image_res, salesprice用REAL类型存以元为单位的价格image_res存的是R.drawable.food1这种资源ID的int值sales记录销量cart购物车表id, food_id, food_name, price, count, total_price购物车本质上就是个临时订单total_price其实是price * count的冗余字段查询时省一次乘法运算orders订单表id, table_no, total_price, create_time, statusstatus用int存0待上菜、1制作中、2已完成再补充一张order_detail明细表的话就是四张用来存一个订单下多个菜品的快照。注意这里有个关键点订单明细里必须冗余一份food_name和price不能只存food_id。因为菜品表以后可能会改价格而历史订单应该永远显示下单时的价格。这就是典型的快照设计思路做电商类本地App时都会遇到。表与表之间的关系不复杂cart表独立于orders表存在下单成功时把cart表里的数据复制到order_detail然后清空cart。sqlite建表语句类似这样CREATE TABLE food ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, price REAL NOT NULL, category TEXT, image_res INTEGER, sales INTEGER DEFAULT 0 );菜品初始数据我建议直接在DBHelper的onCreate里用insert语句写死十几道菜而不是做一个导入功能。虽然写死数据显得不太灵活但对新手来说却是最稳的方案——不用处理Excel解析、不用管Assets目录文件读取数据库一创建就有数据可用。3. 核心功能实现思路主要逻辑和几个关键点3.1 菜品列表加载与购物车同步菜品列表页是整个App的门面。用RecyclerView展示food表里的数据Adapter的ViewHolder持有菜品名、价格、图片、加入购物车按钮。列表本身没什么难度真正的难点在于购物车的数量和总价如何跨页面保持一致。我踩过的坑是MainActivity里改了购物车回到MenuActivity时列表上的角标数字还是旧的。解决办法是在Activity的onResume()里重新查一次购物车总数量刷新角标。不要用静态变量存购物车数据没有意义——App进程被系统杀掉之后静态变量就没了数据还是要从SQLite读。再讲一个高频操作加入购物车。很多人会先查cart表里有没有这道菜有就update count没有才insert。这样做逻辑没错但代码写起来啰嗦。Android的SQLiteDatabase提供了一个insertWithOnConflict方法配合CONFLICT_REPLACE一次调用就能解决去重问题——前提是给food_id字段加UNIQUE约束第二次插入同一道菜时自动覆盖旧记录。public void addToCart(FoodItem food, int count) { SQLiteDatabase db dbHelper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(food_id, food.getId()); values.put(food_name, food.getName()); values.put(price, food.getPrice()); values.put(count, count); db.insertWithOnConflict(cart, null, values, SQLiteDatabase.CONFLICT_REPLACE); }这里又涉及到钱的计算。菜品价格在food表里存的是REAL类型也就是float但真正做金额计算时千万不要直接用float或double相加。0.1 0.2在double里不是0.3而是0.30000000000000004这在支付场景是不可接受的。我封装了一个PriceUtils统一用BigDecimal处理public static BigDecimal totalPrice(ListCartItem items) { BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { BigDecimal price BigDecimal.valueOf(item.getPrice()); BigDecimal count BigDecimal.valueOf(item.getCount()); total total.add(price.multiply(count)); } return total.setScale(2, RoundingMode.HALF_UP); }BigDecimal的setScale(2, HALF_UP)就是保留两位小数、四舍五入的意思。很多人图省事直接Math.round()但在多菜品累加时误差会被放大所以这个工具类值得一写。3.2 下单流程与订单状态流转下单按钮点击后的流程是取出cart表所有数据 → 计算总价 → 生成orders记录 → 把购物车明细复制到order_detail → 清空cart → 跳转到订单详情/历史订单页。这里有一个很容易被忽略的点订单状态是动态变化的不能只靠界面上的按钮来维护。我用的方式是状态机思想——订单状态只在固定的几个值之间流转不允许随意跳转。待上菜0刚下单成功此时可以进行标记为制作中的操作。制作中1后厨开始做菜此时可以进行标记为已完成的操作。已完成2终态不能再做任何状态变更。对应的Java写法是给OrderBean提供一个updateStatus(int newStatus)方法里面用switch判断当前状态和下一状态的合法性public boolean transitionTo(int target) { switch (this.status) { case 0: if (target 1) { this.status 1; return true; } break; case 1: if (target 2) { this.status 2; return true; } break; case 2: return false; } return false; }这种写法虽然简单但能防止很多低级Bug——比如用户从已完成的订单里再次点下单或者重复提交订单。别小看这种状态管理等到你以后写电商订单、预约系统全是同一个套路。我把这个状态机写进点菜系统里后来做后台管理系统时直接照搬了这个思路。另外下单成功后清空购物车这个动作我用了一个事务来保证原子性。SQLite的事务用法很简单db.beginTransaction(); try { // 插入订单 // 复制明细 // 清空购物车 db.setTransactionSuccessful(); } finally { db.endTransaction(); }事务的意义在于如果复制明细这步失败了插入订单和清空购物车也不会发生。否则会出现订单已经生成了、但明细是空的这种脏数据。自己本地单机用好像没什么但这是写生产级代码的底线要求习惯要早养。4. 从Studio项目到手机上的成品APK编译与项目移植实操4.1 生成APK包debug签名和release签名的区别项目写完只是一个开始最终要变成一个能在手机上安装的APK。Android Studio打包有两种路径很多人第一次操作时分不清。第一种是快速打包。菜单栏点 Build → Build Bundle(s) / APK(s) → Build APKAndroid Studio会自动完成编译、资源合并、签名过程生成一个debug包。这个包使用的签名是默认的debug.keystore适合开发阶段自己装到手机上测试。缺点也很明显debug包在正式发布场景会弹风险提示而且部分市场平台不认debug签名。第二种是正式签名打包。Build → Generate Signed Bundle / APK → 选APK需要先创建一个KeyStore.jks签名文件。创建过程中要填一些组织信息这些都会写进签名文件里但注意签名文件的密码一定要记牢后续版本更新还得用同一个签名。在Android平台上签名就是应用的身份证签名不同系统会认为这是两个完全不同的App不能直接覆盖安装。签名选择上还有v1和v2两个方案。Android 7.0及以上推荐用v2签名它校验更快也更安全。但如果你要兼容Android 5.x、6.x的旧设备就需要勾选v1方案一起签。我用表格整理一下判断逻辑场景签名方案自用测试装自己手机Build APK的debug包即可发给朋友安装覆盖Android 5.0勾选v1v2上架应用商店Android 7.0为目标纯v2即可部分旧商店可能要求v14.2 移植成品项目的经典问题与解决链路热搜词里移植android studio项目这条我特别有共鸣。很多找我拿代码的同学第一步就卡在Gradle同步上。拿到一个别人的Android Studio项目打开后转圈半天然后报错Gradle sync failed。这个问题的根源通常不是代码而是环境版本不匹配。最常见的三个坑逐一排查第一Gradle版本不对。别人用的Gradle 7.5你本地的AS默认Gradle版本是8.2两个版本对AGPAndroid Gradle Plugin版本的要求不同。打开项目根目录的build.gradle文件看classpath com.android.tools.build:gradle:版本号这一行。如果版本差异大就在File → Project Structure → Project里改对应版本然后重新同步。第二SDK或Build Tools版本缺失。项目里指定的compileSdkVersion如果比你本地安装的SDK版本高AS会尝试自动下载但下载失败的概率不小。遇到这种情况先打开SDK Manager确认本地装了什么版本的SDK再回build.gradle里把compileSdkVersion和targetSdkVersion改成已安装的版本。注意targetSdkVersion降级要谨慎会影响部分系统行为差异。第三依赖库下载超时。国内网络环境从Google的Maven仓库拉依赖库经常超时Gradle sync卡在Downloading...很久然后失败。解决方法是换镜像仓库。在项目根级的build.gradle里加上阿里云的Maven仓库buildscript { repositories { maven { url https://maven.aliyun.com/repository/public } maven { url https://maven.aliyun.com/repository/google } google() mavenCentral() } }我遇到过最刁钻的情况是整个项目在别人的电脑上能跑我这边怎么同步都失败最后发现是项目里用了某个库的旧版本在新版的Gradle下编译一直报错。处理方式是全局搜索implementation依赖把明显过时的库换成AndroidX对应的新版本。AndroidX和旧版support库共存也容易出问题尽量统一用其中一个。移植完成后如果出现代码里飘红但编译能过的情况通常只是IDE索引问题Clean Project Rebuild Project一般能搞定。5. 开发中绕不开的几个实际问题模拟器、返回键与存储权限5.1 模拟器起不来怎么办我最初用Android Studio自带的模拟器调试点菜系统结果AVD启动时报错HAXM installation failed或者Intel HAXM is not installed。这台电脑明明是Intel处理器为什么装不了原因多半是BIOS里虚拟化技术VT-x没开。排查链路就是先看任务管理器 → 性能 → CPU确认虚拟化是否显示已启用。如果显示未启用重启电脑进BIOS找到Intel Virtualization Technology改成Enabled。改完再进AS启动模拟器就正常了。如果不想折腾BIOS还有两个方案一是用Android Studio自带的Device Manager创建模拟器时选ARM架构镜像但运行速度明显慢二是干脆用真机调试用USB线连接手机打开开发者选项和USB调试AS会自动识别设备。点菜系统这种小项目用真机调试的速度体验比模拟器好太多。顺便说一句如果模拟器在Windows上特别卡检查一下是否开了Windows Hyper-VHyper-V和HAXM有时会冲突。5.2 onBackPressed失效新API的适配问题点菜系统的订单确认页我要求用户点返回键时弹一个确认对话框确认放弃本次点菜吗而不是直接退出。最初我用的还是传统写法重写Activity的onBackPressed()Override public void onBackPressed() { new AlertDialog.Builder(this) .setMessage(确认放弃本次点菜吗) ... .show(); }这段代码在旧设备上没问题但在targetSdk 33及以上编译时会发现方法上多了个Deprecated标记而且在部分Android 13设备上完全不生效。根本原因是9.0之后的系统对返回键的处理从Activity方法迁移到了OnBackPressedDispatcher回调。新写法是这样OnBackPressedCallback callback new OnBackPressedCallback(true) { Override public void handleOnBackPressed() { new AlertDialog.Builder(MainActivity.this) .setTitle(提示) .setMessage(确认放弃本次点菜吗) .setPositiveButton(确定, (dialog, which) - { finish(); }) .setNegativeButton(取消, null) .show(); } }; getOnBackPressedDispatcher().addCallback(this, callback);这个改动本质上是把返回键行为从重写变成了注册回调好处是多个页面可以各自注册消费返回事件系统按注册顺序分发。如果你接手的老项目里有大量onBackPressed重写代码建议统一迁移到新回调模式否则随着targetSdkVersion升高这部分逻辑会逐渐失效。5.3 存储权限为什么不能直接读SD卡根目录如果你给点菜系统加了从相册选菜品图片的功能就会遇到存储权限问题。Android 4.4之后系统限制了对SD卡根目录的直接写入Android 6.0引入了运行时权限需要在代码里主动申请到Android 10进一步收紧公开目录要申请MANAGE_EXTERNAL_STORAGE权限而且这权限在应用商店审核时会被卡。我自己的做法是点菜系统里的图片全部走drawable资源不碰SD卡。如果非要处理外部文件正确姿势是使用系统提供的StorageManager或SAFStorage Access Framework让用户通过系统文件选择器选文件App拿到Uri之后通过ContentResolver读取。这比直接拼文件路径去/sdcard/DCIM/...里找文件要稳定得多因为Android各版本的公开目录概念本身就一直在变拼路径的方案迟早会翻车。热搜词里还有一条android studio拿到uri怎样读取其下文件说的就是这个场景。通用流程是Intent.ACTION_OPEN_DOCUMENT打开文件选择器 → 回调里拿到Uri →contentResolver.openInputStream(uri)得到输入流 → 读取或拷贝。用这个流程拿到的Uri授权范围限定在你选中的那个文件不需要申请整个SD卡的读写权限。6. 一个小扩展基于这个项目还能继续加什么最后分享一个我实际改过的升级思路给菜品列表加一个销量排序按钮并且把搜索框接进来。改动点主要在两个地方。第一给菜品表加一个按销量排序的查询方法ORDER BY sales DESC然后RecyclerView换一下数据源第二搜索用模糊匹配SQL语句写成WHERE name LIKE % || ? || %用参数占位符而不是字符串拼接避免拼接出错。public ListFoodItem getFoodsByKeyword(String keyword) { SQLiteDatabase db dbHelper.getReadableDatabase(); String sql SELECT * FROM food WHERE name LIKE ? ORDER BY sales DESC; Cursor cursor db.rawQuery(sql, new String[]{% keyword %}); // 遍历Cursor封装成List }这个功能加下来大概需要半天时间但它把点击事件防抖连续点击搜索按钮导致数据库多次查询、输入框监听器TextWatcher、列表刷新都串起来了做完你会发现自己对Activity生命周期和数据刷新的理解又深了一层。再往外扩展就是给订单加一个桌号选择Preference存储上次选择的桌号、加今日营业额统计用SQLite的SUM聚合函数、或者用Chart库把销量做成柱状图。每一步都不难但每加一步你对这套项目架构的掌控力就会强一分。这也是为什么我始终建议练项目不要只跑通一遍就扔要把它当成一块可以不停加功能的画布。照着这个思路把点菜系统完整写一遍你会发现自己对Android Studio的熟练度会上一个大台阶——哪怕只是理解清楚项目结构和Gradle同步流程这个时间就花得值了。
返回列表