專案: 使用Room儲存資料並在RecyclerView顯示(使用MVVM架構)(1) 建立資料庫

Lin Li
12 min readJun 4, 2023

--

感覺一段時間沒更新了,最近有點忙,也學了很多東西,還來不及消化跟統整,今天就直接用一個專案來當例子。當然這專案會需要具備許多先行知識,例如MVVM架構、DataBinding、ViewModel等等。在之後我會補充相關的基礎筆記。

MVVM是甚麼呢?簡單來說就是一個架構,這邊直接引用這個網站

模組間的關係

模組化最重要的用意是每個物件職責分明,修改時只要更動相應的 Class,也方便 Class 重複使用。

View

負責顯示 UI、監聽動作

包含 Layout 的 XML、Activity 類、Fragment 類、Adapter 類

不處理邏輯,只負責顯示資料,接收到點擊事件就請 ViewModel 處理

View 和 ViewModel 間通常會使用觀察者模式,由 ViewModel 提供 LiveData 給 View 觀察,只要觀察到有資料變動就更新畫面

ViewModel

負責提供畫面所需資料、邏輯判斷

提供 LiveData

依需求向 Model 存取資料

Model

負責提供資料

通常會叫做 xxxxxRepository

若是資料有多個資料來源(來自 API、來自 SQLite),Repository 層可以抽象化,變成一個 interface,就像是官方文件上的那樣。

圖片取自: https://developer.android.com/jetpack/guide

由於我們這個專案算簡單,只會用到本地資料,所以我們分成三個部分:

  • 資料(Database):在這個專案中,資料庫是我們存放所有資料的地方。由於我們只使用本地資料,我們會使用Room來實現SQLite資料庫。所有的數據操作都會在這裡進行,包括查詢、新增、更新和刪除。
  • 儲存庫(Repository):儲存庫在架構中扮演了重要的角色,它在ViewModel和資料庫之間起到了橋梁的作用。通過它,ViewModel可以進行數據的存取,而不直接與資料庫交互。這樣可以讓數據源與其它部分的耦合度降低。
  • 視圖層(ViewModel):ViewModel是MVVM架構的核心部分,它負責向UI層提供必要的數據,並處理來自UI層的所有請求。在這個專案中,我們的ViewModel會與View通過LiveData進行通信,當資料發生變動時,它會通知View進行更新。
圖片來源:https://ithelp.ithome.com.tw/articles/10267000

首先我們先在專案中加上以下依賴:

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt' //添加這行
}
android {
//...省略其他
buildFeatures {
dataBinding = true
}
}
dependencies {
//省略其他依賴
// ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version")
// ViewModel utilities for Compose
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version")
// LiveData
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version")
// Lifecycles only (without ViewModel or LiveData)
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version")

//Room
implementation("androidx.room:room-runtime:$room_version")
kapt("androidx.room:room-compiler:$room_version")
// optional - Kotlin Extensions and Coroutines support for Room
implementation("androidx.room:room-ktx:$room_version")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1")
}

接著我們就先從實現SQLite資料庫開始。

什麼是Room?

用簡單的說法就是能讓你簡單地使用SQLite的庫。詳細可以在官方文檔中查看。根據官方文檔的介紹,分為以下三個部分:

Room 有三個主要元件:

資料庫類別,用於保存資料庫並做為應用程式保留資料基礎連線的主要存取點。

資料實體,代表應用程式資料庫中的資料表。

資料存取物件 (DAO),提供應用程式可用於查詢、更新、插入及刪除資料庫中資料的方法。

在我們的專案中,這三個元件的實作如下:

我們可以創建一個名為db的packege,接著照下列步驟新增這三個檔案。

SubscriberEntity

我們建立一個data class。這個訂閱者資料表有幾個欄位:id、姓名與email。其中id是primary key。

import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "subscriber_data_table")
data class SubscriberEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "subscriber_id")
val id: Int,
@ColumnInfo(name = "subscriber_name")
val name : String,
@ColumnInfo(name = "subscriber_email")
val email : String
)
  • tableName = “subscriber_data_table” : 代表這個資料表的名稱。也可以不設定,此時就會預設為類名。
  • @PrimaryKey(autoGenerate = true) : 這代表自動產生primary key。
  • @ColumnInfo(name = “subscriber_id”) : 代表欄位名稱,不設定的話會自動以變數名稱為欄位名。

SubscriberDao

