本節(jié)我們來利用Room實現(xiàn)歌單標簽的增刪改查,即安裝APP的時候默認添加一些歌單標簽存入“我的歌單標簽”數(shù)據(jù)庫中,在歌單標簽編輯頁面可以對“我的歌單標簽”進行增刪改查,并且能對“我的歌單標簽”的歌單進行序列重排。
大體效果如下圖所示:

本節(jié)可能您將學習到如下知識點:
- Room的使用方法,包括Entity,Dao,Database的使用等
- 創(chuàng)建數(shù)據(jù)庫時進行一些初始化操作
- 網(wǎng)絡數(shù)據(jù)和數(shù)據(jù)庫數(shù)據(jù)同時獲取,然后組裝數(shù)據(jù)
- 數(shù)據(jù)庫的增刪改查事務的實現(xiàn)
添加Room依賴
在app的build.gradle中加入如下依賴
def room_version = "2.2.5"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
第一個和第二個是必須的,第三個是Room的Kotlin擴展庫,如果Room和協(xié)程一起使用的時候需要添加這個依賴。
Room詳細介紹
使用Room需要定義先定義Entity,Dao,Database,按照JetPack的分層,ViewModel對Database的操作還有一個Repository層。
接下來我們一個個介紹。
Entity (實體)
Entity可以簡單理解為映射的一張表,它的定義如下:
@Entity(tableName = "play_list_tags")
data class PlayListTag(
@PrimaryKey(autoGenerate = false)
val id: Int,
val name: String,
val type: Int,
val category: Int,
val hot: Boolean,
var draggable: Boolean,
var disabled: Boolean,
@ColumnInfo (name = "tag_sequence")
var sequence: Int // 在數(shù)組中的序列
)
-
Entity需要用
@Entity注解,注解中有一個元素tableName,表示對應的數(shù)據(jù)庫表名。 -
Entity必須要要有一個主鍵,可以用
@PrimaryKey注解標記主鍵,autoGenerate表示這個主鍵是否是自增長 - 用
@ColumnInfo可以給表的列定義一個列名,可以和Entity的屬性名不一致
Dao (數(shù)據(jù)訪問對象)
Dao需要定義為Interface,且標記為@Dao注解.
為了比較完整的了解Dao, 我們的PlayListTagDao定義如下:
@Dao
interface PlayListTagDao {
@Query("SELECT id, name, type, category, hot, draggable, disabled, tag_sequence FROM play_list_tags ORDER BY tag_sequence ASC;")
fun getAllTags(): LiveData<List<PlayListTagResponse.PlayListTag>>
@Query("SELECT id, name, type, category, hot, draggable, disabled, tag_sequence FROM play_list_tags ORDER BY tag_sequence ASC;")
suspend fun getTags(): List<PlayListTagResponse.PlayListTag>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertAllTags(tags: List<PlayListTagResponse.PlayListTag>)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertTag(tag: PlayListTagResponse.PlayListTag)
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun updatePlayListTag(tag: PlayListTagResponse.PlayListTag)
@Delete
suspend fun deletePlayListTag(tag: PlayListTagResponse.PlayListTag)
@Transaction
suspend fun updatePlayListTags(tags: List<PlayListTagResponse.PlayListTag>) {
for (tag in tags) {
updatePlayListTag(tag)
}
}
}
我們來分析下上面這些增刪改查和事務的幾個方法:
-
getAllTags有一個注解@Query,表示是一個查詢方法,"SELECT id, name, type, category, hot, draggable, disabled, tag_sequence FROM play_list_tags ORDER BY tag_sequence ASC;"表示查詢所有歌單標簽的方法;這個方法的返回值是LiveData, 即這個查詢方法會異步執(zhí)行,且只要數(shù)據(jù)庫數(shù)據(jù)有變化這個LiveData都會通知觀察者數(shù)據(jù)的變化。 -
getTags和getAllTags一樣是查詢所有歌單標簽的方法,但是返回值是List, 這個函數(shù)是suspend函數(shù),也是異步執(zhí)行,兩個方法的差別是數(shù)據(jù)庫數(shù)據(jù)變化后getTags無法及時監(jiān)聽到變化。需要主動調(diào)用才能獲取新的數(shù)據(jù)。 -
insertAllTags和insertTag都有@Insert注解,表示是插入數(shù)據(jù)的方法,onConflict元素表示的是如果遇到了沖突的處理策略,我們這里使用的是OnConflictStrategy.IGNORE即忽略沖突。 -
updatePlayListTag有@Update注解,表示是一個更新數(shù)據(jù)的方法,更新我們使用的策略是nConflictStrategy.REPLACE即取代舊數(shù)據(jù)同時繼續(xù)事務。 -
deletePlayListTag有@Delete注解,表示是一個刪除數(shù)據(jù)的方法。 -
updatePlayListTags有@Transaction注解,表示這個方法是一個事務,這個事務的功能是一次更新多個PlayListTag。
Database (數(shù)據(jù)庫)
- Database的定義
使用@Database注解可以定義數(shù)據(jù)庫持有者。具體的定義如下:
@Database(version = 1, entities = [PlayListTagResponse.PlayListTag::class], exportSchema = false)
abstract class MusicDatabase : RoomDatabase() {
abstract fun playListDao(): PlayListTagDao
}
-
@Database注解中的version表示數(shù)據(jù)庫版本號,entities中表示的是實體列表,我們目前只有一個實體類PlayListTagResponse.PlayListTag,exportSchema設置為false。 - Database為抽象類,且繼承自RoomDatabase。
-
playListDao為抽象方法,返回PlayListTagDao,即我們上面定義的Dao.
- Database的初始化
Database的初始化需要先使用Room.databaseBuilder方法,這個方法幾個參數(shù):
Room.databaseBuilder(application, MusicDatabase::class.java, "music_database")
.build()
- 參數(shù)一:context,我們可以使用Application
- 參數(shù)二:Class,即我們定義的Database的class
- 參數(shù)三:name, 這個參數(shù)表示的就是數(shù)據(jù)庫的名字
Room.databaseBuilder方法生成的RoomDatabase.Builder對象調(diào)用build方法就生成了MusicDatabase對象
一般為了方便使用,我們可以在MusicDatabase類里面定義一個靜態(tài)方法初始化一個單例對象。
修改后的代碼如下:
@Database(version = 1, entities = [PlayListTagResponse.PlayListTag::class], exportSchema = false)
abstract class MusicDatabase : RoomDatabase() {
abstract fun playListDao(): PlayListTagDao
companion object {
@Volatile
private var INSTANCE: MusicDatabase? = null
fun getInstance(application: Application): MusicDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(application, MusicDatabase::class.java, "music_database")
.build()
INSTANCE = instance
return instance
}
}
}
}
上面代碼的companion object里面的getInstance方法就返回了MusicDatabase的單例對象INSTANCE。
Repository (倉庫)
倉庫相對簡單,就是調(diào)用Dao的方法,比較簡單。
所有的數(shù)據(jù)庫操作應該是異步的,所以和Dao對應,PlayListTagRepository中所有的方法也是suspend函數(shù)。
class PlayListTagRepository(private val dao: PlayListTagDao) {
suspend fun getTags(): List<PlayListTagResponse.PlayListTag> {
return dao.getTags()
}
fun getAllTags(): LiveData<List<PlayListTagResponse.PlayListTag>> {
return dao.getAllTags()
}
suspend fun insertAllTags(tags: List<PlayListTagResponse.PlayListTag>) {
return dao.insertAllTags(tags)
}
suspend fun insertTag(tag: PlayListTagResponse.PlayListTag) {
return dao.insertTag(tag)
}
suspend fun deletePlayListTag(tag: PlayListTagResponse.PlayListTag) {
return dao.deletePlayListTag(tag)
}
suspend fun updatePlayListTag(tag: PlayListTagResponse.PlayListTag) {
return dao.updatePlayListTag(tag)
}
suspend fun updatePlayListTags(tags: List<PlayListTagResponse.PlayListTag>) {
dao.updatePlayListTags(tags)
}
}
DataBase創(chuàng)建時插入數(shù)據(jù)
RoomDatabase.Callback回調(diào)函數(shù)可以在數(shù)據(jù)庫創(chuàng)建時,或者數(shù)據(jù)庫打開的時候進行一些操作,我們需要在數(shù)據(jù)庫創(chuàng)建時候添加一些默然的5個歌單標簽,包括推薦,官方,精品,華語和流行。
private class MusicDatabaseCallBack: RoomDatabase.Callback() {
// 1
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
INSTANCE?.let {
val list = listOf(
PlayListTagResponse.PlayListTag(-10, "推薦", 0, 0, hot = false, draggable = false, disabled = false, sequence = 0),
PlayListTagResponse.PlayListTag(-20, "官方", 0, 0, hot = false, draggable = false, disabled = false, sequence = 1),
PlayListTagResponse.PlayListTag(-30, "精品", 0, 0, hot = false, draggable = false, disabled = false, sequence = 2),
PlayListTagResponse.PlayListTag(5001, "華語", 0, 0, hot = false, draggable = true, disabled = false, sequence = 3),
PlayListTagResponse.PlayListTag(1, "流行", 0, 1, hot = false, draggable = true, disabled = false, sequence = 4)
)
// 2
GlobalScope.launch {
it.playListDao().insertAllTags(list)
}
}
}
}
-
onCreate在數(shù)據(jù)庫創(chuàng)建時會調(diào)用,這里我們插入默認的數(shù)據(jù) -
GlobalScope.launch實現(xiàn)異步插入。
接下來修改Database的初始化方法,添加addCallback傳入MusicDatabaseCallBack。
val instance = Room.databaseBuilder(application, MusicDatabase::class.java, "music_database")
// 回調(diào)函數(shù)
.addCallback(MusicDatabaseCallBack())
.build()
Room實現(xiàn)歌單廣場界面標簽的Tab
歌單廣場界面的標簽根據(jù)數(shù)據(jù)庫存儲的標簽列表展示,由于默認增加了5個標簽,所以如果沒有編輯的話,默認展示的就是這5個從數(shù)據(jù)庫獲取的標簽,即:推薦,官方,精品,華語和流行。
- 添加一個PlayListSquareViewModel類,獲取所有的歌單標簽。
class PlayListSquareViewModel(application: Application): AndroidViewModel(application) {
private val playListTagRepository: PlayListTagRepository
init {
val tagDao = MusicDatabase.getInstance(application).playListDao()
playListTagRepository = PlayListTagRepository(tagDao)
}
/* 返回所有的Tag */
fun getAllTags(): LiveData<List<PlayListTagResponse.PlayListTag>> {
return playListTagRepository.getAllTags()
}
}
- PlayListSquareViewModel繼承自AndroidViewModel,持有一個PlayListTagRepository對象
AndroidViewModel繼承自ViewModel并且持有一個Application對象,這個Application對象剛好創(chuàng)建是Database對象需要的context。
-
getAllTags返回所有的歌單標簽,并且當數(shù)據(jù)庫中的歌單列表變化后能實時發(fā)送給監(jiān)聽者新的數(shù)據(jù)。
- PlayListSquareFragment獲取數(shù)據(jù)
<!-- PlayListSquareFragment.kt -->
private val viewModel by viewModels<PlayListSquareViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 獲取我的Tags
viewModel.getAllTags().observe(viewLifecycleOwner, Observer { tags ->
var playListNamesArray = tags.map { it.name }
// ...
})
}
經(jīng)過修改后,得到的效果如下圖所示:

