ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android Studio点菜系统实战:SQLite存储与购物车订单开发

Android Studio点菜系统实战:SQLite存储与购物车订单开发 最近整理电脑里的旧项目时翻出了一个之前做的Android Studio点菜系统成品项目顺手又跑了一遍发现这个小项目虽然代码量不大但把Android开发里最常碰到的知识点都串起来了界面布局、列表渲染、数据库存储、页面跳转、数据回传甚至还有一些权限和文件处理的坑。如果正在学Android开发或者需要一个能交作业、能演示、能二次改写的完整项目这个点菜系统会是个不错的参考。这个项目当时是帮一家小餐厅做的演示版本业务链路很简单顾客看菜单、点菜、加入购物车、结算下单老板在后台维护菜品。老规矩先把话说在前面这是一个本地单机版的项目数据全部存在手机自己的SQLite数据库里不依赖服务器所以不需要网络配置导入Android Studio就能跑。源码到手之后既能直接编译成APK安装到手机上演示也可以当成课设、毕设的基础框架去改造。1. 整体设计思路点菜系统这个选题为什么值得做很多新手学完Android基础之后会卡在一个问题单个知识点都会但不知道一个完整App是怎么拼起来的。点菜系统恰恰是一个把知识点“串起来”的好选题因为它有一条非常完整的业务链路看菜单 - 选菜品 - 加购物车 - 提交订单 - 查看历史订单。这条链路覆盖了Android开发里最重要的几块内容而且每一项都有很强的实用性。1.1 技术选型为什么用原生Java和SQLite这个项目用的是经典的原生方案Java语言 XML布局 SQLite数据库没有引入第三方网络库和数据库框架。选这个方案的原因很直接作为成品项目它的首要目标是让任何人拿到源码都能看懂、能改、能跑。如果一上来就上Jetpack Compose、Room、Retrofit很多刚入门的朋友光是在理解框架上就得花掉大量时间反而看不到业务本身是怎么跑的。界面方面用的是传统XML布局搭配RecyclerView做菜品列表和购物车列表。XML布局的学习资料最多遇到问题随手能搜到解法SQLite则是Android内置的轻量级数据库零配置适合单机数据管理。这里多说一句如果是想把这个项目作为进阶练习完全可以往MVVM架构、Room数据库方向改造后面第6章我会专门讲扩展思路。1.2 功能模块划分与数据流整个项目按功能可以拆成三大块顾客端核心流程菜品分类浏览、菜品搜索、加入购物车、购物车数量调整、结算下单。订单管理订单生成、历史订单列表、订单详情包含订单里的每道菜和数量。后台管理菜品的新增、编辑、删除菜品上下架。数据流更清晰菜品数据和管理员操作都落在SQLite数据库的几张表里购物车状态在Activity之间传递用的是内存对象订单提交时再把购物车数据写进数据库。这样设计的好处是每个模块彼此独立改起来不牵扯其他部分。早期版本我犯过一个错误把购物车数据也存到数据库里结果每次增删改查都要处理同步问题反而更麻烦。后来干脆购物车只活在内存里Activity回收了就清空简单直接。2. 核心功能拆解菜单浏览、购物车、结算这三块怎么实现2.1 菜品分类与列表展示分类Tab RecyclerView组合菜品展示是点菜系统的门面。我的做法是顶部用TabLayout做分类切换下面用RecyclerView展示对应分类的菜品卡片。每个菜品卡片包含菜品图片、名称、价格、销量以及“加入购物车”按钮。这里有个细节值得说分类数据来自数据库里的category表包括“热菜”“凉菜”“汤品”“主食”“饮品”等。菜品表里通过category_id外键关联分类。这样做的好处是以后想加一个“新品推荐”分类只需要在数据库里插入一条分类记录App不用改代码。-- 分类表 CREATE TABLE category ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL ); -- 菜品表 CREATE TABLE dish ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL, name TEXT NOT NULL, price REAL NOT NULL, image TEXT, sales INTEGER DEFAULT 0, is_available INTEGER DEFAULT 1, FOREIGN KEY (category_id) REFERENCES category(id) );点击“加入购物车”按钮时核心逻辑是判断购物车里有没有同款菜。如果有把数量加1如果没有新增一个购物车条目。这个判断看着简单但最容易出bug的地方在于对象的比较两个菜品对象只要id相同就是同一个菜不能仅仅因为name相同或者价格相同就合并。2.2 购物车设计与金额计算内存态用着最顺手购物车我用的是一个单例模式的CartManager内部维护一个ListCartItem。CartItem里面包含菜品信息、数量和选中状态。为什么用单例因为购物车需要在菜品列表页、购物车页面、结算确认页等多个界面之间共享数据单例可以避免每次跳转页面都把购物车数据通过Intent传来传去省事也安全。public class CartManager { private static CartManager instance; private ListCartItem cartItems; private CartManager() { cartItems new ArrayList(); } public static synchronized CartManager getInstance() { if (instance null) { instance new CartManager(); } return instance; } // 添加菜品、修改数量、计算总价等方法 }总价计算有一个效率问题很多人喜欢每次点“”或“-”的时候立刻重算总价并刷新UI。在菜品数量少时没问题但购物车一旦超过10个条目重复刷新会造成肉眼可见的卡顿。我改成只在两种情况下刷新一是添加新菜品时二是点击结算按钮时。购物车页面本身还是逐项刷新但列表之外的合计金额只在关键节点算一次这样流畅度提升明显。2.3 结算流程与订单表设计事务是底线结算页做的事情不算复杂展示购物车里的所有菜品、算总价、让顾客填写备注、确认下单。真正要注意的是订单数据的持久化。订单需要存两张表订单主表order和订单明细表order_item。订单主表记录订单编号、总金额、下单时间、备注订单明细表记录每个订单里的菜品名称、单价、数量。一个订单对应多条明细这是典型的一对多关系。这里必须强调一个原则写入订单主表和明细表要放在同一个数据库事务里。如果先写了主表、再写明细表中间一旦出错会出现“有订单但订单里没菜”的脏数据。用db.beginTransaction()包起来要么全部成功要么全部回滚这才是可靠的做法。下单成功之后清空购物车跳转到订单详情页或订单列表页。模拟支付我做得比较保守直接弹了一个“支付成功”的对话框没有接入真实的支付SDK。如果要把这个项目用于商用支付环节需要单独对接支付宝或微信的SDK那又是另一个故事了。3. 环境准备与项目导入从拿到源码到跑出APK的完整链路这个部分写给首次接触Android Studio的朋友。成品项目拿到手之后第一件事不是看代码而是先把环境跑通。经常有人问我“明明代码没问题为什么一运行就报错”十有八九是环境版本不匹配造成的。3.1 Android Studio版本与SDK配置建议我的开发环境用的是Android Studio 2023.1.1JDK 17Gradle 8.0以上。如果你的Android Studio版本比较新导入项目时可能会提示升级Gradle版本这里建议优先用项目自带的Gradle版本跑通不要直接点升级因为升级Gradle有概率引发兼容性连锁反应。项目打开后第一步是做三件事确认SDK路径、确认SDK版本、等待Gradle同步完成。在build.gradle里可以看到我配置的compileSdk 34、minSdk 21、targetSdk 34。android { compileSdk 34 defaultConfig { applicationId com.example.restaurant minSdk 21 targetSdk 34 versionCode 1 versionName 1.0 } }minSdk 21意味着Android 5.0以上的手机都可以运行覆盖了绝大多数设备。新手如果是为了在模拟器上测试这个配置没有任何问题。同步Gradle时需要联网下载依赖初次同步通常很慢我用的是阿里云镜像仓库把repositories节点替换成国内镜像能节省大量时间。3.2 gradle下载慢的解决方式与中文界面设置如果你发现Gradle一直卡在下载依赖多半是访问Maven Central不顺畅。解决方法很简单打开项目根目录的build.gradle把仓库地址改成阿里云公共仓库。buildscript { repositories { maven { url https://maven.aliyun.com/repository/google } maven { url https://maven.aliyun.com/repository/public } } }顺便提一句Android Studio的中文问题。很多新手问“Android Studio怎么设置中文”实际上是担心英文界面影响操作。当前版本的Android Studio可以在Settings - Plugins里搜索“Chinese Language Pack”安装中文语言包重启之后界面就会变成中文。不过我个人建议代码和报错信息还是习惯英文为好因为网上的技术资料和报错搜索基本都是英文过度依赖中文界面反而不利于查问题。3.3 编译APK和模拟器、真机调试项目跑通后编译APK很简单菜单栏点Build - Build Bundle(s) / APK(s) - Build APK(s)编译完成后在app/build/outputs/apk/debug/目录下能找到debug版的APK直接传到安卓手机上就能安装。模拟器是另一个容易踩坑的地方。Windows电脑上启动模拟器如果报错“Intel HAXM安装失败”或者“WHPX启动失败”先到Settings - SDK Manager - SDK Tools里确认是否安装了“Android Emulator Hypervisor Driver”。如果还是不行最稳的办法是开启Windows的Hyper-V功能并在Android Studio的虚拟设备配置里改一下硬件加速选项。说实话我自己测试这种小项目时更推荐用真机插上线开USB调试直接跑没有模拟器的那些性能损耗和冷启动时间。4. 数据存储与关键代码解析数据库、适配器、页面跳转4.1 SQLite数据库的封装思路这个小项目里我封装了一个DBHelper类继承自SQLiteOpenHelper。数据库名为restaurant.db版本号从当前是1后续如果改动表结构记得升级版本号并在onUpgrade里做数据迁移逻辑。最常见的坑是改了表结构但没升级版本老用户安装新APK时直接闪退。public class DBHelper extends SQLiteOpenHelper { public static final String DB_NAME restaurant.db; public static final int DB_VERSION 1; public DBHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_CATEGORY_TABLE); db.execSQL(CREATE_DISH_TABLE); db.execSQL(CREATE_ORDER_TABLE); db.execSQL(CREATE_ORDER_ITEM_TABLE); initData(db); // 预置分类和几道菜品方便首次打开就能演示 } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 这里要写迁移逻辑不能直接DROP TABLE否则用户数据全丢 } }initData是我专门放的一个方法在创建数据库的同时插入默认的分类和菜品数据。这样项目第一次启动后界面上就有内容可以看不用手动去后台添加数据。这个思路很适合做演示项目评委或使用者拿到手就能看到效果。4.2 RecyclerView适配器与列表刷新列表适配器是列表页的核心。我写了一个DishAdapter继承RecyclerView.Adapter内部用ViewHolder缓存控件引用在onBindViewHolder里把菜品数据绑定到界面上。有几个细节值得记录一下图片加载菜品图片用的是本地资源ID和文件路径结合的方式没有接入第三方图片库减少依赖。点击事件适配器里定义了一个OnItemClickListener接口把点击事件抛给Activity处理。不要在onBindViewHolder里写具体的跳转逻辑不然适配器会越来越臃肿。数据更新修改购物车数量后我调用的是notifyItemChanged(position)而不是notifyDataSetChanged()。前者只会刷新当前这一项性能更好动画也更自然。4.3 Activity跳转与数据回传菜品列表页进入结算页时需要携带订单信息我用Intent的putExtra传值。从结算页回到菜品列表页时需要知道下单是否成功用startActivityForResult在新版Android Studio中推荐使用ActivityResultLauncher但我在这个项目里保留了传统写法便于理解。Intent intent new Intent(MainActivity.this, SettleActivity.class); startActivityForResult(intent, REQUEST_CODE_SETTLE);然后在onActivityResult里根据结果码判断下单结果成功就清空购物车并提示用户失败则保留购物车方便用户重新结算。ActivityResultLauncher的写法虽然更规范但底层逻辑和Intent一致先理解传统写法再迁移过去是很自然的路径。5. 常见问题与避坑实录编译、权限、界面交互的手动排雷5.1 onBackPressed无效问题新版Android Studio创建的项目里传统的onBackPressed重写方法已经废弃了如果你按老代码写上Override public void onBackPressed()会发现根本没反应。这是因为从Android 13开始系统强制推荐用OnBackPressedCallback。Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) { Override public void handleOnBackPressed() { // 在这里处理返回键逻辑 } }); }我在购物车页面用这个回调做了“按返回键时弹出确认对话框”防止顾客误触返回丢弃购物车内容。这个改动很小但体验细节提升很明显。5.2 运行时权限与读取文件的问题做点菜系统时如果涉及到从SD卡读取菜品图片很容易碰到权限问题。Android 6.0以上需要动态申请权限Android 11以后还有分区存储限制直接访问SD卡根目录往往会失败。有朋友问过“StorageManager读取文件一直报错”以及“无法对SD卡根目录授权”这基本都是分区存储策略导致的不是代码写错了。我的处理方式是菜品图片优先放在App私有目录getExternalFilesDir或getFilesDir完全不需要额外的存储权限目录由系统托管卸载App时自动清除。如果非要读取公共存储目录里的图片推荐用系统文件选择器Intent去获取URI然后通过ContentResolver读取而不是直接操作文件路径。这里给一个通用模板public void pickImage() { Intent intent new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType(image/*); startActivityForResult(intent, REQUEST_PICK_IMAGE); }拿到URI之后再用ContentResolver配合openInputStream读取输入流解析Bitmap全程不需要申请存储权限。如果你的SDK版本是29以上这条路最省心。5.3 编译报错速查表报错现场常见原因解决方向Gradle同步一直卡住依赖下载慢或网络不通换阿里云镜像源找不到SDK目录未配置Local SDK路径Settings里设置Android SDK Locationduplicate value for resource资源名冲突检查res/values下是否存在同名id或stringUnsupported class file major versionJDK版本过新或过旧统一到JDK 17Manifest merger failed依赖库与主工程AndroidManifest冲突根据提示添加tools:replace属性模拟器无法启动WHPX/HAXM未启用开启Windows的Hyper-V功能或更换模拟器镜像5.4 一个隐藏的内存泄漏点有段时间我发现这个项目长时间操作后会出现卡顿排查后发现是购物车和菜品列表之间频繁创建对象并且没有消除对Activity的引用。其实在Adapter里写holder.itemView.setOnClickListener(v - startActivity(...))的时候如果外部类是非静态内部类的匿名对象它会隐式持有外部Activity的引用。退出界面后如果后台线程或单例还持有AdapterActivity就回收不了这就是内存泄漏。我后来把购物车的Click事件统一通过回调接口往外抛不让Adapter直接感知Activity的存在这个问题的可能性就大大降低了。单例模式里的CartManager同理它只有纯数据和计算方法完全不引用Context就不会泄漏Activity。如果你要在单例里用Toast或数据库建议用getApplicationContext()而不是Activity。6. 二次开发与扩展建议单机版怎么进化成联网版、MVVM版这个成品项目目前的定位是本地单机版适合学习、演示和交作业。如果哪天你想拿它做真正的商用系统有几个明确的方向可以走。6.1 架构升级从传统写法到MVVM整个项目目前是标准的Activity Adapter DAO结构如果开始感觉代码乱了、Activity越来越胖推荐向MVVM迁移。用ViewModel承载菜品列表和购物车的状态用LiveData通知UI更新用Repository统一管理数据来源。迁移的每个步骤都可以独立进行不需要推翻重来。先给数据库操作套上Repository层再让Activity只保留界面更新逻辑逐渐就能改造成MVVM。6.2 数据联网引入网络层和后端本地版的数据全部存在SQLite里意味着顾客换手机数据就没了老板也没法远程维护菜单。联网化改造的第一步是引入Retrofit和OkHttp把“新增菜品”“获取菜单列表”“提交订单”等操作从DBHelper换成网络请求。后端可以选Spring Boot之类的传统服务端也可以选免费的BaaS平台结合Firebase或Supabase快速实现账号体系和云端数据库同步工作量会小很多。这里有个经验业务逻辑层先定义一个接口比如MenuDataSource然后把SQLiteMenuDataSource和RemoteMenuDataSource都实现它界面层只依赖接口。这样切换数据源时基本不用动界面代码。6.3 功能扩展扫码点餐、桌台管理、多端同步如果往商用方向走可以加的功能实在太多比如每个餐桌一个二维码顾客扫码进入点餐页面后台分桌管理订单比如菜品销量统计、营业报表比如老板在Web端管理菜单手机App实时拉取更新。每个方向单拎出来都是一个不错的深化练习。我个人建议优先做桌台管理因为它是把单机版变成多用户版本的最关键一步订单表里增加一个table_id字段用于区分不同桌台的订单后续所有运营功能都可以围绕桌台展开。结尾再分享一个我踩过的坑这个项目做完之后我最有感触的不是某个具体功能而是“内存态与持久态边界”的把握。早期版本我把整个点菜过程都往数据库里塞每点一个菜就写一条记录结果数据库越来越大逻辑也越来越绕。后来才想明白像购物车这种临时数据就该活在内存里像订单这种需要追溯的数据才需要落库。做任何项目都先把这个边界画清楚代码会清爽很多。另外如果你遇到过“拿到成品项目却跑不起来”的尴尬可以先冷静检查三件事Gradle版本和JDK是否匹配、本地SDK路径是否配置、依赖仓库能否访问。这三件事占我排查项目导入问题的八成以上。点菜系统这个小项目麻雀虽小但五脏俱全用来练手和学习Android开发的完整流程是真的够用。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进