前一天的文章介紹了查詢頁的核心畫面-QueryResultActivity.kt ,我們了解到了它是如何接收大樓條件、管理 Spinner、觀察 ViewModel 結果,並更新 RecyclerView。
而今天要介紹的 QueryResultViewModel.kt ,是要來探討查詢頁的狀態與邏輯更新,它接收 building (大樓)、 floor(樓層)、 day(星期)、 time(節次/時段),從 Repository 取得所有教室課表,最後篩出 emptyRooms 給 Activity 顯示。
一開始我對現代 Android 開發(MVVM 架構)的概念不太熟悉,以為 ViewModel 的部分和 Activity 所寫的檔案可以放在一起;不過實際了解後,發現分開寫是有它的用意的:
QueryResultViewModel.kt負責什麼和 Hermes Agent 一起重讀檔案後,我了解到它就像是查詢頁面的「大腦」,專門負責維護查詢狀態與計算真正的空教室清單(emptyRooms)。
仔細剖析程式碼後,整個 ViewModel 的運作可以拆解成三大核心邏輯:
1. 接收 building 與查詢條件
private val selectedBuildingName = savedStateHandle.getLiveData<String>("building")
val selectedDay = MutableLiveData<String>()
val selectedTime = MutableLiveData<String>()
val selectedFloor = MutableLiveData<String>()
building 來自 MainActivity.kt 的 Intent,SavedStateHandle 能確保頁面在被系統回收時,從 MainActivity 傳過來的 building 條件不會丟失。selectedDay、selectedTime、selectedFloor 則由 QueryResultActivity 依照 Spinner 選項寫入。2. 產生樓層選單 floors
// 根據建築物名稱設定編號前綴(聖言樓為 SF,進修部大樓為 ES)
val buildingPrefix = if (building == "聖言樓") "SF" else "ES"
schedules.map { it.classroom }
.filter { it.startsWith(buildingPrefix) } // 只留下該大樓的前綴 (SF/ES)
.mapNotNull { name ->
// 提取樓層數字(ex. 從"SF130"提取"1")
val numberPart = name.removePrefix(buildingPrefix)
if (numberPart.isNotEmpty()) numberPart.substring(0, 1) else null
}
.distinct()
.sortedBy { it.toIntOrNull() ?: Int.MAX_VALUE } // 按照樓層順序
.map { "${it}樓" } // 轉換為顯示文字
.let { listOf("全部樓層") + it } // 在清單最前面加上「全部樓層」選項
floor 根據選映的大樓,自動計算出 floorSpinner 可選樓層(例如:全部樓層、1樓、2樓 等等)。3. 重新計算 emptyRooms 的核心邏輯
val filteredList = schedules.filter { schedule ->
val matchesBuilding = schedule.classroom.startsWith(buildingPrefix)
val floorNumber = if (currentFloor == "全部樓層") null else currentFloor.removeSuffix("樓")
val matchesFloor = floorNumber?.let {
schedule.classroom.removePrefix(buildingPrefix).startsWith(it)
} ?: true
val isAvailable = isRoomAvailable(schedule, timeSlotColumn)
matchesBuilding && matchesFloor && isAvailable
}
value = filteredList
承接 QueryResultActivity.kt ,QueryResultViewModel.kt 是這樣連接流程的:
QueryResultViewModel.kt
↓
收到 building / day / time / floor
↓
依查詢條件檢查教室
↓
篩選出符合條件的空教室
↓
更新 emptyRooms
QueryResultViewModel 是查詢頁的狀態與邏輯中心:
SavedStateHandle 取得首頁傳來的 building,並接收 QueryResultActivity 寫入的 selectedDay、selectedTime、selectedFloor。update() 會把中文大樓轉成 SF 或 ES,把星期與節次轉成 mon1、tueN 這類的 Kotlin 欄位代碼,然後對所有教室進行篩選。讀到 QueryResultViewModel 時,我才真正看見 RoomRush 查詢空教室的核心。
前面的 Activity 負責收集使用者操作,但真正判斷一間教室能不能顯示在結果列表裡,是在 ViewModel 的 update() 裡完成。
這段邏輯可以拆成三個問題:
程式最後用 matchesBuilding && matchesFloor && isAvailable 把三個條件串起來。這讓我理解到,所謂查詢功能其實是把使用者選項轉成明確條件,再一筆一筆過濾資料。
用一句話來記憶,我會說:
QueryResultViewModel把使用者選的building、day、time、floor轉成三個篩選條件:大樓符合、樓層符合、該時段不是X。
了解了 QueryResultViewModel.kt 如何將查詢邏輯與狀態串接起來後,接下來的關鍵就是:ViewModel 算出的 emptyRooms 資料陣列,到底是如何在 EmptyRoomAdapter.kt 中被變成畫面上的列表?
在下一篇文章中,我們將拆解 RecyclerView 的渲染細節——看我們如何處理「樓層標題」與「教室卡片」的動態組合,並讓這張列表不只是好看,還具備實質的點擊與互動功能。