Android Jetpack - Room

Room 简介

Room 持久化库提供了一个基于 SQLite 的抽象层,以便在利用 SQLite 的全部功能的同时实现更强大的数据库访问

Room 库帮你的 App 在设备上创建一个缓存,并作为此 App 的单一事实来源,允许用户在应用内查看关键信息的一致副本,无论用户是否具有互联网连接

处理重要数量的结构化数据的应用程序可以从本地保存数据中受益匪浅。最常见的用例是缓存相关的数据。这样,当设备无法访问网络时,用户仍然可以在离线时浏览该内容。然后,在设备重新联机后,任何用户启动的内容更改都会同步到服务器

Room 的三个主要部分包括

  • Database

    包含数据库持有者,并作为应用程序的持久关系数据的基础连接的主要访问点。

    使用@Database注释的类应满足以下条件:

    • 继承了 RoomDatabase 的抽象类
    • 在注解中包含与数据库相关联的实体类列表
    • 包含一个无参抽象方法,并返回使用 @Dao 注释的类。 在运行时,你可以通过调用 Room.databaseBuilder()Room.inMemoryDatabaseBuilder()来获取 Database 实例
  • Entity

    数据库中的表

  • DAO

    包含用于访问数据库的方法

该应用程序使用 Room 数据库来获取与该数据库关联的数据访问对象或 DAO。然后,应用程序使用每个 DAO 从数据库中获取实体,并将对这些实体的任何更改保存回数据库。最后,应用程序使用实体来获取和设置与数据库中的表列对应的值

Room 组件关系图

Repository 简介

什么是 Repository ?

Repository 类抽象出对多个数据源的访问。存储库不是体系结构组件库的一部分,但是建议的代码分离和体系结构的最佳实践。 Repository 类为数据访问应用程序的其余部分提供了一个干净的 API

img
为什么使用 Repository ?

Repository 管理查询并允许您使用多个后端。在最常见的示例中,Repository 实现了用于决定是从网络获取数据还是使用在本地数据库中缓存的结果的逻辑,既避免了 ViewModel 和数据的直接交互又统一了单一真实数据源的逻辑

Repository 在 MVVM 架构中的位置

使用 Room 组件

本示例我会参照 CodeLabs 来做一个 WordList,我会精简一下流程,完整代码示例在文末

1、创建 Project

2、添加依赖

app 的 build.gradle 添加依赖

apply plugin: 'kotlin-kapt'
...
dependencies {
    ...
    // Room components
    implementation "androidx.room:room-runtime:$rootProject.roomVersion"
    implementation "androidx.room:room-ktx:$rootProject.roomVersion"
    kapt "androidx.room:room-compiler:$rootProject.roomVersion"
    androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
    // Lifecycle components
    implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
    kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
    androidTestImplementation "androidx.arch.core:core-testing:$rootProject.androidxArchVersion"
    // ViewModel Kotlin support
    implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"
    // Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
    // Material
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
    implementation 'com.google.android.material:material:1.0.0'
}

Project 的 build.gradle 中追加版本号

ext {
    roomVersion = '2.2.0-alpha01'
    archLifecycleVersion = '2.2.0-alpha02'
    androidxArchVersion = '2.0.0'
    coroutines = '1.2.1'
}

Project 的 gradle.properties 中追加如下内容,转换为 AndroidX 项目

android.enableJetifier=true
android.useAndroidX=true
3、创建 Entity、DAO、Database

Entity

在 Room 中,每个被 @Entity 标注的 data class 都被视为 Database 中的一张表

@Entity(tableName = "word_table")
data class Word(@PrimaryKey @ColumnInfo(name = "c_word") val word: String)

为了使 Demo 尽可能的简单,该 Entity 只有一个参数,并被 @PrimaryKey 注释作为主键,此处 @ColumnInfo 的作用是给此参数取一个别名 c_word ,该别名会被真实的记录于数据库的字段中,如果在同一张表中存在多个可能重复的字段时例如 BookA().wordBookB().word ,就可以使用 @ColumnInfo(name = "a_word")@ColumnInfo(name = "b_word") 的方式来区分,如果你想直接使用参数名作为字段名就不用加此注解

@Entity(tableName = "word_table")
data class Word(@PrimaryKey(autoGenerate = true) val id: Int,
                val word: String)

关于 Entity 详细解释见 Entity DOC

DAO

为了避免 UI 阻塞,一些比较耗时的操作如 insert ,可以使用 suspend 关键字修饰,然后利用协程在非 UI 线程执行此方法,需要注意的是,带有 LiveData 返回类型的方法 getAllWords() 并没有添加 suspend 关键字,这是因为 suspend 关键字会在编译时自动给方法添加参数导致 Not sure how to convert a Cursor to this method's return type 异常,解决办法可以参考 issues231

