The use of Jetpack's Room, combined with Flow

This article mainly refers to official documents, and then takes the example of saving search history.

Ready to work

RoomIn the SQLitelast it provides an abstraction layer, so that at the same time take advantage of the power of SQLite, and can smoothly access the database.

rely

For use in the application Room, add the following dependency to the application build.gradlefile.

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"
}

Main components

  • 数据库: Contains the database holder and serves as the main access point for the underlying connection of the persistent relational data that the application has retained.
    Use @Databasethe comment class should meet the following criteria:
    • It is an extension RoomDatabaseof an abstract class.
    • Add a list of entities associated with the database in the comment.
    • Contains an @Daoabstract method that has 0 parameters and returns an annotated class.
      At runtime, you can call Room.databaseBuilder()or Room.inMemoryDatabaseBuilder()get Databaseinstance.
  • Entity: Represents a table in the database.
  • DAO: Contains methods for accessing the database.

The application uses the Room database to obtain the Data Access Object (DAO) associated with the database. Then, the application uses each DAO to retrieve entities from the database, and then saves all changes to those entities back into the database. Finally, the application uses entities to get and set the values ​​corresponding to the table columns in the database.

The relationship is shown in the figure:
Insert picture description here

Ok, after understanding the basic concepts, let's take a look at how to do it.

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 object corresponds to a table, use @Entityannotations, and declare your table name
  • @PrimaryKeyPrimary key, autoGenerate = trueincrement
  • @ColumnInfoColumn, and declare the column name, typeAffinityfield type
  • @Ignore Declare ignored objects

A very simple table, mainly nameand insertTimefields.

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: Increase
  • @Delete: 删
  • @Update:改
  • @Query: Check

One thing to note here is 查询所有搜索历史that I Flowmodified the returned collection .

As long as any of the data in the database is updated, no matter which line to change the data, then re-execute the queryoperation and distribute again Flow.

In the same way, if an irrelevant data is updated, it Flowwill also be distributed and will receive the same data as before.

This is because the content update notification function of the SQLite database is based on table (Table) data as a unit, rather than row (Row) data as a unit, so as long as the data in the table is updated, it will trigger the content update notification. Room doesn't know which data is updated in the table, so it will trigger the query operation defined in DAO again. You can use Flow's operators, such as distinctUntilChanged to ensure that you will only be notified when the data you care about is updated.

    //按类型 查询所有搜索历史
    @Query("SELECT * FROM t_history WHERE type=:type")
    fun getAll(type: Int = 1): Flow<List<History>>

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

database

@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
        }
    }

}
  • Use @Databaseannotation statement
  • entities Array, corresponding to all tables in this database
  • version Database version number

note:

If your application runs in a single process, you should follow it when instantiating the AppDatabase object 单例设计模式.
The cost of each RoomDatabase instance is quite high, and you hardly need to access multiple instances in a single process.

use

Get the database where you need it

mHistoryDao = HistoryDatabase.getInstance(this).historyDao()

Get search history

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

collectThis is Flowthe way to get data, not the only way to view the document .

Why put 协程it inside, because the database operation is time-consuming, and the coroutine can easily specify the thread, so that it does not block the UIthread.

Looking at the Flow source code, I found that Flow is under the coroutine package

package kotlinx.coroutines.flow

Take collect as an example. It is also suspendmodified. Since it supports it 挂起, 协程wouldn't it be wonderful to cooperate .

    @InternalCoroutinesApi
    public suspend fun collect(collector: FlowCollector<T>)

Save search history

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

Clear local history

    private fun cleanHistory() {
    
    
        MainScope().launch(Dispatchers.IO) {
    
    
            mHistoryDao.deleteAll()
        }
    }

Author: https://blog.csdn.net/yechaoa

Database upgrade

Database upgrade is an important operation. After all, it may cause data loss, which is also a serious problem.

Room Migrationperforms upgrade operations through classes. We only need to tell the Migrationclass what has changed, such as 新增fields or tables.

Define the migration class

    /**
     * 数据库版本 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)")
        }
    }

MigrationTwo parameters are received:

  • startVersion old version
  • endVersion new version

Notify database updates

    mPersonDatabase = Room.databaseBuilder(
        context.applicationContext,
        HistoryDatabase::class.java,
        DATABASE_NAME
    ).addMigrations(MIGRATION_1_2, MIGRATION_2_3)
        .build()

Complete code

@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)")
            }
        }
    }

}

Note: The
@Database revision of the version number in the comment, if it is a new table entities, should also be added to the parameter.

Recommended upgrade sequence

Modify the version number -> Add Migration -> Add to databaseBuilder

Configure compiler options

Room has the following annotation processor options:

  • room.schemaLocation: Configure and enable the ability to export the database schema to a JSON file in a given directory. For details, see Room migration.
  • room.incremental: Enable Gradle incremental annotation processor.
  • room.expandProjection: Configure Room to rewrite the query so that the top star projection only contains the columns defined in the DAO method return type after expansion.
android {
    
    
    ...
    defaultConfig {
    
    
        ...
        javaCompileOptions {
    
    
            annotationProcessorOptions {
    
    
                arguments += [
                    "room.schemaLocation":"$projectDir/schemas".toString(),
                    "room.incremental":"true",
                    "room.expandProjection":"true"]
            }
        }
    }
}

After configuration, compile and run, a folder will be generated under the module schemasfolder, and there is a jsonfile under it , which contains the basic information of the database.

{
    
    
  "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, the basic usage explanation is over, if it is useful to you, please give me a thumbs up ^ _ ^

reference

Guess you like

Origin blog.csdn.net/yechaoa/article/details/112712384