食来运转 (WhatFood)
一个完整的 Android 随机食物推荐应用,帮助你决定「今天吃什么」。采用 Kotlin + Jetpack Compose + Room 构建,纯离线可用,同时支持云端数据同步。
🎰 老虎机抽选
加权随机算法 + 三列卷轴动画 + 长按拉杆触发,完全模拟真实老虎机体验
🔍 自由探索
六大餐别横向卡片浏览 + 搜索过滤 + 左右滑动拉黑/喜欢
👤 云端同步
登录后自动同步口味偏好和食物喜厌到云端,换设备无缝恢复
🔐 两步验证
TOTP 标准两步验证,保护账号安全并支持找回密码
🎨 液态玻璃
参考 iOS 26 设计的浮动胶囊导航栏,毛玻璃模糊 + 弹性动画
📦 离线优先
168 种食物数据内置在 APK 中,无网络也能完整使用
技术栈
环境搭建
在开始学习代码之前,需要先准备好开发环境。以下是必需的工具清单。
| 工具 | 版本 | 用途 |
|---|---|---|
| Android Studio | 2024.2+ | IDE,编写和调试代码 |
| JDK | 17+ | 编译 Kotlin 代码 |
| Gradle | 8.7 | 构建工具(自动下载依赖、打包APK) |
| Android SDK | API 35 | Android 平台开发包 |
| MySQL | 8.0 | 后端数据库(可选) |
如果只是学习前端 Android 代码,不需要安装 MySQL。只有想运行完整的登录/同步功能时才需要。
确保 Android Studio 中 SDK Manager 已下载 API 35 的 SDK Platform 和 Intel x86_64 模拟器镜像。
项目结构
整个项目有 31 个 Kotlin 源文件,按功能分层组织。理解这个结构是读懂代码的第一步。
ShiLaiYunZhuan/
├── build.gradle.kts # 项目构建配置
├── app/
│ ├── build.gradle.kts # 模块构建 + 依赖声明
│ └── src/main/
│ ├── AndroidManifest.xml
│ ├── res/ # 图片、字体、颜色等资源
│ └── java/com/example/whatfood/
│ ├── MainActivity.kt # 应用入口
│ ├── viewmodel/
│ │ └── MainViewModel.kt # 核心业务逻辑
│ ├── ui/
│ │ ├── WhatFoodApp.kt # 导航骨架
│ │ ├── auth/AuthPages.kt # 登录注册页
│ │ ├── components/ # 通用组件
│ │ ├── theme/ # 主题配色
│ │ └── navigation/ # 路由定义
│ ├── data/
│ │ ├── entity/ # 数据库表
│ │ ├── dao/ # 数据库操作
│ │ ├── database/ # 数据库实例
│ │ ├── repository/ # 数据仓库
│ │ └── preferences/ # 本地偏好存储
│ ├── network/ # 网络通信
│ └── pageone_shilaiyunzhuan/ # 老虎机页面
│ └── engine/ # 抽取算法| 层级 | 目录 | 职责 |
|---|---|---|
| UI 层 | ui/, page*/ | 界面渲染、用户交互 |
| 状态层 | viewmodel/ | 持有状态、处理业务逻辑 |
| 数据层 | data/ | 数据库操作、JSON解析 |
| 网络层 | network/ | HTTP请求、API调用 |
Android 基础
如果你只有 Java 基础,这一节帮你理解 Android 的核心概念。
Kotlin 入门
Kotlin 是 Android 的官方语言。以下对照 Java 解释常用语法。
| 概念 | Kotlin | Java 等价 |
|---|---|---|
| 变量 | val x = 5(不可变)var y = 5(可变) | final int x = 5;int y = 5; |
| 空安全 | val s: String? = null | String s = null;(无编译期保护) |
| 安全调用 | s?.length(null时返回null) | s != null ? s.length() : null |
| Elvis | s ?: "默认" | s != null ? s : "默认" |
| 数据类 | data class User(val name: String) | 手写 getter/setter/equals/hashCode |
| when | when(x){1->"一" else->"其他"} | switch(x){case 1:return"一";default:return"其他";} |
| Lambda | list.filter{it>0} | list.stream().filter(x->x>0) |
| 协程 | suspend fun fetch() | Future/Callback(异步更复杂) |
| Flow | Flow<List<T>> | LiveData/RxJava Observable |
如果你有 Java 基础,花 1-2 天看 Kotlin 官方文档就能上手本项目的代码。重点理解
val/var、空安全、data class、suspend 函数。Jetpack Compose
Compose 是 Android 的现代 UI 框架,,用 Kotlin 代码写界面,不再需要 XML 布局文件。
@Composable 函数
用 @Composable 注解标记的函数会被 Compose 编译器特殊处理,,自动追踪状态变化并更新 UI。
@Composable
fun Greeting(name: String) {
Text("你好, $name!")
}Modifier 修饰符链
Modifier 是 Compose 的核心概念,,控制组件的大小、位置、间距、背景、点击等所有外观和行为。
Text("Hello",
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.background(Color.White)
.clickable { onClick() }
)State , 响应式状态
使用 remember 和 mutableStateOf 创建状态变量。当状态变化时,所有依赖它的 UI 自动重新渲染。
var count by remember { mutableIntStateOf(0) }
Button(onClick = { count++ }) {
Text("点击了 $count 次")
}布局组件
Compose 提供三大基础布局:
- Box , 层叠布局(子元素可互相覆盖)
- Column , 垂直排列
- Row , 水平排列
本项目的老虎机界面就是 Box 中叠了 5 层图片实现的。
Room 数据库
Room 是 Android 官方的 SQLite 抽象层,,让你用 Kotlin 对象操作数据库,不用手写 SQL。
Entity , 数据库表
@Entity 标记的类会自动映射为 SQLite 表。每个属性对应一列。
@Entity(tableName = "foods")
data class FoodEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val name: String, // 食物名称
val mainType: String, // 早餐/正餐/...
val baseWeight: Float, // 基础权重
val isFavorite: Boolean // 是否喜欢
)DAO , 数据访问对象
@Dao 接口定义所有数据库操作。Room 在编译时自动生成实现代码。
@Dao
interface FoodDao {
@Query("SELECT * FROM foods ORDER BY name ASC")
fun observeAllFoods(): Flow<List<FoodEntity>> // 响应式查询
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertFood(food: FoodEntity) // 插入或替换
}Database , 数据库实例
使用单例模式确保全局只有一个数据库连接。
@Database(entities = [FoodEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun foodDao(): FoodDao
companion object {
@Volatile private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
INSTANCE ?: Room.databaseBuilder(
context, AppDatabase::class.java, "shilaiyunzhuan.db"
).build().also { INSTANCE = it }
}
}
}
}网络通信
本项目使用 Retrofit + OkHttp 与 Spring Boot 后端通信,实现登录注册和数据云端同步。
核心架构
// ApiService.kt , 定义所有 API 端点
interface ApiService {
@POST("api/auth/login")
suspend fun login(@Body body: AuthRequest): AuthResponse
@POST("api/sync/upload")
suspend fun syncUpload(
@Header("Authorization") token: String,
@Body data: SyncUploadData
): Map<String, Any>
}
// HttpClient.kt , 创建 Retrofit 实例
object HttpClient {
val api: ApiService = Retrofit.Builder()
.baseUrl("http://23.95.190.125:8850/")
.addConverterFactory(GsonConverterFactory.create())
.build().create(ApiService::class.java)
}Retrofit 将其接口方法自动转换为 HTTP 请求。Gson 自动将 JSON ↔ Kotlin 对象互转。
老虎机动画
这是本项目的核心体验,,三列食材图片纵向滚动,最终停在随机抽取的食物上。
动画架构(5层叠加)
从底到顶依次渲染:背景图 → 三列卷轴 → 机身装饰框 → 结果文字 → 拉杆
卷轴动画时长 2400ms,列间错开 300ms(依次启动),拉杆动画 23 帧序列
抽取算法
加权随机轮盘赌算法,,不是等概率随机,而是考虑了口味偏好、收藏加权、动态补偿。
算法流程
- 4层过滤:mainType → 去黑名单 → 排除上次同组 → 餐段偏好
- 加权计算:有效权重 = (baseWeight + dynamicWeight) × 口味倍率 × 收藏倍率
- 轮盘赌:按权重比例随机选择(权重大的食物概率更高)
- 动态补偿:未抽中的食物 dynamicWeight+1(下次更容易被抽到)
自由探索
用户可以浏览所有食物,搜索特定食物,左右滑动标记喜欢或拉黑。
马卡龙色系
六大餐别各有专属配色:早餐浅粉、正餐浅绿、下午茶浅蓝、夜宵浅橙、零食浅棕、饮品浅紫
在大类名称区域左右滑动→切换卡片。在食物条目上左右滑动→拉黑/喜欢。两者不冲突。
登录与同步
用户注册→两步验证初始化→登录→自动从云端下载口味和喜好数据。
数据流
注册 → 两步验证(可选) → 登录 → 获取 JWT Token
→ 清除本地数据 → 从云端下载 → 覆盖本地
→ 用户修改口味/喜好 → 静默上传云端
→ 下载锁(3秒)防止循环覆盖后端架构
Spring Boot 3.2.5 后端,部署在 RackNerd VPS (Ubuntu 24.04, 1GB RAM)。
| API 端点 | 方法 | 说明 |
|---|---|---|
| /api/auth/register | POST | 注册 |
| /api/auth/login | POST | 登录(返回JWT) |
| /api/auth/totp | POST | 两步验证操作 |
| /api/auth/forgot-password | POST | 忘记密码 |
| /api/sync/upload | POST | 上传数据 |
| /api/sync/download | POST | 下载数据 |
学习资源
推荐的学习路径和参考资料。
7 天学习计划
| 天数 | 内容 |
|---|---|
| Day 1 | 阅读 docs/00_read_me_first.md,了解项目全貌 |
| Day 2-3 | 学习 Kotlin 基础语法,理解 data class、Flow、协程 |
| Day 4 | 学习 Compose 核心概念(@Composable、Modifier、State) |
| Day 5 | 理解 MVVM 架构,通读 MainViewModel |
| Day 6 | 研究老虎机动画和抽取算法 |
| Day 7 | 理解网络层和后端架构 |
Kotlin 官方文档 (kotlinlang.org)、Compose 官方文档 (developer.android.com)、本项目 docs/ 目录下的13份文档