@Dao
interface WordDao {
    @Insert
    suspend fun insert(word: Word)
    @Query("DELETE FROM word_table")
    suspend fun deleteAll()
    @Delete
    suspend fun delete(word: Word)
    @Query("SELECT * from word_table ORDER BY word ASC")
    fun getAllWords() : LiveData<List<Word>>
}

关于 DAO ,Room 为我们提供了四种注解:@Insert @Delete@Update@Query ,其中只有 Query 的参数是 SQL 语句,它支持 SELECT、INSERT、UPDATE 、DELETE 四种语句,所以如果你愿意多写几句 SQL 的话,理论上你的 DAO 中完全可以只有 Query

Database

通过单例模式提供全局唯一的 WordRoomDatabase 实例,通过 addCallback 方法添加可以在数据库启动阶段的回调,在 onOpenonCreate 阶段处理一些初始化操作,比如填充默认数据等

@Database(entities = [Word::class], version = 1,exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
    abstract fun wordDao(): WordDao
    companion object {
        @Volatile
        private var INSTANCE: WordRoomDatabase? = null
        fun getDatabase(context: Context, scope: CoroutineScope): WordRoomDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) {
                return tempInstance
            }
            synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    WordRoomDatabase::class.java,
                    "Word_database"
                ).addCallback(WordDatabaseCallback(scope))
                 .build()
                INSTANCE = instance
                return instance
            }
        }
    }
    private class WordDatabaseCallback(
        private val scope: CoroutineScope
    ) : RoomDatabase.Callback() {
        override fun onOpen(db: SupportSQLiteDatabase) {
            super.onOpen(db)
            INSTANCE?.let { database ->
                scope.launch(Dispatchers.IO) {
                    populateDatabase(database.wordDao())
                }
            }
        }
        suspend fun populateDatabase(wordDao: WordDao) {
            // when you want to execute something on database open
                        // wordDao.deleteAll()
                        
                        // var word = Word("Hello")
                        // wordDao.insert(word)
                        // word = Word("World!")
                        // wordDao.insert(word)
        }
    }
}
4、创建 Repository

Repository 作为 ViewModel 与数据操作的中间层,避免了 ViewModel 与数据的直接交互,即方便了 ViewModel 的测试,又能在 Repository 中实现单一真实数据源策略,从而使 ViewModel 更加关注于业务层逻辑

class WordRepository (private val wordDao: WordDao){
    val allWords: LiveData<List<Word>> = wordDao.getAllWords()
    @WorkerThread
    suspend fun insert(word: Word){
        wordDao.insert(word)
    }
    @WorkerThread
    suspend fun delete(word: Word){
        wordDao.delete(word)
    }
}
5、创建 ViewModel

由于初始化 Database 需要用到 Context ,所以此 ViewModel 继承了包含 Application 的 AndroidViewModel。因为要从主线程调用 insert、delete 等方法,所以此处启动了协程来执行这些方法进行数据库操作,并使用 IO Dispatchers

class WordViewModel (application: Application) : AndroidViewModel(Application()){
    private val repository: WordRepository
    val allWords: LiveData<List<Word>>
    init {
        val wordsDao = WordRoomDatabase.getDatabase(application,viewModelScope).wordDao()
        repository = WordRepository(wordsDao)
        allWords = repository.allWords
    }
    fun insert(word: Word) = viewModelScope.launch (Dispatchers.IO){
        repository.insert(word)
    }
    fun delete(word: Word) = viewModelScope.launch (Dispatchers.IO){
        repository.delete(word)
    }
}
6、在 MainActivity 中注册 Observer
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
  
    ...
  
    viewModel.allWords.observe(this, Observer {
        adapter.setWords(it)
    })
}

allWords 是一个 LiveData 类型,经过注册之后,无论是修改或者删除,都会立刻被 Observer 感知并通知 UI 更新列表

完整示例代码

https://github.com/realskyrin/jetpack_room

参考

https://developer.android.com/topic/libraries/architecture/room

https://github.com/googlesamples/android-sunflower

https://medium.com/androiddevelopers/introducing-android-sunflower-e421b43fe0c2

https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin

https://developer.android.com/reference/android/arch/persistence/room/OnConflictStrategy

Jetpack 系列文章

Android Jetpack - Lifecycles

Android Jetpack - ViewModel

Android Jetpack - DataBinding

Android Jetpack - Room

Android Jetpack - LiveData

Android Jetpack - WorkManager(待更)

Android Jetpack - Paging(待更)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342