iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 23
0
Mobile Development

30天,從0開始用Kotlin寫APP系列 第 23

Day 23 | JetPack 與他的產物 - Room (Part 1)

Jetpack

相信在過去幾天的文章有提到多次 Jetpack ,那什麼又是Jetpack 呢?

Android Jetpack was inspired by the Support Library, a set of components to make it easy to take advantage of new Android features while maintaining backwards compatibility

Android Jetpack 是一種 Support Library ,其中包含多種工具(LiveData 、ViewModel、Room ... 等等都是)
那他出現的目的是要讓開發者在開發 App 時更簡單,並且能開發出好容易達到 向後兼容( Maintaining backwards compatibility )的 App

向後兼容為何那麼重要? 因為 Android API 實在是太多了,每個版本出來的時候又或多或少會調整,而 Android 使用者的 API 使用範圍又很大(根據 Google 目前的統計,開發者要支援 Android API 21 到 API 30,才能夠支援九成以上的使用者)
因此開發者有時候必須指定某段 Code 只支援 API 26 以上,而另外一段 Code 是要支援 API 23 以下等等,其實非常麻煩的,畢竟我們都希望同一段 Code 就能支援全部的 API

而 Jetpack 就可以幫助到我們完成這樣的事情,記得在一次 Android 分享會的時候,Google 來的分享者直接帥氣的說只要你依照 Jetpack 的 Best Practice 下去開發 Android 權限,那就不用再擔心版本問題了!

而因為 Jetpack 裡面有太多太多的工具和 Library ,因此這邊就直接上連結,對某個功能有興趣的朋友可以直接到這邊取用

Room

那講完了為什麼會有 Jetpack 和他的能力後,接下來就可以開始介紹他專門處理 SQLite ORM 的小孩 - Room

再 Highlight 一次 Room 能帶來的優點

  • 編譯時會驗證 SQL 是否正確
  • Room 將 SQLite 映射到 POJO ,而沒有使用樣板代碼
  • Room 支援 SQL Migration
  • 可以與 LiveData 、RxJava 等等 Jetpack 的工具一起使用

MVVM + LiveData + Room

在 MVVM 架構中資料流是很重要的核心概念,而資料流就是利用之前提過的 LiveData 達到,透過 Model 提供資料,ViewModelModel 的資料轉成 View 需要的格式,而 View 只要去訂閱 ViewModel ,當 Model 中資料有變時,就會打資料流更新的 Event ,而因為 View 有訂閱這個事件,因此也可以拿到新的資料然後更新 UI

RoomModel 的一部份,也是提供資料流的一個來源,因此他也支援上述的模式,當 DataBase 發現 Table 中資料有變動(有 InsertUpdataDelete 等 Action 發生),便會打 Event 觸發 UI 取得新資料並更新

導入 Room

dependencies {
  implementation "androidx.room:room-runtime:2.2.5"
  kapt "androidx.room:room-compiler:2.2.5"

  // optional - Kotlin Extensions and Coroutines support for Room
  implementation "androidx.room:room-ktx:2.2.5"

  // optional - Test helpers
  testImplementation "androidx.room:room-testing:2.2.5"
}

Room 架構

Room 的架構如圖所示,可以發現主要分3個部份,從小單位到大單位分別是

  1. Entities
  2. Data Access Objects ( DAOs )
  3. Room Database

Entites

Entites 是用類別的方式來定義 Database 裏面的 TableColumn ,其中會用

  • @Entity 裝飾詞定義 Table
  • @ColumnInfo 裝飾詞定義 Column
  • @Ignore 裝飾詞定義 不用存到 Database 的屬性

直接上例子比較直觀,我們可以直接從之前寫好的 Model - PirateInfo.kt 下去改

@Entity	
@JsonClass(generateAdapter = true)
data class PirateInfo(
    @field:Json(name = "id") @PrimaryKey(autoGenerate = true) val id: Long = 0L,
    @field:Json(name = "name") val name: String = "",
    @field:Json(name = "height") val height: Int = 0,
    @field:Json(name = "weight") val weight: Int = 0,
    @field:Json(name = "base_experience") val attack: Int = 0
    @ColumnInfo(name = "create_date") val createDate: Date = Date()
) {
    fun getIdString(): String = String.format("#%03d", id)
    fun getWeightString(): String = String.format("%.1f KG", weight.toFloat() / 10)
    fun getHeightString(): String = String.format("%.1f M", height.toFloat() / 10)
    fun getAttackString(): String = "$attack/$maxAttack"

    companion object {
        const val maxAttack = 1000
    }
}

DAOs

定義出 Entity 之後就是需要定義 SQL 存取方法來取得 Table 內的資料,而這部份就是交給 DAO 來做處理,其中常見方法的就是 CRUD( Insert 、Select 、Update 、Delete)

而這些方法也都有各自的裝飾詞來區分行為

  • @Query(SQL語法) 就是做 Select
  • @Insert(Entity) 對應到新增物件到 Database
  • @Update(Entity) 對應到更新 Database 中物件的內容
  • @Insert(Entity) 對應到刪除 Database 中的物件

而 Room 也會根據這些裝飾詞產生相對應資料庫操作實作,這樣做的優點是 App 跑 Testing 時能透過 Mock data 模擬 Database 操作,讓測試變得更容易

@Dao
interface PirateInfoDao {

  @Insert(onConflict = OnConflictStrategy.REPLACE)
  suspend fun insertPirateInfo(pirateInfo: PirateInfo)

  @Query("SELECT * FROM PirateInfo WHERE id = :id_")
  suspend fun getPirateInfo(id_: Long): LiveData<PirateInfo>
}

Room Database

我們已經完成 Table 和 Sql 的定義,那最後就是要把整個資料庫 Structure 定義起來,比如說資料庫名稱 、版本號 、DAO 、資料庫實體等等

另外在定義資料庫的時候會加上 synchronized ,把實體化的區域 Lock 住直到物件建起來實體後才會釋放,目的是因為正常不太會同時建立到多個 Database 的狀況,另外如果實體還沒建起來就開始 Access Sqlite ,那也會爆出滿滿的 Error

@Database(entities = [PirateInfoDao::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {

    abstract fun pirateInfoDao(): PirateInfoDao

    companion object {
        private const val databaseName = "PirateHegemony"
        private const val TAG = "AppDatabase"

        @Volatile
        private var INSTANCE: AppDatabase? = null

        private fun initDatabase(context: Context): AppDatabase? {
            if (INSTANCE == null) {
                synchronized(this) {
                    val instance = context.applicationContext?.let {
                        Room.databaseBuilder(it, AppDatabase::class.java, databaseName).build()
                    }
                    INSTANCE = instance
                }
            }
            return INSTANCE
        }

        fun getInstance(context: Context): AppDatabase? {
            return INSTANCE ?: initDatabase(context)
        }
    }
}

總結

今天介紹了 Jetpack 的緣由以及 Room 的建立方法,那其實這邊還有地方沒有講到,例如 SQLite 本身沒有支援 Date 的儲存格式,因此要存 Date 到 SQLite 前需要做一個 Convert ,但因為今天時間不太夠,因此留到明天在聊吧

如果內容有任何問題或錯誤,歡迎提出與指教

Reference


上一篇
Day 22 | Android 資料黃金三兄弟 - SharedPeference 、File 、SQLite
下一篇
Day 24 | Jetpack 與他的產物 - Room (Part 2)
系列文
30天,從0開始用Kotlin寫APP30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言