iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Software Development

從 Vibe Coding 到可維護專案:用 Hermes Agent 重讀 RoomRush Android 專題系列 第 6

Day 6 | 空教室到底怎麼算?我從 ViewModel 找答案

  • 分享至 

  • xImage
  •  

前言:building + floor + day/time 如何篩出 emptyRooms?

前一天的文章介紹了查詢頁的核心畫面-QueryResultActivity.kt ,我們了解到了它是如何接收大樓條件、管理 Spinner、觀察 ViewModel 結果,並更新 RecyclerView。

而今天要介紹的 QueryResultViewModel.kt ,是要來探討查詢頁的狀態與邏輯更新,它接收 building (大樓)、 floor(樓層)、 day(星期)、 time(節次/時段),從 Repository 取得所有教室課表,最後篩出 emptyRooms 給 Activity 顯示。


一開始我認為

一開始我對現代 Android 開發(MVVM 架構)的概念不太熟悉,以為 ViewModel 的部分和 Activity 所寫的檔案可以放在一起;不過實際了解後,發現分開寫是有它的用意的:

  • 螢幕旋轉資料不遺失: Activity 在旋轉螢幕時會被銷毀重建,但 ViewModel 會繼續存活,能直接保留剛查好的空教室資料,不用重新查詢。
  • 職責分離: Activity 只管「UI 和元件點擊」;ViewModel 只管「資料運算與空教室篩選」,程式碼不再全部塞在一起。
  • 方便寫單元測試: ViewModel 不影響 UI 與 Android 元件,能直接寫單元測試,驗證過濾邏輯是否正確。
  • 方便未來 UI 改版: 畫面與邏輯解耦後,未來若要把 XML Layout 升級成 Jetpack Compose,只需要重寫 UI 層,ViewModel 的邏輯完全不用改動。

實際讀完後,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 條件不會丟失。
    selectedDayselectedTimeselectedFloor 則由 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
  • 技術分析:這是整個 App 最核心的空教室篩選器,每一間教室資料都要通過三個條件:
    1. 符合大樓(matchesBuilding)
    2. 符合樓層(matchesFloor)
    3. 是否有可用教室(isAvailable)

它接在哪一條流程上

承接 QueryResultActivity.ktQueryResultViewModel.kt 是這樣連接流程的:

QueryResultViewModel.kt
    ↓
收到 building / day / time / floor
    ↓
依查詢條件檢查教室
    ↓
篩選出符合條件的空教室
    ↓
更新 emptyRooms

Hermes Agent 幫我檢查出的重點

QueryResultViewModel 是查詢頁的狀態與邏輯中心:

  • 它從 SavedStateHandle 取得首頁傳來的 building,並接收 QueryResultActivity 寫入的 selectedDayselectedTimeselectedFloor
  • 當所有課表資料與查詢條件都準備好後,update() 會把中文大樓轉成 SF 或 ES,把星期與節次轉成 mon1tueN 這類的 Kotlin 欄位代碼,然後對所有教室進行篩選。
  • 只有大樓符合、樓層符合,而且該時段欄位不是 X 的教室(x代表該時段已被佔用),才會被放進 emptyRooms。

小結

讀到 QueryResultViewModel 時,我才真正看見 RoomRush 查詢空教室的核心。
前面的 Activity 負責收集使用者操作,但真正判斷一間教室能不能顯示在結果列表裡,是在 ViewModel 的 update() 裡完成。
這段邏輯可以拆成三個問題:

  • 這間教室是不是使用者選的大樓?
  • 是不是使用者選的樓層?
  • 在指定星期與節次是不是沒有被佔用?

程式最後用 matchesBuilding && matchesFloor && isAvailable 把三個條件串起來。這讓我理解到,所謂查詢功能其實是把使用者選項轉成明確條件,再一筆一筆過濾資料。

用一句話來記憶,我會說:

QueryResultViewModel 把使用者選的 buildingdaytimefloor 轉成三個篩選條件:大樓符合、樓層符合、該時段不是 X


下一篇預告:查到空教室之後,App 怎麼把結果整理給使用者看?

了解了 QueryResultViewModel.kt 如何將查詢邏輯與狀態串接起來後,接下來的關鍵就是:ViewModel 算出的 emptyRooms 資料陣列,到底是如何在 EmptyRoomAdapter.kt 中被變成畫面上的列表?

在下一篇文章中,我們將拆解 RecyclerView 的渲染細節——看我們如何處理「樓層標題」與「教室卡片」的動態組合,並讓這張列表不只是好看,還具備實質的點擊與互動功能。


上一篇
Day 5 | 使用者選了大樓、星期、節次之後,資料去哪了?
下一篇
Day 7 | 查到空教室之後,App 怎麼把結果整理給使用者看?
系列文
從 Vibe Coding 到可維護專案:用 Hermes Agent 重讀 RoomRush Android 專題7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言