目錄
- 使用Kotlin構(gòu)建MVVM應(yīng)用程序—總覽篇
- 使用Kotlin構(gòu)建MVVM應(yīng)用程序—第一部分:入門篇
- 使用Kotlin構(gòu)建MVVM應(yīng)用程序—第二部分:Retrofit及RxJava
- 使用Kotlin構(gòu)建MVVM應(yīng)用程序—第三部分:Room
寫在前面
這是使用Kotlin構(gòu)建MVVM應(yīng)用程序—第三部分:Room
在上一篇中我們了解了MVVM是怎么處理網(wǎng)絡(luò)數(shù)據(jù)的麻削,而這一篇?jiǎng)t介紹的是如何進(jìn)行數(shù)據(jù)持久化掏熬。
Room
Room是google推出的一個(gè)數(shù)據(jù)持久化庫王悍,它是 Architecture Component的一部分。它讓SQLiteDatabase的使用變得簡(jiǎn)單,大大減少了重復(fù)的代碼,并且把SQL查詢的檢查放在了編譯時(shí)。
Room使用起來非常簡(jiǎn)單疆液,而且可以和RxJava配合使用,和我們的技術(shù)體系十分契合棚放。
加入依賴
首先在項(xiàng)目的build.gradle中加入
allprojects {
repositories {
maven {
url 'https://maven.google.com'
}
jcenter()
}
}
接著在app的build.gradle中加入它的依賴
//room (local)
implementation 'android.arch.persistence.room:runtime:1.0.0'
implementation 'android.arch.persistence.room:rxjava2:1.0.0'
kapt 'android.arch.persistence.room:compiler:1.0.0'
//facebook出品枚粘,可在Chrome中查看數(shù)據(jù)庫
implementation 'com.facebook.stetho:stetho:1.5.0'
現(xiàn)在的結(jié)構(gòu)
這里我們多了一層Repository,使用這一層來確保單一數(shù)據(jù)源飘蚯,保證數(shù)據(jù)來源的唯一和正確性(即不管是來自網(wǎng)絡(luò)或是本地緩存的)馍迄。ViewModel層并不需要知道它使用到的數(shù)據(jù)是怎么來的,就好似開發(fā)者并不需要知道設(shè)計(jì)師是如何畫出UI圖的一樣局骤。
開始正文
使用Room進(jìn)行持久化
-
新建相應(yīng)的表
Room為每個(gè)用@Entity注解了的類創(chuàng)建一張表
@Entity(tableName = "articles")
class Article(var title: String?){
@PrimaryKey
@ColumnInfo(name = "articleid")
var id: Int = 0
var content: String? = null
var readme: String? = null
@SerializedName("describe")
var description: String? = null
var click: Int = 0
var channel: Int = 0
var comments: Int = 0
var stow: Int = 0
var upvote: Int = 0
var downvote: Int = 0
var url: String? = null
var pubDate: String? = null
var thumbnail: String? = null
}
-
創(chuàng)建相關(guān)的Dao
相當(dāng)于Retrofit中的api接口
DAO負(fù)責(zé)定義操作數(shù)據(jù)庫的方法攀圈。在SQLite實(shí)現(xiàn)的版本中,所有的查詢都是在LocalUserDataSource文件中完成的峦甩,里面主要是 使用了Cursor對(duì)象來完成查詢的工作赘来。有了Room,我們不再需要Cursor的相關(guān)代碼凯傲,而只需在Dao類中使用注解來定義查詢犬辰。
@Dao
interface PaoDao{
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insetAll(articles: List<Article>)
@Query("SELECT * FROM Articles WHERE articleid= :id")
fun getArticleById(id:Int):Single<Article>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertArticle(article :Article)
}
-
創(chuàng)建數(shù)據(jù)庫
相當(dāng)于創(chuàng)建RetrofitClient對(duì)象
我們需要定義一個(gè)繼承了RoomDatabase的抽象類。這個(gè)類使用@Database來注解冰单,列出它所包含的Entity以及操作它們的 DAO 幌缝。
@Database(entities = arrayOf(Article::class),version = 1) abstract class AppDatabase :RoomDatabase(){ abstract fun paoDao(): PaoDao companion object { @Volatile private var INSTANCE: AppDatabase? = null fun getInstance(context: Context): AppDatabase = INSTANCE ?: synchronized(this) { INSTANCE ?: buildDatabase(context).also { INSTANCE = it } } private fun buildDatabase(context: Context) = Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "app.db") .build() } }
Over ,集成Room十分的簡(jiǎn)單诫欠。
更多關(guān)于Room的使用方法涵卵,它的遷移,表之間的關(guān)聯(lián)和字段荒叼。
推薦查看泡網(wǎng)的Room專題:Room
實(shí)踐
這里我們對(duì)上一篇中的從服務(wù)器端獲取到的Article文章進(jìn)行持久化轿偎。
- 修改一下Model層的代碼,添加Repository作為ViewModel層的數(shù)據(jù)源
class PaoRepo constructor(private val remote:PaoService,private val local :PaoDao){
//首先查看本地?cái)?shù)據(jù)庫是否存在該篇文章
fun getArticleDetail(id:Int)= local.getArticleById(id)
.onErrorResumeNext {
//本地?cái)?shù)據(jù)庫不存在被廓,會(huì)拋出EmptyResultSetException
//轉(zhuǎn)而獲取網(wǎng)絡(luò)數(shù)據(jù),成功后保存到數(shù)據(jù)庫
remote.getArticleDetail(id)
.doOnSuccess { local.insertArticle(it) }
}
}
我們的目錄結(jié)構(gòu)會(huì)如下圖所示:
- 修改我們的ViewModel層的數(shù)據(jù)源
在上一篇中我們使用的是PaoService
網(wǎng)絡(luò)數(shù)據(jù)作為數(shù)據(jù)源坏晦,這里只需要修改為PaoRepo
class PaoViewModel(private val repo: PaoRepo)
以后統(tǒng)一使用PaoRepo來為PaoViewModel提供數(shù)據(jù)
- 在View層中將
PaoRepo
注入到PaoViewModel
//////model
val remote=Retrofit.Builder()
.baseUrl(Constants.HOST_API)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build().create(PaoService::class.java)
val local=AppDatabase.getInstance(applicationContext).paoDao()
val repo = PaoRepo(remote, local)
/////ViewModel
mViewMode= PaoViewModel(repo)
////binding
mBinding.vm=mViewMode
看看效果
Roomigrant
Roomigrant is a helper library to automatically generate Android Room library migrations using compile-time code generation.
這是一個(gè)幫助開發(fā)者進(jìn)行Room數(shù)據(jù)遷移的庫,使用起來非常方便。
在使用Room的過程中昆婿,或多或少都會(huì)遇到增加表间护、改變表字段的情況。但是Room的遷移比較麻煩挖诸,可以看看理解Room的數(shù)據(jù)遷移這篇文章,增加新表還好法精,只需要修改版本號(hào)多律,但是如果需要修改表字段的話,由于sqlite的歷史原因:SQLite的ALTER TABLE命令非常局限搂蜓,只支持重命名表以及添加新的字段狼荞。
因此我們需要:
- 創(chuàng)建一個(gè)新的臨時(shí)表,
- 把users表中的數(shù)據(jù)拷貝到臨時(shí)表中
- 丟棄users表
- 把臨時(shí)表重命名為users
使用Room帮碰,Migration的實(shí)現(xiàn)是這樣的:
static final Migration MIGRATION_3_4 = new Migration(3, 4) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// Create the new table
database.execSQL(
"CREATE TABLE users_new (userid TEXT, username TEXT, last_update INTEGER, PRIMARY KEY(userid))");
// Copy the data
database.execSQL(
"INSERT INTO users_new (userid, username, last_update) SELECT userid, username, last_update FROM users");
// Remove the old table
database.execSQL("DROP TABLE users");
// Change the table name to the correct one
database.execSQL("ALTER TABLE users_new RENAME TO users");
}
};
對(duì)于開發(fā)者還需要編寫大量的sql語句相味,可以說是不友好了。
但是幸運(yùn)的是Roomigrant幫助我們做到了以上的事殉挽,對(duì)于修改表字段丰涉,只需要簡(jiǎn)單的
自定義一個(gè)Rule,通知一下什么表下的哪個(gè)字段需要修改
// version 3 的users表的字段uId
// version 4 相應(yīng)的字段改為了userId
@FieldMigrationRule(version1 = 3, version2 = 4, table = "users", field = "uId")
fun migrate_3_4_Object1Dbo_intVal(): String {
return "`users`.`userId`"
}
這樣Roomigrant便可以幫助我們生成那些模板式的代碼斯碌。
寫在最后
本項(xiàng)目的github地址:https://github.com/ditclear/MVVM-Android
更多的例子可以查看:https://github.com/ditclear/PaoNet
這是使用Kotlin構(gòu)建MVVM項(xiàng)目的第三部分一死,主要講了怎么在MVVM中進(jìn)行數(shù)據(jù)的持久化以及為ViewModel層提供Repository作為唯一的數(shù)據(jù)源。
總結(jié)一下前三篇的內(nèi)容便是:
使用Retrofit提供來自服務(wù)端的數(shù)據(jù)傻唾,使用Room來進(jìn)行持久化投慈,然后提供一個(gè)Repository來為ViewModel提供數(shù)據(jù),ViewModel層利用RxJava來進(jìn)行數(shù)據(jù)的轉(zhuǎn)換冠骄,配合DataBinding引起View層的變化伪煤。
邏輯很清晰了,但唯一的遺憾便是為了提供一個(gè)ViewModel我們需要寫太多模板化的代碼了
//////model
val remote=Retrofit.Builder()
.baseUrl(Constants.HOST_API)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.build().create(PaoService::class.java)
val local=AppDatabase.getInstance(applicationContext).paoDao()
val repo = PaoRepo(remote, local)
/////ViewModel
mViewMode= PaoViewModel(repo)
如果能不寫該多好凛辣。
上帝說:可以抱既。
所以下一篇的內(nèi)容便是依賴注入—Dagger2,從入門到放棄到恍然大悟到愛不釋手蟀给。蝙砌。。