JetPack知識點實戰(zhàn)系列八:Room數(shù)據(jù)庫實現(xiàn)增刪改查和事務處理

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

大體效果如下圖所示:

實現(xiàn)功能

本節(jié)可能您將學習到如下知識點:

  1. Room的使用方法,包括Entity,Dao,Database的使用等
  2. 創(chuàng)建數(shù)據(jù)庫時進行一些初始化操作
  3. 網(wǎng)絡數(shù)據(jù)和數(shù)據(jù)庫數(shù)據(jù)同時獲取,然后組裝數(shù)據(jù)
  4. 數(shù)據(jù)庫的增刪改查事務的實現(xiàn)

添加Room依賴

appbuild.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,DaoDatabase,按照JetPack的分層,ViewModelDatabase的操作還有一個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ù)組中的序列
    )
  1. Entity需要用@Entity注解,注解中有一個元素tableName,表示對應的數(shù)據(jù)庫表名。
  2. Entity必須要要有一個主鍵,可以用@PrimaryKey注解標記主鍵,autoGenerate表示這個主鍵是否是自增長
  3. @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)
        }
    }

}

我們來分析下上面這些增刪改查和事務的幾個方法:

  1. 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ù)的變化。
  2. getTagsgetAllTags一樣是查詢所有歌單標簽的方法,但是返回值是List, 這個函數(shù)是suspend函數(shù),也是異步執(zhí)行,兩個方法的差別是數(shù)據(jù)庫數(shù)據(jù)變化后getTags無法及時監(jiān)聽到變化。需要主動調(diào)用才能獲取新的數(shù)據(jù)。
  3. insertAllTagsinsertTag都有@Insert注解,表示是插入數(shù)據(jù)的方法,onConflict元素表示的是如果遇到了沖突的處理策略,我們這里使用的是OnConflictStrategy.IGNORE即忽略沖突。
  4. updatePlayListTag@Update注解,表示是一個更新數(shù)據(jù)的方法,更新我們使用的策略是nConflictStrategy.REPLACE即取代舊數(shù)據(jù)同時繼續(xù)事務。
  5. deletePlayListTag@Delete注解,表示是一個刪除數(shù)據(jù)的方法。
  6. 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

}
  1. @Database注解中的version表示數(shù)據(jù)庫版本號,entities中表示的是實體列表,我們目前只有一個實體類PlayListTagResponse.PlayListTag,exportSchema設置為false。
  2. Database為抽象類,且繼承自RoomDatabase
  3. playListDao為抽象方法,返回PlayListTagDao,即我們上面定義的Dao.
  • Database的初始化

Database的初始化需要先使用Room.databaseBuilder方法,這個方法幾個參數(shù):

Room.databaseBuilder(application, MusicDatabase::class.java, "music_database")
                    .build()
  1. 參數(shù)一:context,我們可以使用Application
  2. 參數(shù)二:Class,即我們定義的Database的class
  3. 參數(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)
            }
        }
    }
}
  1. onCreate在數(shù)據(jù)庫創(chuàng)建時會調(diào)用,這里我們插入默認的數(shù)據(jù)
  2. 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()
    }

}
  1. PlayListSquareViewModel繼承自AndroidViewModel,持有一個PlayListTagRepository對象

AndroidViewModel繼承自ViewModel并且持有一個Application對象,這個Application對象剛好創(chuàng)建是Database對象需要的context。

  1. 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) {

            }

        }
    }
}

代碼解釋如下:

  1. playListTagRepository利用PlayListTagDao來操作數(shù)據(jù)庫
  2. tagList是數(shù)據(jù)庫和網(wǎng)絡數(shù)據(jù)的組合數(shù)據(jù),被提供給Fragment進行界面的展示
  3. asyncawait的組合,將兩個來源的數(shù)據(jù)獲取都異步進行,且互不阻塞
  4. 當兩個來源的數(shù)據(jù)都獲得后,進行數(shù)據(jù)的裝配
  5. 將數(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相關的關鍵代碼,如果對其他的代碼有疑問,可以參閱前面的文章。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容