使用 Room library 即可輕鬆在 Android app 中使用 database。Room 也稱為 ORM (Object Relational Mapping) library,顧名思義,就是將 relational database 中的 table 對應至可在 Kotlin 程式碼中使用的 objects。在本課程中,您只需要關注讀取 data。使用預先填入的 database,您就能載入公車抵達時間 table 中的 data,並在
RecyclerView
中呈現這些 data。

在課程中,您將瞭解使用 Room 的基礎知識,包括 database class、DAO、實體(entities)和 view models。此外,課程中也會介紹 ListAdapter
class,讓您透過另一種方式在 RecyclerView
中呈現 data;以及 Flow,這是一種類似於 LiveData
的 Kotlin 語言功能,可使 UI 針對 database 變更做出 response。
學習目標
- 將 database table 以 Kotlin objects (實體(entities)) 表示。
- 定義要用於在 app 中使用 Room 的 database class,並從檔案預先填入 database。
- 定義 DAO class,並使用 SQL 查詢從 Kotlin 程式碼存取 database。
- 定義 view models,以允許 UI 與 DAO 互動。
- 瞭解如何在 recycler view 中使用
ListAdapter
。 - 瞭解 Kotlin Flow 的基本概念,以及學習如何實際運用,讓 UI 對基礎 data 的變更做出 response。
開始操作
在本程式碼研究室中,您要使用的 app 稱為 Bus Schedule。此 app 會顯示公車站(bus stops) list,以及從最早到最晚的抵達時間(arrival times)。

輕觸第一個畫面中的任意一列可開啟新畫面,只顯示所選公車站的公車即將抵達時間。

