最近把一套毕设项目完整整理了出来——基于Android Studio的传统美食文化宣传APP,从需求分析、界面设计到编码实现、打包演示,整个过程我都跑通了一遍,源码也整理打包好了。这个项目定位很清晰:做一个聚合了传统美食介绍、历史典故、制作工艺和地方特色的安卓客户端,核心是“文化宣传+信息展示”,交互上带分类浏览、关键词搜索、收藏和分享,虽然结构不复杂,但覆盖了Android开发里最常用的一整套知识点。适合正在准备毕设、想找一份能二次开发的完整源码的人,也适合刚学完基础想拿一个真实项目练手的同学。
我这个帖子不打算写成说明书,更多是站在“已经做完一遍、踩过一轮坑”的角度,把设计思路、数据库怎么建、Activity和Fragment怎么协作、打包怎么避坑,以及答辩时老师喜欢问什么,全部掰开揉碎讲清楚。你拿到源码后,不光是能跑,还能知道每一块为什么这么写,改起来也顺手。
1. 项目定位与需求拆解
1.1 为什么选“传统美食文化”这个方向
选毕设题目时,第一反应是避开那些烂大街的商城APP、图书借阅系统、新闻客户端。食品安全和文化传播是近几年比较受关注的方向,传统美食这个切入点有几个天然优势:第一,内容源极其丰富,中国地大物博,光八大菜系就能撑起几百个详情页,完全不愁没数据;第二,展示型应用逻辑清晰,不需要复杂的事务处理,独立完成度很高;第三,在答辩环节,评委老师很容易对这个主题产生兴趣,每个人都有话聊,提问压力会小一些。
从实际宣传场景看,传统美食文化一直缺一个轻量的、可离线浏览的展示载体。放到网页上,信息容易被淹没在搜索引擎结果里;做成App,用户可以按分类浏览,搜索某一款菜的起源,收藏自己老家的味道。整个App不需要联网实时更新,本地数据库加上静态资源就能跑起来,这也正合适一个学生团队或个人在几周内完成。
1.2 功能需求与模块划分
我做的第一件事是画用例图,把“用户”可能干的事全部列出来。最终收敛为主界面、分类浏览、全局搜索、详情展示、收藏、关于与分享六块。
主界面负责整体视觉入口,顶部是轮播推荐位,下面按热门菜品或地域分类排列;分类浏览页支持点击分类标签筛选对应美食;搜索框支持模糊匹配菜名和简介;详情页展示图片、朝代出处、食材列表、制作步骤和文化故事;收藏页从数据库里读取用户点击过小红心的记录;关于页放项目介绍和技术栈说明,同时提供一个分享入口,把当前页面的菜名和简介通过系统分享面板发出去。
角色权限没有单独设计,毕设阶段不做登录注册,这样既省去很多鉴权的麻烦,又可以把更多精力放到核心内容上。如果你拿这套代码去扩展,后面要加“管理员发布美食”功能,我建议单独做一个本地管理入口,或者引入轻量后端,而不是在现有用户端里硬塞。
1.3 技术选型:Java、SQLite与本地数据
语言我选了Java,而不是Kotlin。不是Kotlin不好,而是毕设阶段用Java更稳妥:网上中文资料多,遇到编译问题好排查;老师答辩时对Java的套路也更熟悉;还有一点,老版本项目、教材和竞赛模板基本都是Java,拿过来能直接参考。
数据库选了SQLite,没有用Room。Room封装确实好用,但毕设如果只写“我用了Room”,老师追问底层实现时容易露怯。直接用SQLiteOpenHelper写建表和增删改查,反而能展示你对数据库基础的理解。食材、简介这类静态内容我放在assets目录下的JSON文件里,App首次启动时解析并写入SQLite。这样既避免了几十条数据手动写进数据库文件的麻烦,也让二次开发时替换内容变得非常简单——改JSON,不碰Java代码。
UI方面,主题采用Material Components,用了Toolbar、BottomNavigationView、ViewPager2、RecyclerView和CardView。具体组件选型不追求新,但都是现在主流的做法,网上能查到的坑我基本都踩过。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 界面设计与数据模型设计
2.1 从草图到布局:“文化感”不是玄学
传统美食文化的App,界面不能做得像后台管理系统一样干巴巴。我一开始用了纯白色背景加黑色文字,怎么看都像新闻阅读器。后来把主题色定成了深棕和米黄的搭配,色调接近“陶土”“麻布”的质感。按钮用圆角,卡片带轻微阴影,图片统一做圆角16dp,这些细节在视觉上马上有了文化味。
导航结构用了经典的“单Activity + 多Fragment”,底部三个Tab分别是首页、分类、我的。Activity作为壳,承载Fragment切换;每个Tab内部再用子Fragment管理自己的页面。这样做的好处是底部导航栏不用每个页面都写一遍,而且Fragment之间通过ViewModel或接口通信,逻辑清晰。首页内部嵌套了轮播和列表,我是用一个大的Fragment,里面用RecyclerView嵌套实现了“轮播+分类入口+推荐列表”的纵向滚动效果,避免多Fragment叠加导致逻辑混乱。
布局文件中有一个容易忽略的点:所有需要显示图片的页面,都要在根布局设置android:clipToOutline="true",配合background的圆角shape,才能让Glide加载出来的图按圆角裁剪,否则图片四个角是方的,卡片圆角就白设了。
2.2 数据库表结构与初始化策略
SQLite数据库我设计了三张表:category(美食分类)、food(美食详情)、favorite(用户收藏)。category表结构很简单:id、name、emoji(分类图标字符)、sort_order(排序字段)。food表是核心,字段包括id、category_id、title、intro(简介)、history(历史典故)、ingredients(主要食材)、steps(制作步骤)、image_url(图片路径)、is_recommend(是否推荐)。favorite表记录了food_id和create_time,用于收藏页展示。
建表语句我用了一个容易被忽略的小技巧:外键约束先不写,而是通过Java代码保证一致性。原因很简单,SQLite默认外键开关是关闭的,写了外键不生效不说,老师如果现场打开外键,反而可能因为数据顺序问题报错。收藏表里即使food_id对应菜品被删了,也不影响App崩溃,最多收藏页显示空。这一点在答辩时主动讲出来,会显得你考虑过异常场景。
初始化数据时,我把JSON文件放在了assets/data/init_data.json,首次启动时用SQLiteDatabase.beginTransaction()批量插入。事务能保证几十条数据要么全部插入成功,要么全部回滚,避免中途断电或崩溃导致数据残缺。
2.3 图片资源与加载方式
图片我放在了assets/images目录下,数量控制在50张以内,每张压缩到100KB以下,这样APK体积不会膨胀太严重。加载图片用的Glide,版本4.15.1,指定load("file:///android_asset/images/xxx.jpg")的方式读取assets图片。为什么不直接用drawable目录?因为assets目录支持运行时按文件名动态加载,后续如果要接后端,只需改成URL,代码改动很小。
JSON里存的image_url是相对路径,比如images/sichuan/mapo_tofu.jpg,程序里统一拼接file:///android_asset/前缀。如果你后续要换成网络图,把这个拼接方法一换就行。
3. 核心功能实现与关键代码讲解
3.1 环境搭建与Gradle配置
由于Android Studio最近的版本更新很快,我在2023年年底用的时候已经升到了Hedgehog版本(2023.1.1)。这个版本默认要求Gradle 8.2及以上,AGP(Android Gradle Plugin)版本对应8.2.0以上。如果你创建的工程用的AGP版本太老,编译时会直接报错“Minimum supported Gradle version is 8.2”。所以在build.gradle(Project级)里,我写的classpath是:
groovy复制classpath "com.android.tools.build:gradle:8.2.0"
gradle-wrapper.properties里对应设置为:
properties复制distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
模块级build.gradle的核心配置如下:
groovy复制android {
namespace "com.example.foodculture"
compileSdk 34
defaultConfig {
applicationId "com.example.foodculture"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.github.bumptech.glide:glide:4.15.1'
implementation 'com.google.code.gson:gson:2.10.1'
}
compileSdk 34对应Android 14,如果你本地的SDK Platform版本低于这个数字,Android Studio会提示自动下载。国内网络条件下,建议提前把SDK、Gradle下载好,最好用镜像,不然卡在Gradle sync那一步很痛苦。
3.2 首页轮播图与推荐列表
首页的轮播图我用的是ViewPager2,结合一个内部的Handler做自动轮播。核心思路是:轮播图片集合放在一个List<Integer>里,无限轮播采用“起始位置设为大数”的取模技巧。Adapter中重写getItemCount()返回一个大数值,onBindViewHolder通过position % imageList.size()获取正确的图片。这样做比手动处理最后一页到第一页的跳转自然得多。
自动轮播的Runnable这样写:
java复制private final Runnable autoPlayTask = new Runnable() {
@Override
public void run() {
if (viewPager2 != null) {
int current = viewPager2.getCurrentItem();
viewPager2.setCurrentItem(current + 1, true);
handler.postDelayed(this, 3000);
}
}
};
要注意,在onPause()里必须移除回调,否则页面切到后台时轮播还在跑,既浪费资源又可能导致不稳定的异常。首页下方的推荐列表用RecyclerView嵌套在同一个Fragment里,数据从数据库查询is_recommend = 1的美食列表,展示卡片式布局。每个卡片包含图片、菜名、简介和分类标签,点击跳转到详情Activity。
3.3 美食详情页与收藏功能
详情页是一个单独的Activity,通过Intent接收food_id。在onCreate里直接根据ID查数据库,然后把数据填充到布局中。整个页面用ScrollView包裹,因为内容比较长,需要滚动。顶部图片高度设定为220dp,下面的标题、简介、历史典故、食材、制作步骤依次排列。
收藏功能的实现有两种方案:一种是详情页内部管理收藏状态,另一种是全局收藏单例。我采用前者,在详情页布局放一个ImageView充当小红心,初始状态从favorite表查询,如果有记录就显示红色实心,否则显示灰色空心。
点击时执行以下逻辑:
java复制private void toggleFavorite(int foodId) {
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("food_id", foodId);
values.put("create_time", System.currentTimeMillis());
long count = DatabaseUtils.queryNumEntries(db, "favorite", "food_id = ?", new String[]{String.valueOf(foodId)});
if (count > 0) {
db.delete("favorite", "food_id = ?", new String[]{String.valueOf(foodId)});
favoriteImg.setImageResource(R.drawable.ic_favorite_border);
} else {
db.insert("favorite", null, values);
favoriteImg.setImageResource(R.drawable.ic_favorite);
}
}
这里我一开始犯过一个低级错误:每次点击都重新getWritableDatabase(),但没有及时close(),导致运行一段时间后报“database is locked”。后来我把db创建和关闭逻辑统一放到了DBHelper的getInstance()单例里,每次操作完在finally中关闭数据库连接。千万不要一个页面操作好几个SQLiteDatabase实例。
3.4 搜索与分类筛选
搜索我放在了顶部的SearchView里,监听onQueryTextChange,每次输入变化都去数据库做一次模糊查询:
java复制String sql = "SELECT * FROM food WHERE title LIKE ? OR intro LIKE ?";
Cursor cursor = db.rawQuery(sql, new String[]{"%" + query + "%", "%" + query + "%"});
为了不让搜索卡顿,我在onQueryTextChange中做了200ms的防抖,用Handler.removeCallbacks和postDelayed延迟查询。等用户停止输入再执行SQL,否则每敲一个字母都查一次数据库,Cursor和Adapter频繁刷新,很容易出现闪烁。
分类筛选相对简单,分类Tab使用TabLayout和RecyclerView联动,点击Tab时把category_id传入列表查询方法。我在分类Fragment里用了FragmentStateAdapter,把“全部”也当成一个特殊分类,category_id等于0时查询全部,避免单独写一段无分类条件逻辑。
3.5 关于页和分享
关于页放了App的Logo、版本号、技术栈说明,以及“所用图片来源于公开资料,仅用于课程学习”的声明。分享功能用了系统Intent:
java复制private void shareFood(Food food) {
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("text/plain");
shareIntent.putExtra(Intent.EXTRA_TEXT, "推荐一道传统美食:" + food.getTitle() + "\n" + food.getIntro());
startActivity(Intent.createChooser(shareIntent, "分享到"));
}
分享功能虽然写起来简单,但演示效果很好,答辩时现场把一个菜分享到微信或备忘录,比单纯截图有说服力。
4. 打包、真机调试与常见坑
4.1 模拟器和真机的选择
开发期间大部分时间我用的模拟器是Android Studio自带的Pixel设备,API 34镜像。但模拟器启动慢,而且我的电脑没有开启硬件加速,跑起来卡得不行。后来转向真机调试,用的是USB数据线连接手机,开启开发者选项里的“USB调试”。
真机调试要注意几点:第一,必须安装对应品牌的USB驱动,否则电脑识别不到设备;第二,Android Studio右上角选择设备时,如果真机没出现,多半是驱动问题或者USB模式选错了,把“仅充电”改成“传输文件”通常能解决;第三,部分手机需要关掉“USB安装监控”或通过校验弹窗确认。我用的是小米手机,第一次连接时需要登录账号并开启“USB安装”,否则IDE会提示“device unauthorized”。
模拟器方面,新版Android Studio默认用Android Emulator hypervisor driver,如果你的电脑不支持Hyper-V或HAXM,会报“emulator: ERROR: x86_64 emulation currently requires hardware acceleration”。解决方法是去SDK Manager里安装“Android Emulator Hypervisor Driver”,然后在BIOS里打开虚拟化。但说实话,毕设演示直接用真机最稳。
4.2 Gradle与SDK版本兼容坑
这一块是新手最容易崩溃的地方,我整理几个高频报错和对应解法。
第一个:“Could not find com.android.support:appcompat-v7:26.1.0”。原因是项目用了老式support库,而新SDK环境已经迁移到AndroidX。统一改成androidx.appcompat:appcompat:1.6.1,并且在gradle.properties里加两行:
properties复制android.useAndroidX=true
android.enableJetifier=true
第二个:“SDK location not found. Define location with sdk.dir in the local.properties file or with an ANDROID_HOME environment variable.”这个多半是工程文件里少了local.properties,在里面指定本机SDK路径就可以。如果你用Android Studio直接打开项目,通常会自动生成。
第三个:AGP版本和Gradle版本不匹配,报错信息通常很长,核心提示是“Minimum supported Gradle version is X.X”。我建议直接把Project级build.gradle里的AGP升级到8.2.0,Gradle wrapper升级到8.2,一分钟解决,不用自己瞎猜。
第四个:资源编译报错,比如drawable文件名用了中文或大写字母。Android资源文件名只能用小写字母、数字和下划线,图片也不能放在drawable文件夹后直接用file:///android_asset/路径加载,这两个概念别混了。
4.3 打包APK与演示技巧
打包APK之前,先要生成签名文件。在Android Studio里点Build -> Generate Signed Bundle / APK,选择APK,下一步选择Create new key store,填一堆证书信息。这里不用太较真,密码别忘记就行,反正毕设不会真的上架到应用商店。签名文件生成后,后续每次打包都能复用,不用重复建。
演示时最怕现场SQLite初始化失败或白屏。我做了两个保护措施:第一,App启动时在SplashActivity里检测数据库是否有数据,如果没有就先解析assets里的JSON并插入,插入完成后才跳转主界面;第二,所有图片加载都在Glide的onLoadFailed里设置一个默认背景,即使某张图片缺失,页面也不会崩。
答辩前我会建议把App的桌面图标名改成“中华美食文化”,图标尺寸1024x1024,效果比默认机器人图标好很多。可以在mipmap目录下替换ic_launcher.png,或者在AndroidManifest.xml里指定android:icon="@mipmap/ic_launcher_food"。
4.4 毕设答辩高频问题速查
答辩环节老师一般不会真的去读你几千行代码,而是集中问设计理念和技术点。我把自己遇到的高频问题整理成下表:
| 问题 | 参考回答思路 |
|---|---|
| 为什么不用MySQL而用SQLite? | SQLite是嵌入式关系型数据库,适合单机离线场景,不需要配置服务器,直接集成在App里;MySQL通常配合服务端使用,毕设做纯客户端不需要。 |
| 数据是怎么初始化的? | 把菜品数据写成JSON放在assets目录,首次启动通过Gson解析后批量插入SQLite,避免手动一条条插。 |
| 搜索功能怎么实现的? | 通过SQL的LIKE模糊查询,匹配菜名和简介,搜索框做了防抖处理。 |
| 收藏功能的数据存在哪? | 存在SQLite的favorite表,点击红心时插入或删除记录,列表页刷新展示。 |
| 如何防止SQL注入? | 使用rawQuery时把查询条件放到selectionArgs里,让系统帮我转义,不使用字符串拼接。 |
| 图片为什么用Glide? | Glide具备缓存和异步加载能力,避免主线程加载大图造成卡顿或OOM。 |
| 如果数据量变成10万条,架构怎么改? | 可以增加分页加载、引入网络后端和服务器数据库,或者使用Room加协程做更高效的异步查询。 |
注意,回答时不要背答案,最好能带着代码文件现场指给老师看,老师对你的印象会加分。
5. 源码结构与二次开发建议
5.1 源码目录结构解析
整套源码我做了比较整洁的分层,你拿到后能看到如下目录结构:
text复制app/src/main/
├── java/com/example/foodculture
│ ├── activity
│ │ ├── MainActivity.java
│ │ ├── DetailActivity.java
│ │ ├── SplashActivity.java
│ │ └── SearchActivity.java
│ ├── adapter
│ │ ├── BannerAdapter.java
│ │ ├── FoodListAdapter.java
│ │ └── CategoryAdapter.java
│ ├── database
│ │ ├── DBHelper.java
│ │ └── FoodDao.java
│ ├── model
│ │ ├── Food.java
│ │ └── Category.java
│ ├── fragment
│ │ ├── HomeFragment.java
│ │ ├── CategoryFragment.java
│ │ └── FavoriteFragment.java
│ └── utils
│ ├── AssetUtils.java
│ └── GlideUtils.java
├── res/layout
│ ├── activity_main.xml
│ ├── activity_detail.xml
│ ├── activity_splash.xml
│ ├── fragment_home.xml
│ ├── item_food_card.xml
│ └── ...
├── res/values
│ ├── colors.xml
│ ├── strings.xml
│ └── themes.xml
└── assets
├── data/init_data.json
└── images/...
这个结构遵循了“按职能分包”的常规做法,model层放业务模型,activity和fragment层放界面逻辑,adapter层放列表适配,database层统一管理数据库访问,utils放工具方法。新手拿到源码后,先从SplashActivity开始看,再到MainActivity,基本就能串起整个流程。
5.2 如何把“美食”换成“非遗”“茶文化”
很多同学拿了美食源码后,想换个主题当自己的毕设,其实操作起来非常方便,因为核心数据都在assets的JSON文件里。
第一步,打开assets/data/init_data.json,修改JSON里的分类名称和菜品描述。建议保留原JSON的字段结构,只改值,字段名千万别动,否则Gson解析会报错。
第二步,替换assets/images目录下的图片,文件名要和JSON里image_url路径保持一致。这一步最容易出问题,图片文件名有一处对不上,详情页就会显示加载失败,演示时露馅。
第三步,改App的主题色。在res/values/colors.xml里把colorPrimary改成对应文化的颜色。比如茶文化可以改成淡绿,非遗可以改成砖红。strings.xml里的App名称也一并改掉。
第四步,如果分类数量变化,需要同步改数据库初始化逻辑。不过因为我用category表存分类,JSON解析时会动态生成category记录,所以加减分类不用改Java代码,只要把JSON里的category数组增删即可。
5.3 后续可以继续扩展的方向
这套代码虽然是一个完整的毕设项目,但真要落地成作品,还有很多可以扩展的地方。比如,给详情页加一个“语音朗读”按钮,用系统TTS把历史典故读出来,适合老年用户;或者加一个“随机推荐一道菜”功能,用一个小算法从数据库里随机取一条数据,点击首页右上角的骰子图标触发。
更实用的是把收藏页做成支持左滑取消收藏,滑动删除可以用RecyclerView的ItemTouchHelper实现,代码量不大但效果很加分。如果有人想朝“全栈”方向发展,可以在后端用Spring Boot写REST接口,App通过Retrofit拉取远端数据,assets只作为离线兜底。不过要提醒一点:一旦上了网络,核心业务从“本地展示”变成“前后端交互”,工作量翻倍,老师问的内容也会更多,建议根据自己的准备时间权衡。
我在实际做这套项目时最深刻的体会是:代码量不大,但每个环节都藏着细节。越早把环境、Gradle、真机调试这些“地基”打稳,后面写业务代码就越顺手。尤其是第一次跑通“从JSON到SQLite再到界面显示”的完整链路时,那种成就感会支撑你连续改好几天bug。如果你也在做类似的毕设题材,记住一点:不要为了炫技堆库,每一步都能讲出“为什么这么做”,比用了多少新技术重要得多。
最后再分享一个小技巧:在项目里多写注释,特别是数据库字段和Activity跳转的部分。答辩时老师可能随意点开一个文件,如果你的注释清晰、命名规范,第一印象就会好很多。这套源码我已经把关键位置都标注过了,你拿到后可以先跑起来,再根据我上面讲的结构去逐模块阅读,很快就能上手。