Room實現(xiàn)歌單標簽編輯界面的增刪改查
歌單標簽編輯界面有些特殊,我的標簽列表是從數(shù)據(jù)庫中獲取的,而所有的標簽列表是從網(wǎng)絡獲取的。所以界面的展示需要同時從兩個Repository倉庫獲取數(shù)據(jù).

- ViewModel實現(xiàn)網(wǎng)絡和數(shù)據(jù)庫同時獲取數(shù)據(jù)
class PlaylistTagViewModel(application: Application) : AndroidViewModel(application) {
// 1
public val playListTagRepository: PlayListTagRepository
// 2
private var _tagList: MutableLiveData<Pair<List<PlayListTagResponse.PlayListTag>, List<PlaylistTagSection>>> = MutableLiveData()
val tagList: LiveData<Pair<List<PlayListTagResponse.PlayListTag>, List<PlaylistTagSection>>> = _tagList
init {
val tagDao = MusicDatabase.getInstance(application).playListDao()
playListTagRepository = PlayListTagRepository(tagDao)
queryTagsWithDatabaseAndNetwork()
}
private fun queryTagsWithDatabaseAndNetwork() {
viewModelScope.launch(Dispatchers.IO) {
try {
// 3
val networkRequest = async { PlaylistRepository.getPlaylistTags() }
val databaseRequest = async { playListTagRepository.getTags() }
val networkResponse = networkRequest.await()
val databaseResponse = databaseRequest.await()
// 4
val section = mutableListOf(
PlaylistTagSection(0, "語種", mutableListOf()),
PlaylistTagSection(1, "風格", mutableListOf()),
PlaylistTagSection(2, "場景", mutableListOf()),
PlaylistTagSection(3, "情感", mutableListOf()),
PlaylistTagSection(4, "主題", mutableListOf())
)
for (tag in networkResponse.tags) {
when (tag.category) {
0 -> {
section[0].list.add(tag)
}
1 -> {
section[1].list.add(tag)
}
2 -> {
section[2].list.add(tag)
}
3 -> {
section[3].list.add(tag)
}
4 -> {
section[4].list.add(tag)
}
}
}
// 5
withContext(Dispatchers.Main) {
_tagList.value = Pair(databaseResponse, section)
}
} catch (e: Exception) {
}
}
}
}
代碼解釋如下:
-
playListTagRepository利用PlayListTagDao來操作數(shù)據(jù)庫 -
tagList是數(shù)據(jù)庫和網(wǎng)絡數(shù)據(jù)的組合數(shù)據(jù),被提供給Fragment進行界面的展示 - 用
async和await的組合,將兩個來源的數(shù)據(jù)獲取都異步進行,且互不阻塞 - 當兩個來源的數(shù)據(jù)都獲得后,進行數(shù)據(jù)的裝配
- 將數(shù)據(jù)分發(fā)到主線程,刷新界面
- 增加標簽
private fun addMyTag(tag: PlayListTagResponse.PlayListTag) {
//...
tag.sequence = mTags.size - 1
GlobalScope.launch {
viewModel.playListTagRepository.insertTag(tag)
}
}
- 刪除標簽
private fun removeMyTag(position: Int) {
//...
val willDeleteTag = mTags[position]
GlobalScope.launch {
viewModel.playListTagRepository.deletePlayListTag(willDeleteTag)
}
}
- 兩個標簽交換位置 - 用事務修改標簽
fun onItemMove(fromPosition: Int, toPosition: Int) {
Collections.swap(mTags, fromPosition - 1, toPosition - 1)
// 數(shù)據(jù)庫中進行替換
mTags[toPosition-1].sequence = toPosition - 1
mTags[fromPosition-1].sequence = fromPosition - 1
// 用APP的生命周期
GlobalScope.launch {
withContext(Dispatchers.IO) {
viewModel.playListTagRepository.updatePlayListTags(
listOf(
mTags[fromPosition - 1],
mTags[toPosition - 1]
)
)
}
}
}
至此功能就完成了。文中列出了Room相關的關鍵代碼,如果對其他的代碼有疑問,可以參閱前面的文章。