@Dao
interface SubscriberDAO {
//Insert可以有參數 onConflict
@Insert
suspend fun insertSubscriber(subscriberEntity: SubscriberEntity) : Long

@Update
suspend fun updateSubscriber(subscriberEntity: SubscriberEntity)

@Delete
suspend fun deleteSubscriber(subscriberEntity: SubscriberEntity)

@Query("DELETE FROM subscriber_data_table")
suspend fun deleteAll()

@Query("SELECT * FROM subscriber_data_table")
fun getAllSubscribers() : LiveData<List<SubscriberEntity>>
}

接著我們創建一個介面(interface),用於定義我們會如何與資料庫進行互動。這個介面被稱為資料存取物件 (DAO)。DAO 是一種封裝資料存取的方式,通過方法調用來實現對資料庫的查詢、更新、插入以及刪除等操作。

在我們的案例中,SubscriberDAO 介面定義了我們將會如何對 subscriber_data_table 表進行操作。我們在此介面中定義了一些函數,並使用 Room 提供的註解,如 @Insert、@Update、@Delete 和 @Query,來表示這些函數應該如何操作資料庫。

例如,insertSubscriber 函數使用 @Insert 註解,表示該函數將在資料庫中插入一個 SubscriberEntity。同樣地,deleteSubscriber 和 updateSubscriber 分別使用 @Delete 和 @Update 註解,表示這兩個函數將在資料庫中刪除和更新 SubscriberEntity。

此外,我們還定義了一個使用 @Query 註解的函數 getAllSubscribers,此函數將會執行一個 SQL 查詢語句,從資料庫中檢索所有的 SubscriberEntity,並將結果返回為一個 LiveData 對象。這使我們的 UI 可以觀察該資料並在資料更改時自動更新。

通過這種方式,我們將資料庫操作封裝在 SubscriberDAO 介面中,從而實現了對資料庫的有效抽象,並使其可以更容易地在應用程式中使用。

要特別注意的是,這裡使用了懸掛函數(suspend function),這是 Kotlin 協程架構的一個重要元素。協程的強大之處在於它讓我們能以近似同步的方式撰寫異步程式碼,這意味著當我們在進行某些耗時的非同步操作(例如,對資料庫的操作)時,我們的主線程不會被阻塞。

因為資料庫操作往往需要一些時間來完成,這使得在主線程上直接執行這些操作變得不切實際。如果我們使用的是 Java,我們則需要透過如 Executors、RxJava 等不同的方式來進行異步處理。然而,有了 Kotlin 協程,這些複雜的異步處理方式就變得不再必要。

至於協程的相關知識,我會找時間補充 XD~

SubscriberDatabase

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase

@Database(entities = [SubscriberEntity::class],version = 1)
abstract class SubscriberDatabase : RoomDatabase() {

abstract val subscriberDAO : SubscriberDAO

companion object{
@Volatile
private var INSTANCE : SubscriberDatabase? = null
fun getInstance(context: Context):SubscriberDatabase{
synchronized(this){
var instance = INSTANCE
if(instance==null){
instance = Room.databaseBuilder(
context.applicationContext,
SubscriberDatabase::class.java,
"subscriber_data_database"
).build()
INSTANCE = instance
}
return instance
}
}
}
}

接著我們將建立資料庫類別,此類別繼承自 RoomDatabase。這裡,我們使用 @Database 註解,並將我們剛才定義的 SubscriberEntity 資料實體加入。版本號為 1,當我們的資料庫結構需要變動時,這個版本號就會有所用處,但在這裡我們先不多做解釋,之後有機會再進行補充。

在這個資料庫類別中,我們定義了一個抽象的 SubscriberDAO,這代表我們將使用剛才定義的 DAO 來存取資料庫。

接著,我們在 Companion 物件中定義了一個 getInstance 函數,這個函數是以單例模式來實作的。這是因為,我們通常不會需要在應用程式中創建多個資料庫實例,使用單例模式可以確保在應用程式中整個生命週期裡只會有一個資料庫連線。

如果 INSTANCE 是空的,我們就會用 Room 的 databaseBuilder 方法來創建一個新的資料庫實例,並將這個實例指派給 INSTANCE。否則,我們就直接回傳已經存在的 INSTANCE。

這種寫法是固定的,所以在以後需要建立database時只要將這段複製修改即可。:P

透過這種方式,我們成功創建了一個可以存取我們資料的資料庫,並且確保我們在整個應用程式中都使用相同的資料庫實例。

以上就已完成資料庫的建置。接下來的文章我們會將儲存庫與視圖層做好。

--

--