Jetpack之Room的使用,結合Flow

語言: CN / TW / HK

本文主要還是參考官方文件,然後以儲存搜尋歷史為例操作一波。

準備工作

RoomSQLite 上提供了一個抽象層,以便在充分利用 SQLite 的強大功能的同時,能夠流暢地訪問資料庫。

依賴

如需在應用中使用 Room,請將以下依賴項新增到應用的 build.gradle 檔案。

dependencies {
  def room_version = "2.2.5"

  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"

  // optional - Test helpers
  testImplementation "androidx.room:room-testing:$room_version"
}
複製程式碼

主要元件

  • 資料庫:包含資料庫持有者,並作為應用已保留的持久關係型資料的底層連線的主要接入點。 使用 @Database註釋的類應滿足以下條件:
    • 是擴充套件 RoomDatabase 的抽象類。
    • 在註釋中新增與資料庫關聯的實體列表。
    • 包含具有 0 個引數且返回使用@Dao註釋的類的抽象方法。
    在執行時,您可以通過呼叫 Room.databaseBuilder()Room.inMemoryDatabaseBuilder() 獲取 Database 的例項。
  • Entity:表示資料庫中的表。
  • DAO:包含用於訪問資料庫的方法。

應用使用 Room 資料庫來獲取與該資料庫關聯的資料訪問物件 (DAO)。然後,應用使用每個 DAO 從資料庫中獲取實體,然後再將對這些實體的所有更改儲存回資料庫中。 最後,應用使用實體來獲取和設定與資料庫中的表列相對應的值。

關係如圖: 在這裡插入圖片描述

ok,基本概念瞭解之後,看一下具體是怎麼搞的。

Entity

@Entity(tableName = "t_history")
data class History(

    /**
     * @PrimaryKey主鍵,autoGenerate = true 自增
     * @ColumnInfo 列 ,typeAffinity 欄位型別
     * @Ignore 忽略
     */

    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER)
    val id: Int? = null,

    @ColumnInfo(name = "name", typeAffinity = ColumnInfo.TEXT)
    val name: String?,

    @ColumnInfo(name = "insert_time", typeAffinity = ColumnInfo.TEXT)
    val insertTime: String?,

    @ColumnInfo(name = "type", typeAffinity = ColumnInfo.INTEGER)
    val type: Int = 1
)
複製程式碼
  • Entity物件對應一張表,使用@Entity註解,並宣告你的表名即可
  • @PrimaryKey 主鍵,autoGenerate = true 自增
  • @ColumnInfo 列,並宣告列名 ,typeAffinity 欄位型別
  • @Ignore 宣告忽略的物件

很簡單的一張表,主要是nameinsertTime欄位。

DAO

@Dao
interface HistoryDao {

    //按型別 查詢所有搜尋歷史
    @Query("SELECT * FROM t_history WHERE type=:type")
    fun getAll(type: Int = 1): Flow<List<History>>

    @ExperimentalCoroutinesApi
    fun getAllDistinctUntilChanged() = getAll().distinctUntilChanged()

    //新增一條搜尋歷史
    @Insert
    fun insert(history: History)

    //刪除一條搜尋歷史
    @Delete
    fun delete(history: History)

    //更新一條搜尋歷史
    @Update
    fun update(history: History)

    //根據id 刪除一條搜尋歷史
    @Query("DELETE FROM t_history WHERE id = :id")
    fun deleteByID(id: Int)

    //刪除所有搜尋歷史
    @Query("DELETE FROM t_history")
    fun deleteAll()
}
複製程式碼
  • @Insert:增
  • @Delete:刪
  • @Update:改
  • @Query:查

這裡有一個點需要注意的,就是查詢所有搜尋歷史返回的集合我用Flow修飾了。

只要是資料庫中的任意一個數據有更新,無論是哪一行資料的更改,那就重新執行 query 操作並再次派發 Flow

同樣道理,如果一個不相關的資料更新時,Flow 也會被派發,會收到與之前相同的資料。

