前一篇文章介紹了 EmptyRoomAdapter.kt,我們理解了 Adapter 如何把 emptyRooms 轉化為帶有樓層標題的 RecyclerView 列表。然而,光是顯示列表還不夠,當使用者點擊某個教室時,系統該如何將他帶入下一頁的「教室詳細頁」?
今天要讀的兩個檔案—RoomDetailActivity.kt 與 RoomDetailViewModel.kt,正是來解決這個問題。
值得一提的是,這兩個檔案在職責分工上,與 Day 5、Day 6 所介紹的 QueryResultActivity 及 QueryResultViewModel 有著異曲同工之妙,完美貫徹了 View 與 ViewModel 的架構邊界。
當初決定設計這個詳細頁功能時,想說如果可以點選每個教室的詳細資訊,會讓使用者能更直覺應用,整個 App 也很有互動感。
一開始我在想,依據查詢頁(QueryResult)設計成 View 與 ViewModel 的架構,或許詳細頁(RoomDetail)這部分,也是需要這樣的職責分離,這點果然不出所料。
不過同時在 UI 的設計上,我們也有點不確定,如果單純只有一個頁面,要怎麼接收單一的教室資訊?這點在我和 Hermes Agent 討論後,漸漸清楚裡面的技術細節。
| UI 畫面 | 操作流程 |
|---|---|
![]() |
![]() |
RoomDetailActivity.kt 及 RoomDetailViewModel.kt負責什麼和 Hermes Agent 一重讀檔案後,我了解了這兩個檔案的職責:
RoomDetailActivity.kt(UI 控制器):負責接收上一頁傳來的教室名稱,觀察 ViewModel 查到的教室資料,並把教室名稱、教室類型、是否可飲食顯示到 room_detail.xml 畫面上。RoomDetailViewModel.kt(資料邏輯中心):根據教室名稱(classroomName ),透過 Repository 取得該教室的教室資料(ClassroomSchedule)。承接上個小標,我找到了當時解答接收單一的教室資訊的技術細節:
val classroomName = intent.getStringExtra(EXTRA_CLASSROOM_NAME)
if (classroomName == null) {
finish()
return
}
EmptyRoomAdapter 觸發教室點擊事件後,QueryResultActivity 取得被點擊的教室名稱,並透過 Intent 的 Extra 傳給 RoomDetailActivity。RoomDetailActivity 再使用 getStringExtra(EXTRA_CLASSROOM_NAME) 取得這個名稱。finish() 關閉頁面返回,避免 App 載入空資料造成閃退。Activity 拿到教室名稱後,不直接碰資料庫,而是交給 RoomDetailViewModel 處理:
// ViewModel 透過建構子注入 Repository
class RoomDetailViewModel(
private val repository: ClassroomScheduleRepository
) : ViewModel() {
// 將 Repository 的資料轉為 LiveData 供 Activity 觀察
fun getDetails(classroomName: String): LiveData<ClassroomSchedule?> {
return repository.getClassroomDetails(classroomName).asLiveData()
}
}
RoomDetailViewModel 不直接操作資料庫,而是透過 Repository 取得教室資料,再使用 .asLiveData() 將資料流轉換成 LiveData,供 Activity 觀察。Repository,因此建立 ViewModel 時會透過 Factory 將 Repository 傳入。
.asLiveData()的小流程:
Repository
↓
Flow<ClassroomSchedule?>
↓ asLiveData()
LiveData<ClassroomSchedule?>
↓
Activity observe
private fun observeClassroomDetails(name: String) {
viewModel.getDetails(name).observe(this, Observer { schedule ->
if (schedule != null) {
updateUi(schedule)
}
})
}
updateUi() 更新畫面。private fun updateUi(schedule: ClassroomSchedule) {
classroomNameTextView.text = schedule.classroom
classroomTypeValueTextView.text = when (schedule.classroomType) {
"Normal" -> "一般教室"
"Computer" -> "電腦教室"
"Drawing" -> "製圖教室"
"Lab" -> "實驗室"
"MeetingRoom" -> "研討室"
else -> "未知"
}
foodAllowedValueTextView.text = if (schedule.classroomType == "Normal") "是" else "否"
}
classroomType 為 Normal 的一般教室顯示為「可飲食」,其他類型則顯示「否」)。讓冰冷的數據變成使用者直覺可讀的畫面。先來看 RoomDetailActivity.kt 的流程,這裡的重點放在「接收教室 → 建立 ViewModel → 觀察資料 → 更新畫面」:
QueryResultActivity
↓
傳遞 classroomName
↓
RoomDetailActivity
↓
取得 classroomName
↓
呼叫 RoomDetailViewModel
↓
Observe ClassroomSchedule
↓
updateUi() 顯示教室詳細資訊
RoomDetailViewModel.kt 的流程,重點放在「拿到 classroomName 後,如何取得資料」:
RoomDetailActivity
↓
傳入 classroomName
↓
RoomDetailViewModel
↓
呼叫 Repository
↓
取得課表資料流
↓
轉換為 LiveData
↓
回傳 ClassroomSchedule
RoomDetailActivity 是教室詳細頁的 UI 控制器。
classroomName,如果沒有取得教室名稱,就直接 finish() 關閉頁面。viewModel.getDetails(name) 並觀察回傳的資料。ClassroomSchedule 後,updateUi() 會顯示教室名稱,將 classroomType 轉成中文,並依照是否為 Normal 判斷能否飲食。RoomDetailViewModel 是教室詳細頁的資料邏輯中心。
ClassroomScheduleRepository,因為 ViewModel 不直接查 DAO 或 Room Database,而是透過 Repository 取得資料。getDetails(classroomName) 會把教室名稱交給 Repository 查詢該教室的 ClassroomSchedule,並用 asLiveData() 將 Repository 回傳的資料流轉成 LiveData<ClassroomSchedule?>,讓 RoomDetailActivity 可以 observe。RoomDetailViewModelFactory 來建立。讀到 RoomDetailActivity 和 RoomDetailViewModel 後,我看到 RoomRush 查詢流程的最後一步:使用者不是只看到空教室列表,還可以點進某一間教室看詳細資訊。
Activity 不直接查資料庫,它負責接收資料、觀察 ViewModel、更新畫面,但不直接查資料庫。教室類型從英文代碼轉成中文,是否可飲食也在 updateUi() 裡轉成使用者看得懂的文字。
ViewModel 也不直接碰 DAO,而是透過 Repository 取得資料。Repository 回傳的資料流再用 asLiveData() 轉成 Activity 可以觀察的形式。
用各一句話來總結這兩份檔案:
RoomDetailActivity從 Intent 取得被點擊的教室名稱,觀察 ViewModel 查到的資料,再用updateUi()顯示教室名稱、類型與是否可飲食。
RoomDetailViewModel不顯示畫面,只負責根據教室名稱(classroomName)透過 Repository 取得教室資料,並轉成LiveData給 Activity 觀察。
到目前為止,我們已經把 RoomRush 從首頁到詳細頁的查詢流程大致串起來了。然而下一篇,我想要倒回去QueryResultViewModel.kt 裡,我們有說明,程式會判斷某間教室在指定時段是不是可以使用,判斷的其中一個條件,是看課表資料裡的欄位是不是:『X』。
我想要深入探討這細節,因為當初我在和 Hermes Agent 一起重讀這個部份時,我很容易搞混。如果這個『X』的邏輯判斷寫錯,整套空教室演算法算出來的結果就會完全顛倒。