bus stop data 來自 app 預先封裝(prepackaged)的 database。不過,在目前的狀態下,app 首次執行時不會顯示任何內容。您的工作是整合 Room,讓 app 顯示預先填入(prepopulated)的 arrival times database。
範例程式碼網址:https://github.com/google-developer-training/android-basics-kotlin-bus-schedule-app/tree/starter
分支版本:starter
新增 Room dependency
和任何其他 library 一樣,您必須先新增必要的 dependency,才能在 Bus Schedule app 中使用 Room。這只需要兩個小幅變更,每個 Gradle 檔案中各一個。
- 在 project-level 的
build.gradle
檔案中,請在ext
區塊中定義room_version
。
1 | ext { |
- 在 app-level 的
build.gradle
檔案中,請於 dependency list 的結尾新增下列 dependency。
1 | implementation "androidx.room:room-runtime:$room_version" |
- 請同步(sync)處理變更並建構 project,確認是否已正確新增 dependency。
sync 後會產生以下 error:
重要:sync 後,會產生 Could not find method kapt() for arguments error。參考 issue,需要在 app-level build.gradle
檔案的 plugins 中新增 id 'kotlin-kapt'
:
1 | plugins { |
最後重新 sync 即可解決。
在接下來的幾個頁面中,我們會介紹將 Room 整合至 app 所需的 components:
- models
- DAO
- view models
- database class
建立實體(entity)
在先前的程式碼研究室中瞭解 relational databases 後,您便知道系統如何將 data 整理成含有多欄(columns)的 table 其中每欄(column)都代表特定 data type 的特定屬性。如同 Kotlin 中的 class 為每個 object 都提供一個 template,database 中的 table 也會為其中每個項目(item)或每列(row)各提供一個 template。因此,Kotlin class 可用於代表 database 中的每個 table,這點並不讓人意外。
使用 Room 時,每個 table 都以一個 class 表示。在 Room 等 ORM (Object Relational Mapping) library 中,通常稱為 model classes 或實體(entities)。
Bus Schedule app 的 database 只包含「schedule」這一個 table,當中包含 bus arrival 的部分基本資訊。
id
:提供 unique ID 做為主鍵的 integerstop_name
:stringarrival_time
:integer
請注意, database 中使用的 SQL type 實際上對於 Int
是 INTEGER
,對於 String
是 TEXT
。不過,使用 Room 時,您應該只在定義 model classes 時考慮 Kotlin type。系統會自動將 model class 的 data type 對應至 database 使用的 data type。
如果 project 含有多個檔案,建議您將檔案整理成不同的 packages,以便為各個 class 提供更好的存取權控管(access control),並更輕鬆地找出相關 class。
如要為 schedule
table 建立實體(entity),請在 com.example.busschedule
package 中加入名為 database
的新 package。在這個 package 中,為實體(entity)新增名為 schedule
的新 package。接著,在 database.schedule
package 中建立名為 Schedule.kt
的新檔案,並定義名為 Schedule
的 data class。
1 | data class Schedule( |
如 SQL 基礎課程所述,table 應有一個主鍵(primary key),用來識別每個 rows。您要新增至 Schedule
class 的第一個屬性是 integer,代表專屬 ID(uniquely identify)。新增一個新屬性,並為其標示 @PrimaryKey
註解。此項操作會指示 Room 在插入新 row 時,將這個屬性視為主鍵(primary key)。
1 | val id: Int |
為 bus stop 的名稱 新增一欄(column)。該 column 的 type 應為 String
。如果是新 column,則需要新增 @ColumnInfo
註解來指定該 column 的 name。通常,與 Kotlin 屬性使用的 lowerCamelCase
不同,SQL column name 的字詞以底線分隔。我們也不允許這個 column 中的 value 為空值(null),請使用 @NonNull
註解加以標示。
1 | val stopName: String, |
注意:在 SQL 中,根據預設,column 的 value 可以是空值(null),但您可以視需要另外將 value 標示為非空值(non null)。這不同於 Kotlin 的運作方式,在 Kotlin 中,根據預設,value 不可為空值(null)。
Arrival times 會在 database 中以 integers 表示。這是 Unix timestamp,可轉換成可用日期(date)。雖然不同版本的 SQL 都提供了轉換日期(dates)的方式,但您仍可按照自己的需要使用 Kotlin date formatting functions。將下列 @NonNull
column 新增至 model class。
1 | val arrivalTime: Int |
最後,為了讓 Room 辨識出這個 class 可用於定義 database table,您需要為 class 本身新增一個註解。在 class name 之前的行中新增 @Entity
。
根據預設,Room 會使用 class name 做為 database table name。因此,目前 class 定義的 table name 就是 Schedule
。另外,您也可以選擇指定 @Entity(tableName="schedule")
,但由於 Room 查詢不區分大小寫,因此可以不使用明確定義小寫的 table 名稱。
現在,schedule 實體(entity)的 class 應如下所示。
1 |
|
定義 DAO
需要新增以整合 Room 的下一個 class 是 DAO。DAO 代表 Data Access Object,是一種可供存取 data 的 Kotlin class 。具體來說,DAO 包含用來讀取及操控 data 的 function。在 DAO 上呼叫 function 相當於在 database 上執行 SQL 指令。事實上,DAO function 就像您在此 app 中定義的 function 一樣,通常會指定 SQL 指令,以便您準確指定希望該 function 執行的操作。定義 DAO 時,在先前程式碼研究室中瞭解的 SQL 知識可派上用場。
- 請為 Schedule entity 新增 DAO class。在
database.schedule
package 中,建立名為ScheduleDao.kt
的新檔案,並定義名為ScheduleDao
的 interface。與Schedule
class 相似,您必須加上註解@Dao
,才能在 Room 中使用 interface。
1 |
|
注意:雖然 DAO 是首字母縮寫,但 Kotlin 程式碼的命名慣例只會將首字母縮寫中的第一個字母大寫,因此請使用 ScheduleDao 名稱,而非 ScheduleDAO 名稱。
- App 有兩個畫面,每個畫面都需要不同的查詢。第一個畫面會依 arrival time 遞增排序所有 bus stops。在這種情況下,查詢只需要取得所有 columns,並加入適當的
ORDER BY
子句。查詢以傳遞至@Query
註解的 string 形式指定。 - 定義 function
getAll()
,該 function 會 returnSchedule
list,其中包括@Query
註解,如下所示。
- App 有兩個畫面,每個畫面都需要不同的查詢。第一個畫面會依 arrival time 遞增排序所有 bus stops。在這種情況下,查詢只需要取得所有 columns,並加入適當的
1 |
|
- 對於第二個查詢,同樣建議您,從 schedule table 中選取所有 columns。不過請注意,您只需符合所選(selected) stop name 的結果,因此請新增
WHERE
子句。如要引用查詢的 Kotlin value,請在查詢前加上冒號:
(例如來自 function 參數的:stopName
)。和以前一樣,結果會依 arrival time 遞增排序。 - 定義
getByStopName()
function ,該 function 採用名為stopName
的參數String
,並 returnSchedule
object 的List
,包含@Query
註解,如下所示。
- 對於第二個查詢,同樣建議您,從 schedule table 中選取所有 columns。不過請注意,您只需符合所選(selected) stop name 的結果,因此請新增
1 |
|
定義 ViewModel
現在,DAO 已設定完畢,從技術面來說,您已備齊所有必要項目,必須開始透過 fragments 存取 database。然而,雖然這在理論上可行,但通常不被視為最佳做法。這是因為在較複雜的 app 中,可能有多個畫面只會存取 data 的特定部分。雖然 ScheduleDao
相對簡單,但在面對兩個或更多不同的畫面時,很容易會失控。舉例來說,DAO 看起來大致如下:
1 |
|
雖然畫面 1 的程式碼可以存取 getForScreenOne()
,但該程式碼不適合存取其他 method。最佳做法是將顯示在 view 的 DAO 部分分隔成獨立 class ,稱為「view model」。這是行動 app 中常見的架構模式。使用 view model 有助於更清晰地區分 app UI 及其 data model 的程式碼。也有助於單獨測試程式碼的各個部分,隨著您繼續 Android 開發之旅,之後便會深入探索這個主題。

如果使用 view model,您可以利用 ViewModel
class。ViewModel
class 用於儲存與 app UI 相關的 data,並且具有生命週期感知特性,對於 lifecycle events 的 response 與 activity 或 fragments 極為相似。如果畫面旋轉等 lifecycle events 會導致 activity 或 fragments 被刪除並重新建立,則不需要重新建立相關聯的 ViewModel
。您無法直接存取 DAO class,因此最好使用 ViewModel
子類別,將 loading data 工作與 activity 或 fragments 區分開。
注意:Bus Schedule 是一個相對簡單的 app ,只包含兩個畫面且內容大致相同。基於教學目的,我們將建立單個 view model class,可供兩個畫面使用,但在大型 app 中,則可以針對各個 fragments 使用單獨的 view model。
- 如要建立 view model class,請在名為
viewmodels
的新 package 中建立名為BusScheduleViewModel.kt
的新檔案。定義 view model 的 class。該 class 應採用ScheduleDao
type 的單一參數。
1 | class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() { |
- 由於這個 view model 會同時用於兩個畫面,因此您需要新增 method 來取得完整 schedule 和依照 stop name 進行篩選的 schedule。做法是呼叫
ScheduleDao
的對應 method。
1 | fun fullSchedule(): List<Schedule> = scheduleDao.getAll() |
雖然已經完成 view model 的定義,但您不能直接將 BusScheduleViewModel
實例化(instantiate),並預期一切運作正常。由於 ViewModel
class BusScheduleViewModel
必須有生命週期感知特性,因此應以可回應 lifecycle events 的 object 進行實例化(instantiate)。
如果直接在其中一個 fragments 中執行實例化(instantiate),則 fragments object 必須處理一切 (包括所有記憶體管理工作),而這超出 app 程式碼的工作範圍。您可以改為建立名為 factory 的 class,該 class 會替您將 view model object 實例化(instantiate)。
- 如要建立 factory,請在 view model class 下方建立繼承自
ViewModelProvider.Factory
的新 classBusScheduleViewModelFactory
。
1 | class BusScheduleViewModelFactory( |
- 您只需要一個樣板程式碼,就能正確對 view model 執行實例化(instantiate)。您不必直接初始化 class,而是可以覆寫名為
create()
的 method,該 method 會 returnBusScheduleViewModelFactory
以及一些錯誤檢查(error checking)。在BusScheduleViewModelFactory
class 中實作create()
,如下所示。
1 | override fun <T : ViewModel> create(modelClass: Class<T>): T { |
- 在 class 上
control + o
,即可選擇要覆寫的 method
現在您可以使用 BusScheduleViewModelFactory.create()
對 BusScheduleViewModelFactory
object 執行實例化(instantiate),這樣一來,您的 view model 便可具有生命週期感知特性,而無需 fragments 直接處理這項工作。
建立 database class 及 pre-populate database
目前您已定義 models、DAO 及 view model,以供 fragments 存取 DAO,但您還需要指示 Room 如何處理所有這些 classes。此時 AppDatabase
class 便可派上用場。一款使用 Room 的 Android app (例如您自己的 app),可分類 RoomDatabase
class ,並具有幾項主要工作。在您的 app 中,AppDatabase
需要:
- 指定 database 中定義的 entities。
- 提供對各個 DAO class 的 single instance 的存取。
- 執行任何其他設定,例如預先填入資料庫(pre-populate database)。
您可能想知道為什麼 Room 無法找到所有 entities 和 DAO object,很有可能是您的 app 擁有多個 database,或是存在許多場景(scenarios),在這些場景下,library 無法假設您或開發人員的意圖(intent)。透過 AppDatabase
class,您可完全控管 models、DAO class,以及您想執行的任何 database 設定。
- 如要新增
AppDatabase
class,請在database
package 中,建立名為AppDatabase.kt
的新檔案,並定義繼承自RoomDatabase
的新 abstract classAppDatabase
。
1 | abstract class AppDatabase: RoomDatabase() { |
- database class 可方便其他 class 存取
DAO
class。新增一個 abstract function,該 function 會 returnScheduleDao
。
1 | abstract fun scheduleDao(): ScheduleDao |
- 使用
AppDatabase
class時,建議您確保畫面上只有一個 database instance,以避免出現競爭狀況或其他潛在問題。instance 儲存在 companion object 中,您也必須備妥一個 method,該方法不是 return 現有 instance,就是首次建立 database。必須在 companion object 中定義此項。請在scheduleDao()
function 正下方新增下列companion object
。
1 | companion object { |
筆記:companion object 只會存在一個 instance,也稱為 Singleton Pattern,目的為保證一個 class 只會產生一個 object,而且要提供存取該 object 的 method。
在 companion object
中,新增 type 為 AppDatabase
的屬性 INSTANCE
。這個 value 最初設為 null
,因此 type 標有 ?
標記。此外還標有 @Volatile
標記。本課程將詳細說明如何使用 volatile 屬性。不過,還是建議您將其用於 AppDatabase
instance,以免發生潛在錯誤。
1 |
|
在 INSTANCE
屬性下方,定義一個 function 以 return AppDatabase
instance:
1 | fun getDatabase(context: Context): AppDatabase { |
在 getDatabase()
的實作中,如果已有 instance,可以使用 Elvis 運算子 return database 的現有 instance,如果尚未有 instance 則首次建立 database。在這個 app 中,由於系統已預先填入 data,您也可呼叫 createFromAsset()
來載入現有 data。您可以在 project 的 assets.database
package 中找到 bus_schedule.db
檔案。
- database class 就像 model class 和
DAO
一樣,需要註解來提供特定資訊。所有 entities type (使用ClassName::class
存取 type 本身) 都會列在 array 中。database 也會提供 version number,請將這個 value 設為1
。請按照下列方式新增@Database
註解。
1 |
|
注意:每次變更 database 結構時,version number 就會遞增。app 會檢查這個版本與 database 中的版本,判斷是否應執行遷移(migration)以及遷移(migration)方式。
您已建立 AppDatabase
class,只要再完成一個步驟就能開始使用。您需要提供 Application
class 的自訂子類別,並建立 lazy
屬性來存放 getDatabase()
的結果。
- 在
com.example.busschedule
package 中,新增名為BusScheduleApplication.kt
的 class,並繼承Application
class。
1 | class BusScheduleApplication : Application() { |
- 新增
database
屬性(type 為AppDatabase
)。這個屬性應延遲處理,並 return 在AppDatabase
class 上呼叫getDatabase()
的結果。
1 | class BusScheduleApplication : Application() { |
- 最後,為了確保使用
BusScheduleApplication
class (而非預設的 base classApplication
),您必須稍微變更 manifest。在AndroidMainifest.xml
中,將android:name
屬性設為com.example.busschedule.BusScheduleApplication
。
1 | <application |
這時就可以設定 app 的 model 了。您可以開始透過 UI 使用 Room 的 data。在接下來幾頁中,您需要為 app 的 RecyclerView
建立 ListAdapter
,藉此顯示 bus schedule data,並以動態(dynamically)方式回應 data 變更。
建立 ListAdapter
現在,我們需要完成一些艱鉅的工作,並將 model 彙整到 view 中。以往使用 RecyclerView
時,您需要使用 RecyclerView
。Adapter
可顯示靜態(static) data list。儘管特別適用於 Bus Schedule 等 app,但使用 database 時,往往遇到的都是即時(real time)處理 data 的變更。即使只有一個 item 的內容有變更,系統仍會重新整理整個 recycler view。但對大多數使用持續性的 app 來說,這種做法並不夠。
動態變更 list 的替代方案稱為 ListAdapter
。ListAdapter
使用 AsyncListDiffer 來判斷舊 data list 和新 data list 之間的差異。之後,系統再根據這兩份 list 之間的差異來更新 recycler view。因此,處理頻繁更新的 data 時,recycler view 的執行效能會較高,就像在 database app 中經常遇到的情況一樣。

由於這兩個畫面的 UI 都相同,因此您只需要建立可同時用於這兩個畫面的單個 ListAdapter
即可。
- 建立新 class
BusStopAdapter.kt
。該 class 會繼承一個 genericListAdapter
,其中會列出 UI 的Schedule
object list 和BusStopViewHolder
class。 - 對於
BusStopViewHolder
,您也會傳遞即將定義的DiffCallback
type。 BusStopAdapter
class 本身也會採用參數onItemClicked()
。系統會在第一個畫面選取 item 時,使用此功能來處理 navigation,但在第二個畫面中,您只需傳遞 empty function 即可。
- 建立新 class
1 | class BusStopAdapter(private val onItemClicked: (Schedule) -> Unit) : ListAdapter<Schedule, BusStopAdapter.BusStopViewHolder>(DiffCallback) { |
- import
androidx.recyclerview.widget.ListAdapter
- 與 recycler view adapter 類似,您需要具備 view holder,才能在程式碼中存取透過 layout 檔案建立的 view。接著只要建立
BusStopViewHolder
class 並實作bind()
function,即可將stopNameTextView
的 text 設為 stop name,並將arrivalTimeTextView
的 text 設為格式化日期(formatted date)。
1 | class BusStopViewHolder(private var binding: BusStopItemBinding): RecyclerView.ViewHolder(binding.root) { |
- 覆寫並實作
onCreateViewHolder()
,加載 layout,並將onClickListener()
設定為針對目前位置(position)的 item 呼叫onItemClicked()
。
1 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BusStopViewHolder { |
- 覆寫並實作
onBindViewHolder()
,同時在指定位置(position) bind view。
1 | override fun onBindViewHolder(holder: BusStopViewHolder, position: Int) { |
- 記得您為
ListAdapter
指定的DiffCallback
class 嗎?這只是一個 object,能夠協助ListAdapter
在更新 list 時確定新舊 list 中哪些 item 不同。為此,有以下兩種 method:
areItemsTheSame()
透過僅檢查 ID 來確認 object (database 的 row) 是否相同。areContentsTheSame()
會檢查所有屬性 (不只是 ID) 是否相同。這些方法可讓ListAdapter
判斷 inserted、updated 及 deleted 哪些 item,以便更新相應 UI。
新增 companion object,然後實作 DiffCallback
,如下所示。
1 | companion object { |
這就是設定 adapter 的全部內容。您將在 app 的兩個畫面中使用。
- 首先,您需要在
FullScheduleFragment.kt
中取得 view model 的引用。
1 | private val viewModel: BusScheduleViewModel by activityViewModels { |
- 接著在
onViewCreated()
中,加入以下程式碼,以設定 recycler view,並指派其 layout manager。
1 | recyclerView = binding.recyclerView |
- 然後指派 adapter 屬性。傳入的動作(action)會使用
stopName
navigate 所選的下一個畫面,以便篩選 bus stops list。
1 | val busStopAdapter = BusStopAdapter({ |
- 最後,如要更新 list view,請呼叫
submitList()
,然後傳入該 view model 的 bus stops list。
1 | // submitList() is a call that accesses the database. To prevent the |
- 在
StopScheduleFragment
中執行相同動作。首先,取得 view model 的引用。
1 | private val viewModel: BusScheduleViewModel by activityViewModels { |
- 然後在
onViewCreated()
中設定 recycler view。這次,您只要使用{}
傳入 empty block (function) 即可。輕觸這個畫面中的 row 時,最好不會出現任何動作。
1 | recyclerView = binding.recyclerView |
- Adapter 設定完畢之後,Room 便整合到 Bus Schedule app 中。花點時間執行該 app,畫面中會顯示 arrival times list。只要輕觸任一 row,即可前往 detail 畫面。


使用 Flow 回應 data 變更
雖然已設定 list view,如果呼叫了 submitList()
,系統就能有效處理 data 變更,但您的 app 目前仍無法處理動態更新(dynamic updates)。如果想親眼看一看,您可以嘗試開啟 App Inspector,並執行下列查詢,以便在 schedule
table 中插入新 item。
1 | INSERT INTO schedule |
- 請注意,系統不會在模擬器中執行任何作業。使用者會假設 data 並未變更。您需要重新執行 app ,才能看到這些變更。
問題是每個 DAO function 只 return List<Schedule>
一次。即使更新基礎 data,系統也不會呼叫 submitList()
來更新 UI,而從使用者的角度來看,就好像什麼也沒發生。
如要修正此問題,您可以利用名為 asynchronous flow 的 Kotlin 功能 (通常簡稱為 Flow),此功能可讓 DAO 持續從 database 發出 data。如果 inserted、updated 或 deleted item,系統就會將結果傳回 fragment。
使用名為 collect()
的 function 時,您可以使用 Flow 發出的新 value 呼叫 submitList()
,藉此讓 ListAdapter
根據新 data 更新 UI。
- 如要在 Bus Schedule 中使用 Flow,請開啟
ScheduleDao.kt
。如要轉換 DAO function 以 return Flow,只要將getAll()
function 的 return type 變更為Flow<List<Schedule>>
即可。
1 | // 用 Flow<> 包起來,使 DAO 持續從 database 發出 data |
- import
kotlinx.coroutines.flow.Flow
- 同樣地,請更新
getByStopName()
function 的 return value。
1 | // 用 Flow<> 包起來,使 DAO 持續從 database 發出 data |
- 此外,也要更新 view model 中存取
DAO
的 function。將fullSchedule()
和scheduleForStopName()
的 return value 更新為Flow<List<Schedule>>
。
1 | class BusScheduleViewModel(private val scheduleDao: ScheduleDao): ViewModel() { |
- 最後,在
FullScheduleFragment.kt
中,當您根據查詢結果呼叫collect()
時,系統應更新busStopAdapter
。由於fullSchedule()
是 suspend function,因此需要從 coroutine 中呼叫。取代這一行內容。
1 | busStopAdapter.submitList(viewModel.fullSchedule()) |
這段程式碼會使用 fullSchedule()
return 的 Flow。
1 | lifecycle.coroutineScope.launch { |
- 在
StopScheduleFragment
中執行相同作業,但請將scheduleForStopName()
呼叫改成以下內容。
1 | lifecycle.coroutineScope.launch { |
- 完成上述變更後,您可以重新執行 app,驗證系統現在是否即時(real time)處理 data 變更。App 執行完畢後,請返回 App Inspector,並傳送下列查詢,在 11:00 PM 前插入新的 arrival time。
1 | INSERT INTO schedule |
新 item 隨即會顯示在 list 頂端。

總結
- SQL database 中的 tables 會在 Room 中以稱為 entities 的 Kotlin classes 表示。
- DAO(Data Access Object) 會提供對應於與 database 互動的 SQL 指令的 methods。
ViewModel
是一種 lifecycle 感知 component,可將 app data 與 view 分隔。AppDatabase
class 會指示 Room 要使用哪些 entities、提供 DAO 的存取權,並在建立 database 時執行任何設定。ListAdapter
是搭配RecyclerView
使用的 adapter,非常適合處理動態更新的 list。- Flow 是一種 Kotlin 功能,可用於 return data stream,可與 Room 搭配使用,確保 UI 和 database 保持同步(sync)。