這是因為 SQLite 資料庫的內容更新通知功能是以表 (Table) 資料為單位,而不是以行 (Row) 資料為單位,因此只要是表中的資料有更新,它就觸發內容更新通知。Room 不知道表中有更新的資料是哪一個,因此它會重新觸發 DAO 中定義的 query 操作。您可以使用 Flow 的操作符,比如 distinctUntilChanged 來確保只有在當您關心的資料有更新時才會收到通知。

    //按型別 查詢所有搜尋歷史
    @Query("SELECT * FROM t_history WHERE type=:type")
    fun getAll(type: Int = 1): Flow<List<History>>

    @ExperimentalCoroutinesApi
    fun getAllDistinctUntilChanged() = getAll().distinctUntilChanged()
複製程式碼

資料庫

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

    abstract fun historyDao(): HistoryDao

    companion object {
        private const val DATABASE_NAME = "history.db"
        private lateinit var mPersonDatabase: HistoryDatabase

        //注意:如果您的應用在單個程序中執行,在例項化 AppDatabase 物件時應遵循單例設計模式。
        //每個 RoomDatabase 例項的成本相當高,而您幾乎不需要在單個程序中訪問多個例項
        fun getInstance(context: Context): HistoryDatabase {
            if (!this::mPersonDatabase.isInitialized) {
                //建立的資料庫的例項
                mPersonDatabase = Room.databaseBuilder(
                    context.applicationContext,
                    HistoryDatabase::class.java,
                    DATABASE_NAME
                ).build()
            }
            return mPersonDatabase
        }
    }

}
複製程式碼
  • 使用@Database註解宣告
  • entities 陣列,對應此資料庫中的所有表
  • version 資料庫版本號

注意:

如果您的應用在單個程序中執行,在例項化 AppDatabase 物件時應遵循單例設計模式。 每個 RoomDatabase 例項的成本相當高,而您幾乎不需要在單個程序中訪問多個例項。

使用

在需要的地方獲取資料庫

mHistoryDao = HistoryDatabase.getInstance(this).historyDao()
複製程式碼

獲取搜尋歷史

    private fun getSearchHistory() {
        MainScope().launch(Dispatchers.IO) {
            mHistoryDao.getAll().collect {
                withContext(Dispatchers.Main){
                    //更新ui
                }
            }
        }
    }
複製程式碼

collectFlow獲取資料的方式,並不是唯一方式,可以檢視文件

為什麼放在協程裡面呢,因為資料庫的操作是費時的,而協程可以輕鬆的指定執行緒,這樣不阻塞UI執行緒。

檢視Flow原始碼也發現,Flow是協程包下的

package kotlinx.coroutines.flow
複製程式碼

以collect為例,也是被suspend 修飾的,既然支援掛起,那配合協程豈不美哉。

    @InternalCoroutinesApi
    public suspend fun collect(collector: FlowCollector<T>)
複製程式碼

儲存搜尋記錄

    private fun saveSearchHistory(text: String) {
        MainScope().launch(Dispatchers.IO) {
            mHistoryDao.insert(History(null, text, DateUtils.longToString(System.currentTimeMillis())))
        }
    }
複製程式碼

清空本地歷史

    private fun cleanHistory() {
        MainScope().launch(Dispatchers.IO) {
            mHistoryDao.deleteAll()
        }
    }
複製程式碼

作者:yechaoa

資料庫升級

資料庫升級是一個重要的操作,畢竟可能會造成資料丟失,也是很嚴重的問題。

Room通過Migration類來執行升級的操作,我們只要告訴Migration類改了什麼就行,比如新增欄位或表。

定義Migration類

    /**
     * 資料庫版本 1->2 t_history表格新增了updateTime列
     */
    private val MIGRATION_1_2: Migration = object : Migration(1, 2) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE t_history ADD COLUMN updateTime String")
        }
    }
    /**
     * 資料庫版本 2->3 新增label表
     */
    private val MIGRATION_2_3: Migration = object : Migration(2, 3) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("CREATE TABLE IF NOT EXISTS `t_label` (`id` INTEGER PRIMARY KEY autoincrement, `name` TEXT)")
        }
    }
