TodayClass 是一个本地课表管理 Android 应用,支持从 Excel 课表导入课程、查看今日课表和周课表、编辑课程排课信息,并通过 GitHub Actions 构建发布 release APK。
这份 README 面向二次开发者,重点说明项目结构、开发环境、构建发布流程和主要扩展点。
- Kotlin
- Jetpack Compose + Material 3
- AndroidX Navigation3
- Koin 依赖注入
- Room 本地数据库
- DataStore 偏好设置
- Apache POI 解析 Excel
- FileKit 文件选择
- Gradle Version Catalog
- Android Studio / IntelliJ IDEA
- JDK 17
- Android Gradle Plugin 9.2.1
- Kotlin 2.3.21
- Android SDK Platform 37.0
- minSdk 30
- targetSdk 36
- compileSdk 37
首次打开项目后,建议先同步 Gradle,再运行:
./gradlew testDebugUnitTest
./gradlew assembleDebugapp/src/main/java/com/eggetteluo/todayclass
├── data
│ ├── datastore # 主题等偏好设置
│ ├── local # Room 数据库、DAO、Entity、Relation
│ └── model # UI/业务数据模型
├── di # Koin 模块、导航 entry 注入
├── feature
│ ├── home # 今日课表
│ ├── schedule # 课程排课详情/编辑
│ ├── setting # 设置页
│ ├── upload # Excel 课表导入
│ └── week # 周课表
├── navigation # Route、BottomTab、Navigator
├── ui
│ ├── components # 通用 Compose 组件
│ ├── root # 根 Scaffold/NavDisplay/CompositionLocal
│ └── theme # 主题、色彩、字体
└── util # Excel、课程解析、日期、颜色等工具
项目按 feature 分层组织。页面入口由 NavigationModule 统一提供 ViewModel 和导航回调,Screen 本身只负责 UI 状态展示和事件分发。
推荐新增页面时遵循现有模式:
- 在
navigation/AppRoute.kt添加 Route。 - 在
feature/<name>下添加Screen、ViewModel、UiState。 - 在
di/ViewModelModule.kt注册 ViewModel。 - 在
di/NavigationModule.kt注册 navigation entry,并把 ViewModel / 导航动作传给 Screen。 - 避免在 Screen 内直接调用
koinViewModel()、koinInject()或直接依赖Navigator。
Room 入口:
data/local/AppDatabase.kt
主要表:
CourseEntity:课程基础信息CourseScheduleEntity:排课主表CourseScheduleWeekEntity:排课对应周次CourseTimeRuleEntity:节次时间规则SemesterInfoEntity:学期信息
首次创建数据库时会通过 DefaultTimeRules 写入默认上课时间规则。
如果修改 Entity 字段,需要同步处理:
AppDatabaseversion- migration
- DAO 查询字段
- 导入和编辑逻辑
当前 exportSchema = false,如果后续要维护正式数据库迁移,建议开启 schema 导出并提交 schema 文件。
入口:
feature/upload/UploadScreen.kt
feature/upload/UploadViewModel.kt
util/ExcelUtil.kt
util/CourseUtil.kt
流程:
- FileKit 选择
.xlsx文件。 ExcelUtil读取首个 sheet,并处理合并单元格。CourseUtil.parseCellText解析单元格课程文本。- 用户确认当前周后,
UploadViewModel.saveToDatabase写入 Room。
如果学校课表格式变化,优先调整 CourseUtil 和 ExcelUtil,避免把解析细节扩散到 UI 层。
运行单元测试:
./gradlew testDebugUnitTest构建 debug APK:
./gradlew assembleDebug构建 release APK:
./gradlew assembleRelease生成的 APK 文件名格式:
TodayClass-$versionName-v$versionCode-debug.apk
TodayClass-$versionName-v$versionCode-release.apk
例如:
app/build/outputs/apk/debug/TodayClass-1.1.0-v6-debug.apk
app/build/outputs/apk/release/TodayClass-1.1.0-v6-release.apk
Release 签名通过环境变量注入,避免把密码写入仓库。
本地如需构建已签名 release APK,可设置:
export SIGNING_KEY_STORE_PATH=/absolute/path/to/release-key.jks
export SIGNING_KEY_STORE_PASSWORD=your_store_password
export SIGNING_KEY_ALIAS=your_key_alias
export SIGNING_KEY_PASSWORD=your_key_password
./gradlew assembleRelease如果未设置这些环境变量,本地仍可执行 assembleRelease,但产物不会使用 release keystore 签名。
发布工作流:
.github/workflows/release.yml
触发方式:
- 手动运行
Release APK - 推送匹配
v*或*-v*的 tag
发布 tag 示例:
git tag 1.1.0-v6
git push origin 1.1.0-v6GitHub 仓库需要配置以下 Actions Secrets:
RELEASE_KEYSTORE_BASE64
RELEASE_KEYSTORE_PASSWORD
RELEASE_KEY_ALIAS
RELEASE_KEY_PASSWORD
生成 RELEASE_KEYSTORE_BASE64:
base64 -i app/TC_key.jks | pbcopy查看 keystore alias:
keytool -list -v -keystore app/TC_key.jks工作流会执行:
- 安装 JDK 17。
- 配置 Gradle。
- 配置 Android SDK。
- 安装
platforms;android-37.0。 - 解码 release keystore。
- 执行
testDebugUnitTest assembleRelease。 - 校验 APK 文件存在。
- 使用
apksigner verify校验签名。 - 创建 GitHub Release 并上传 APK。
版本号在 app/build.gradle.kts 中维护:
versionCode = 6
versionName = "1.1.0"发布前建议:
- 更新
versionCode。 - 必要时更新
versionName。 - 本地运行测试和 release 构建。
- 提交变更。
- 创建并推送 tag。
- 不要把 keystore 密码、GitHub token、签名环境变量写入仓库。
local.properties是本地 SDK 配置,不应依赖它做 CI 配置。- 新增依赖优先写入
gradle/libs.versions.toml。 - 业务逻辑优先放在 ViewModel、DAO、util 中,Screen 保持可组合、可预览、低耦合。
- 修改数据库结构时不要只改 Entity,需要补 migration 或明确处理数据兼容。
- 解析 Excel 时尽量保留原始文本,方便排查不同学校课表格式差异。
提交前建议至少运行:
./gradlew testDebugUnitTest涉及 UI、导航、数据库、导入流程或发布配置的改动,建议额外运行:
./gradlew assembleDebug
./gradlew assembleRelease