Jetpack 之 Room 使用初體驗
寫在前面
本文開發環境爲AndroidStudio 4.0 ,開發語言爲kotlin,調試設備爲OPPO Reno。
資料來自於一下鏈接。
Jetpack簡介
其實早在2018年,Google就在I/O大會上發佈了一系列的輔助工具包合集,稱爲Jetpack,主要包含四個方面。
- Architecture
- Foundation
- Behavior
- UI
根據圖片可以看到幾乎是涵蓋Android開發的方方面面。我們只要學會使用其中的幾個組件就能開發出架構很清晰的應用。在Android的官方demo中也是大量採用相關組件去做開發。關於這裏面各模塊的簡單介紹可以參考這個點擊前往。很重要的一點是Jetpack和Kotlin相結合真的是非常令人舒適。
Room初體驗
前面簡單介紹一下Jetpack,但本次重點還是簡單說一下Room的使用體驗,其實這玩意兒,Google 2017年就推出了。但是好像很少人用,反正我是2019年才知道,2020年才自己使用。用起來就一個字,爽!
簡介
Room是Google 針對Android數據庫操作封裝的一個組件,Room 持久性庫在 SQLite 的基礎上提供了一個抽象層,讓用戶能夠在充分利用 SQLite 的強大功能的同時,獲享更強健的數據庫訪問機制。這個組件將數據庫的建庫建表升級,增刪查改的代碼全部都幫你在編譯時去實現了。而你要做的就是寫你的數據類,數據操作類Dao,數據庫類Database。下面我們就看一個簡單的示例,看看Room的魅力。
示例
下面你會覺得非常快,因爲它用起來就是這麼快。
添加依賴
在AndroidStudio(不會還有人用Eclipse吧 😄)中的build.gradle中添加Gradle依賴。通常情況下這個build.gradle文件指的是module下的那個文件。但是也有些項目配置的主module在project的build.gradle裏面,所以看自己的用法。
apply plugin: 'kotlin-kapt'
dependencies {
implementation 'androidx.room:room-runtime:2.2.5'
implementation "androidx.room:room-ktx:2.2.5"
//這個是爲了編譯時根據註解生成代碼
kapt 'androidx.room:room-compiler:2.2.5'
}
下面就可以愉快的,使用Room了。
數據類(表)的創建
如果我們設計一個需要使用到數據庫的應用,首先我們肯定也是先設計表。來看一下用Room設計表。
@Entity(tableName = "people")
data class People(
@PrimaryKey @ColumnInfo(name = "name") val name: String,
@ColumnInfo(name = "age", defaultValue = "18") val age: Int, @ColumnInfo val address: String?
)
沒錯,我們的表建完了,並且我們的數據類也建好了,當列名和屬性名一樣是,甚至可以省略掉name。
數據庫操作類(DatabaseWrapper)的創建
package com.wanghang.mypeople
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
@Dao
interface PeopleDao {
@Insert(onConflict = OnConflictStrategy.IGNORE, entity = People::class)
fun insertPeople(people: People)
@Query("SELECT * FROM people WHERE name= :name LIMIT 1")
fun getPeople(name: String): People
@Query("SELECT age FROM people WHERE name= :name LIMIT 1")
fun getPeopleAge(name: String): Int
}
這個類就是我們以前經常寫的在應用內對本地數據庫進行增刪查改操作,可以想一想以前我們自己寫的DatabaseWrapper類有多複雜。這裏我就簡單寫了一個insert和一個query操作,對於代碼量和邏輯複雜度來說,就已經差距很明顯了。
很顯然這知識一個接口類,沒有真正的實現,因爲真正的實現編譯時kapt幫我們實現了。我們可以看下實現。
package com.wanghang.mypeople;
//刪掉import
@SuppressWarnings({"unchecked", "deprecation"})
public final class PeopleDao_Impl implements PeopleDao {
private final RoomDatabase __db;
private final EntityInsertionAdapter<People> __insertionAdapterOfPeople;
public PeopleDao_Impl(RoomDatabase __db) {
this.__db = __db;
this.__insertionAdapterOfPeople = new EntityInsertionAdapter<People>(__db) {
@Override
public String createQuery() {
return "INSERT OR IGNORE INTO `people` (`name`,`age`,`address`) VALUES (?,?,?)";
}
@Override
public void bind(SupportSQLiteStatement stmt, People value) {
if (value.getName() == null) {
stmt.bindNull(1);
} else {
stmt.bindString(1, value.getName());
}
stmt.bindLong(2, value.getAge());
if (value.getAddress() == null) {
stmt.bindNull(3);
} else {
stmt.bindString(3, value.getAddress());
}
}
};
}
@Override
public void insertPeople(final People people) {
__db.assertNotSuspendingTransaction();
__db.beginTransaction();
try {
__insertionAdapterOfPeople.insert(people);
__db.setTransactionSuccessful();
} finally {
__db.endTransaction();
}
}
@Override
public People getPeople(final String name) {
final String _sql = "SELECT * FROM people WHERE name= ? LIMIT 1";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
int _argIndex = 1;
if (name == null) {
_statement.bindNull(_argIndex);
} else {
_statement.bindString(_argIndex, name);
}
__db.assertNotSuspendingTransaction();
final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
try {
final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
final int _cursorIndexOfAge = CursorUtil.getColumnIndexOrThrow(_cursor, "age");
final int _cursorIndexOfAddress = CursorUtil.getColumnIndexOrThrow(_cursor, "address");
final People _result;
if(_cursor.moveToFirst()) {
final String _tmpName;
_tmpName = _cursor.getString(_cursorIndexOfName);
final int _tmpAge;
_tmpAge = _cursor.getInt(_cursorIndexOfAge);
final String _tmpAddress;
_tmpAddress = _cursor.getString(_cursorIndexOfAddress);
_result = new People(_tmpName,_tmpAge,_tmpAddress);
} else {
_result = null;
}
return _result;
} finally {
_cursor.close();
_statement.release();
}
}
@Override
public int getPeopleAge(final String name) {
final String _sql = "SELECT age FROM people WHERE name= ? LIMIT 1";
final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
int _argIndex = 1;
if (name == null) {
_statement.bindNull(_argIndex);
} else {
_statement.bindString(_argIndex, name);
}
__db.assertNotSuspendingTransaction();
final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
try {
final int _result;
if(_cursor.moveToFirst()) {
_result = _cursor.getInt(0);
} else {
_result = 0;
}
return _result;
} finally {
_cursor.close();
_statement.release();
}
}
}
可以看到其實這就是我們平時自己寫的DataWrapper類啊,而且還實現了cursor bind到對象上,數據庫操作還全部都保持了原子性操作,以及加鎖和釋放鎖的操作。其實這些代碼都可以稱之爲模板代碼,如果熟悉了數據查詢那一套,重複寫這些代碼還是很浪費時間並且容易出現疏漏。交給編譯器生成自然是最好不過。但是友情提示,還是要先會自己寫這些,懂得其中的原理更好。
數據庫創建
按照步驟,表結構設計好,數據查詢類設計好,下面就開始數據庫創建類了。
@Database(entities = [People::class], version = 1)
abstract class PeopleRoomDatabase : RoomDatabase() {
abstract fun peopleDao(): PeopleDao
companion object {
@Volatile
private var INSTANCE: PeopleRoomDatabase? = null
fun getDatabase(context: Context, scope: CoroutineScope): PeopleRoomDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
PeopleRoomDatabase::class.java,
"people_database"
)
.addCallback(PeopleDatabaseCallback(scope))
.build()
INSTANCE = instance
instance
}
}
}
}
這裏是使用單例去創建Database對象,可以看到@Database註解的參數,entities表示表的集合。因爲我的demo只有一個表,就直接將前面映射到表的數據類的類名加上就可以了。然後就是version,數據庫版本號,因爲是第一次牀架所以版本號就是1。
我們可以看一下@Database可以添加那些參數。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Database {
/**
* The list of entities included in the database. Each entity turns into a table in the
* database.
*
* @return The list of entities in the database.
*/
Class<?>[] entities();
/**
* The list of database views included in the database. Each class turns into a view in the
* database.
*
* @return The list of database views.
*/
Class<?>[] views() default {};
/**
* The database version.
*
* @return The database version.
*/
int version();
/**
* You can set the annotation processor argument ({@code room.schemaLocation}) to tell Room to
* export the database schema into a folder. Even though it is not mandatory, it is a good
* practice to have version history of your schema in your codebase and you should commit the
* schema files into your version control system (but don't ship them with your app!).
* <p>
* When {@code room.schemaLocation} is set, Room will check this variable and if it is set to
* {@code true}, the database schema will be exported into the given folder.
* <p>
* {@code exportSchema} is {@code true} by default but you can disable it for databases when
* you don't want to keep history of versions (like an in-memory only database).
*
* @return Whether the schema should be exported to the given folder when the
* {@code room.schemaLocation} argument is set. Defaults to {@code true}.
*/
boolean exportSchema() default true;
}