複製程式碼

Migration接收兩個引數:

  • startVersion 舊版本
  • endVersion 新版本

通知資料庫更新

    mPersonDatabase = Room.databaseBuilder(
        context.applicationContext,
        HistoryDatabase::class.java,
        DATABASE_NAME
    ).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
        .build()
複製程式碼

完整程式碼

@Database(entities = [History::class, Label::class], version = 3)
abstract class HistoryDatabase : RoomDatabase() {

    abstract fun historyDao(): HistoryDao

    companion object {
        private const val DATABASE_NAME = "history.db"
        private lateinit var mPersonDatabase: HistoryDatabase

        fun getInstance(context: Context): HistoryDatabase {
            if (!this::mPersonDatabase.isInitialized) {
                //建立的資料庫的例項
                mPersonDatabase = Room.databaseBuilder(
                    context.applicationContext,
                    HistoryDatabase::class.java,
                    DATABASE_NAME
                ).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                    .build()
            }
            return mPersonDatabase
        }

        /**
         * 資料庫版本 1->2 t_history表格新增了updateTime列
         */
        private val MIGRATION_1_2: Migration = object : Migration(1, 2) {
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("ALTER TABLE t_history ADD COLUMN updateTime String")
            }
        }

        /**
         * 資料庫版本 2->3 新增label表
         */
        private val MIGRATION_2_3: Migration = object : Migration(2, 3) {
            override fun migrate(database: SupportSQLiteDatabase) {
                database.execSQL("CREATE TABLE IF NOT EXISTS `t_label` (`id` INTEGER PRIMARY KEY autoincrement, `name` TEXT)")
            }
        }
    }

}
複製程式碼

注意: @Database註解中版本號的更改,如果是新增表的話,entities 引數裡也要新增上。

建議升級操作順序

修改版本號 -> 新增Migration -> 新增給databaseBuilder

配置編譯器選項

Room 具有以下註解處理器選項:

  • room.schemaLocation:配置並啟用將資料庫架構匯出到給定目錄中的 JSON 檔案的功能。如需瞭解詳情,請參閱 Room 遷移。
  • room.incremental:啟用 Gradle 增量註釋處理器。
  • room.expandProjection:配置 Room 以重寫查詢,使其頂部星形投影在展開後僅包含 DAO 方法返回型別中定義的列。
android {
    ...
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments += [
                    "room.schemaLocation":"$projectDir/schemas".toString(),
                    "room.incremental":"true",
                    "room.expandProjection":"true"]
            }
        }
    }
}
複製程式碼

配置好之後,編譯執行,module資料夾下會生成一個schemas資料夾,其下有一個json檔案,裡面包含資料庫的基本資訊。

{
  "formatVersion": 1,
  "database": {
    "version": 1,
    "identityHash": "xxx",
    "entities": [
      {
        "tableName": "t_history",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT, `insert_time` TEXT, `type` INTEGER NOT NULL)",
        "fields": [
          {
            "fieldPath": "id",
            "columnName": "id",
            "affinity": "INTEGER",
            "notNull": false
          },
          {
            "fieldPath": "name",
            "columnName": "name",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "insertTime",
            "columnName": "insert_time",
            "affinity": "TEXT",
            "notNull": false
          },
          {
            "fieldPath": "type",
            "columnName": "type",
            "affinity": "INTEGER",
            "notNull": true
          }
        ],
        "primaryKey": {
          "columnNames": [
            "id"
          ],
          "autoGenerate": true
        },
        "indices": [],
        "foreignKeys": []
      }
    ],
    "views": [],
    "setupQueries": [
      "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
      "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'xxx')"
    ]
  }
}
複製程式碼

ok,基本使用講解完了,如果對你有用,點個讚唄 ^ _ ^

參考