Android Jetpack架構組件之 Room(使用、源碼篇)

一、前言
最近簡單看了下google推出的框架Jetpack,感受此框架的內容能夠對平時的開發有很大的幫助,也能夠解決不少開發中的問題,對代碼的邏輯和UI界面實現深層解耦,打造數據驅動型UI界面。java

Android Architecture組件是Android Jetpack的一部分,它們是一組庫,旨在幫助開發者設計健壯、可測試和可維護的應用程序,包含一下組件:sql

帶你領略Android Jetpack組件的魅力
Android Jetpack 架構組件之 Lifecycle(使用篇)
Android Jetpack 架構組件之 Lifecycle(源碼篇)
Android Jetpack 架構組件之 ViewModel (源碼篇)
Android Jetpack 架構組件之 LiveData(使用、源碼篇)
Android Jetpack架構組件之 Paging(使用、源碼篇)
Android Jetpack 架構組件之 Room(使用、源碼篇)
Android Jetpack 架構組件之Navigation
Android Jetpack架構組件之WorkManger
實戰:從0搭建Jetpack版的WanAndroid客戶端
上述時Android Architecture所提供的架構組件,本文主要從使用和源碼的角度分析Room組件。數據庫

二、Room 簡介
Room是Google提供的一個ORM庫。Room提供了三個主要的組件:架構

@Database:@Database用來註解類,而且註解的類必須是繼承自RoomDatabase的抽象類。該類主要做用是建立數據庫和建立Daos(data access objects,數據訪問對象)。
@Entity:@Entity用來註解實體類,@Database經過entities屬性引用被@Entity註解的類,並利用該類的全部字段做爲表的列名來建立表。
@Dao:@Dao用來註解一個接口或者抽象方法,該類的做用是提供訪問數據庫的方法。在使用@Database註解的類中必須定一個不帶參數的方法,這個方法返回使用@Dao註解的類
三、Room數據庫使用
數據庫的建立app

包含數據庫持有者,並做爲應用程序持久關係數據的基礎鏈接的主要訪問點,使用@Database註解,註解類應知足如下條件:
數據庫必須是一個抽象類 RoomDatabase的擴展類
在註釋中包括與數據庫關聯的實體列表
必須包含一個具備0個參數且返回帶@Dao註釋的類的抽象方法
經過調用 Room.databaseBuilder()或 獲取實例Room.inMemoryDatabaseBuilder()建立數據庫實例
使用單例實例化數據庫對象
@Database(entities = {User.class}, version = 1)  // 註釋
public abstract class AppDatabase extends RoomDatabase {
    public abstract UserDao userDao();  // 抽象方法
}
以單例形式對外提供RoomDataBase實例
public static UserDataBase getInstance(Context context) {
    if (userDataBase == null) {
        synchronized (UserDataBase.class) {
            if (userDataBase == null) {
                userDataBase = Room.databaseBuilder(context.getApplicationContext()
                        , UserDataBase.class, "user_data").build();
            }
        }
    }
    return userDataBase;
}
定義實體數據:表示數據庫中的表框架

@Entity
使用@Entity註解實體類,Room會爲實體中定義的每一個字段建立一列,若是想避免使用@Ignore註解
Room默認使用類名做爲數據庫表名,要修改表名使用 @Entity 的 tableName屬性
主鍵
 @PrimaryKey :至少定義一個字段做爲主鍵
若是自增加ID 使用設置@PrimaryKey的 autoGenerate 屬性
使用組合主鍵 使用@Entity 的@primaryKeys屬性
Room 默認使用字段名成做爲列名,要修改使用 @ColumnInfo(name = "***") 
@Entity(tableName = "userDataBase")
class User {
    @PrimaryKey(autoGenerate = true)    // 單個主鍵設置爲自增加
    public var id = 0
    @ColumnInfo(name = "nameUser")  // 定義列名
    public var name: String? = null
}
 
@Entity(primaryKeys = ["id", "name"])  // 組合組件
添加索引@Entity
使用 @Entity 的indices 屬性,列出要包含在索引或複合索引中的列的名稱
@Entity(indices = [Index("nameUser"), Index(value = ["name"])])  // 建立索引
@Entity(indices = [Index("nameUser"), Index(value = ["name"] ,unique = true)]) //惟一索引
外鍵約束@ForeignKey
使用@ForeignKey 註釋定義其與實體的 關係;ForeignKey中 entity 爲要關聯的父實體類;parentColumns 爲關聯父實體類的列名;childColumns此實體類中的列名
@Entity(foreignKeys = [ForeignKey(entity = User::class,
        parentColumns = ["id"],
        childColumns = ["user_id"])])
class Book {
    @PrimaryKey
    var bookId: Int = 0
    var title: String? = null
    @ColumnInfo(name = "user_id")
    var userId: Int = 0
}
嵌套對象@Embedded 
使用 @Embedded 註釋來表示要分解到表中子字段的對象(此時數據庫的列爲兩個類中全部的字段)
class Address {
    public var street: String? = null
    public var state: String? = null
    public var city: String? = null
    @ColumnInfo(name = "post_code")
    public var postCode = 0
}
 
// 在User實體中引入Address
@Embedded
public var address: Address? = null
訪問數據庫ide

使用@DAO註解:包含用於訪問數據庫的方法
@Dao                   
public interface UserDao {
    @Insert      // 添加數據註解
    void insertAll(User... users);
 
    @Delete    // 刪除數據註解
    void delete(User user);
}
四、實例實戰
insert:使用註解@Insert,Room會自動將全部參數在單個事物中插入數據庫
@Insert
public fun inertUser(user: User)   // 單個參數能夠返回 long
 
@Insert
public fun insertUserList(array: Array<User>)  // 參數爲集合能夠返回long[]
數據庫添加User
val user = User()
user.name = "趙雲 編號 = $number"
val address = Address()
address.street = "成都接頭"
address.state = "蜀漢"
address.city = "常山"
address.postCode = 10010
user.address = address
userDao.inertUser(user)   // 添加User
添加數據結果:源碼分析


upadte:使用 @Update註解
@Update
public fun update(user: User)     // 可讓此方法返回一個int值,表示數據庫中更新的行數  
 
 
val user = User()
user.id = 1
user.name = "張翼德"
address.city = "涿郡"
.....
userDao.update(user)
點擊 Update 後再查詢結果:此時的趙雲已經改成張翼徳了post


delete:使用@Delete註解
@Delete 
public fun delete(user: User)    //能夠返回一個int值,表示從數據庫中刪除的行數
 
 
val user = User()
user.id = 1      // 要刪除的主鍵 id
userDao.delete(user)
點擊delete後再次查詢數據:編號爲1的數據已被刪除測試


查詢信息 :@Query註解對數據庫執行讀/寫操做
@Query("SELECT * FROM user")
public fun selectAll(): Array<User>    // 查詢全部數據
 
@Query("SELECT * FROM user WHERE name = :name")
public fun selectUser(name:String): Array<User>    // 條件查詢
返回列的子集:建立子類在每一個屬性中使用@ColumnInfo(name = "name")標記對應數據庫中的列名
public class UserTuple{                                  // 一、根據要查詢的字段建立POJO對象           
    @ColumnInfo(name = "name")
    public var name: String? = null
    @ColumnInfo(name = "city")
    public var city: String? = null
}
 
@Query("SELECT name ,city FROM user")  // 二、查詢的結果會映射到建立的對象中
    public List<UserTuple> loadFullName();
 
 
val userList = userDao.loadFullName()
for (userTuple in userList) {
    stringBuilder.append(userTuple.name)
            .append("   ")
            .append(userTuple.city)
            .append("\n")
}
輸出的結果:只有name和city兩列


範圍條件查詢 :查詢城市中全部用戶
@Query("SELECT name ,street FROM user WHERE city IN (:cityArray)")
fun loadUserInCity(cityArray: Array<String>): List<UserTuple>
 
 
val userList = userDao.loadUserInCity(arrayOf("常山"))  // 查詢常山,只會出現趙雲不會出現張翼德


Observable查詢:使用LiveData做爲查詢方法的返回值,註冊觀察者後,數據表更改時自動更新UI
@Query("SELECT name ,street FROM user WHERE city IN (:cityArray"))
fun loadUserInCityLive(cityArray: Array<String>): LiveData<List<UserTuple>>
 
 
 
private lateinit var liveData: LiveData<Array<UserTuple>>   // 定義一個LiveData
get() {
return userDao.loadUserInCityLive(arrayOf("常山"))
}
 
val observer = Observer<Array<UserTuple>> {      // 定義一個觀察者
    val stringBuilder = StringBuilder()
    for (index in it!!.indices) {
        val userTuple = it[index]
        stringBuilder.append(userTuple.name)
                .append("   ")
                .append(userTuple.name)
                .append("   \n")
    }
   tv_main_show.text = stringBuilder.toString()
}
liveData.observe(this, observer)     // 註冊觀察者
運行結果:此時當添加數據時,UI會自動更新;

RxJava 查詢 :返回Observable實例可使用RxJava訂閱觀察者
@Query("SELECT * FROM user WHERE id = :id LIMIT 1")
fun loadUserRxJava(id:Int) : Flowable<User>
 
 
userDao.loadUserRxJava(4)
        .subscribe(Consumer {
            val stringBuilder = StringBuilder()
            stringBuilder.append(it.id)
                    .append("   ")
                    .append(it.name)
                    .append("   \n")
            tv_main_show.text = stringBuilder.toString()
        })


 Cursor查詢:返回Cursor對象
fun loadUserCursor(id:Int) : Cursor
多表查詢:根據表的外鍵多表查詢
@Query("SELECT user.name AS userName, pet.name AS petName "
          + "FROM user, pet "
          + "WHERE user.id = pet.user_id")
五、更新數據庫
編寫 Migration 的實例。每一個 Migration 類指定一個startVersion和endVersion
Room運行每一個 Migration 類的 migrate() 方法,使用正確的順序將數據庫遷移到更高版本
static final Migration MIGRATION_1_2 = new Migration(1, 2) { //由1升級到版本2
    @Override
    public void migrate(SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE book (id  INTEGER , name TEXT )")
    }
};
 
static final Migration MIGRATION_2_3 = new Migration(2, 3) { //由2升級到版本3
    @Override
    public void migrate(SupportSQLiteDatabase database) {
         database.execSQL("ALTER TABLE user ADD COLUMN strength INTEGER NOT NUll DEFAULT 0")  //添加strength列
    }
};
 
 
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
        .addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
升級完數據庫後再次查詢,結果顯示數據庫增長了strength列名:

六、引用複雜數據
Room提供了在原始類型和目標類型之間進行轉換的功能,但不容許實體之間的對象引用,對於其餘類型之間的使用須要自定義轉換器

使用類型轉換器
使用TypeConverter,它將自定義類轉換爲Room能夠保留的已知類型,如:想保存Date類型,而Room沒法持久化實例Date卻能夠實例long,所以提供和long的相互轉換

public class Converters {
 
    @TypeConverter
    public static Date fromTimestamp(Long value) {
        return value == null ? null : new Date(value);
    }
 
    @TypeConverter
    public static Long dateToTimestamp(Date date) {
        return date == null ? null : date.getTime();
    }
}
在抽象數據庫類中添加轉換註解
@TypeConverters({Converters.class})
使用 類型轉換器
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List findUsersBornBetweenDates(Date from, Date to);
以上就是數據庫Room的使用簡介了,基本數據庫的增刪改查以及常見的設置都在其中了,下面咱們來看看Room是如何實現這些過程的,從源碼角度分析數據庫。

七、源碼分析
數據庫的建立和升級

Room數據庫實例的建立由Room.databaseBuilder(context.applicationContext,RoomTestData::class.java, "Sample.db").build()開始的,從代碼中看出時使用Builder模式建立DataBase,因此咱們先看看RoomDatabase.Builde類

RoomDatabase.Builder:除了包含Room的實現類、數據庫名稱的常規設置外,也包含了數據庫的升級信息
@NonNull
public Builder<T> addMigrations(@NonNull  Migration... migrations) {  // 添加數據庫版本升級信息
    if (mMigrationStartAndEndVersions == null) {
        mMigrationStartAndEndVersions = new HashSet<>();
    }
    for (Migration migration: migrations) {
        mMigrationStartAndEndVersions.add(migration.startVersion);
        mMigrationStartAndEndVersions.add(migration.endVersion);
    }
 
    mMigrationContainer.addMigrations(migrations);
    return this;
}
build():建立並初始化數據庫
private static final String DB_IMPL_SUFFIX = "_Impl"
。。。。。。
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX); // 建立DataBase實現類的實例 
db.init(configuration);  // 初始化數據庫
getGeneratedImplementation():反射建立DataBase的實現類
static <T, C> T getGeneratedImplementation(Class<C> klass, String suffix) {
    final String fullPackage = klass.getPackage().getName();  
    String name = klass.getCanonicalName();
    final String postPackageName = fullPackage.isEmpty()
            ? name
            : (name.substring(fullPackage.length() + 1));  // 獲取類名
    final String implName = postPackageName.replace('.', '_') + suffix;  // 拼接類名
    //noinspection TryWithIdenticalCatches
    try {
 
        @SuppressWarnings("unchecked")
        final Class<T> aClass = (Class<T>) Class.forName(
                fullPackage.isEmpty() ? implName : fullPackage + "." + implName);   // 獲取自動生成的類文件
        return aClass.newInstance();  // 建立並返回實例
    } catch (ClassNotFoundException e) {
        。。。。。。
    } 
}
此處獲取到的是系統根據註解自動建立的是實現類RoomDataBase_Impl,Room採用的是註解自動生成代碼方式,根據@DataBase和@Dao的註解,自動生成這兩個註解標記的實現類,系統建立類以下圖:

RoomTestData_Impl:系統自動生成的實現類
public class RoomTestData_Impl extends RoomTestData {
  private volatile UserDao _userDao;
......
  @Override
  public UserDao userDao() {   
    if (_userDao != null) {
      return _userDao;
    } else {
      synchronized(this) {
        if(_userDao == null) {
          _userDao = new UserDao_Impl(this);  // 建立並返回UserDao的實例
        }
        return _userDao;
      }
    }
  }
}
從上面的代碼中看出,系統自動建立了RoomTestData的實現類,並重寫了抽象方法userDao(),在userDao()中使用單例的方式提供UserDao的實現類UserDao_Impl,UserDao_Impl的造成和RoomTestData_Impl的生成同樣,在代碼中從DataBase中調用userDao返回的就是UserDao_Impl的實例;

接着分析數據庫的建立,在上面的代碼中有一句數據庫的初始化代碼db.init(),在db.init()的方法中會調用RoomDataBase中的抽象方法createOpenHelper(),這裏調用的是createOpenHelper()就是RoomTestData_Impl自動實現的方法:

protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
 
  final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(3) {
    @Override
    public void createAllTables(SupportSQLiteDatabase _db) {
      _db.execSQL("CREATE TABLE IF NOT EXISTS `user` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `strength` INTEGER NOT NULL, `name` TEXT, `street` TEXT, `state` TEXT, `city` TEXT, `post_code` INTEGER)");   // 建立數據庫
      _db.execSQL("CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)");
      _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"8ece9a1581b767a0f460940849e9b463\")");
    }
 
    @Override
    public void dropAllTables(SupportSQLiteDatabase _db) {
      _db.execSQL("DROP TABLE IF EXISTS `user`");  // 刪除數據庫
    }
 
   @Override
protected void validateMigration(SupportSQLiteDatabase _db) {  // 處理數據庫的版本升級
 。。。。。。
}
  }, "8ece9a1581b767a0f460940849e9b463", "061261cef54147a569851cbbb906c3be");
}
 。。。。。。
  return _helper;
}
上面的代碼中執行一下操做:

建立SupportSQLiteOpenHelper.Callback 的實例並重寫方法
在onCreate()中Sql語句建立user表和room_master_table表
在dropAllTables()中建立刪除數據庫的SQL語句
在validateMigration()中完成數據庫的升級
上面SupportSQLiteOpenHelper.Callback 的實現類爲RoomOpenHelper,下面一塊兒看看RoomOpenHelper源碼:

@Override
public void onCreate(SupportSQLiteDatabase db) {
    updateIdentity(db);
    mDelegate.createAllTables(db);  // mDelegate爲上面建立的RoomOpenHelper.Delegate實例
    mDelegate.onCreate(db);
}
 
@Override
public void onUpgrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
if (mConfiguration != null) {
    List<Migration> migrations = mConfiguration.migrationContainer.findMigrationPath(
            oldVersion, newVersion);
    if (migrations != null) {
        for (Migration migration : migrations) {
            migration.migrate(db);
        }
        mDelegate.validateMigration(db);   // 調用validateMigration方法處理數據庫的更新
        updateIdentity(db);
        migrated = true;
    }
}
}
 
@Override
public void onDowngrade(SupportSQLiteDatabase db, int oldVersion, int newVersion) {
    onUpgrade(db, oldVersion, newVersion);
}
從上面代碼中能夠看出,在onCreate()方法中調用了mDelegate.createAllTables(db),這裏的mDelegate就是上面建立RoomOpenHelper方法中第二個參數RoomOpenHelper.Delegate,因此這裏就是在onCreate()中建立了數據庫,在onUPgrade()中調用 mDelegate.validateMigration(db)完成數據庫的升級,到這裏數據庫的建立和升級已經介紹完畢了,下面就一塊兒看看Room是如何訪問數據庫的。

數據庫的訪問

@Dao數據庫的實現類: UserDao_Impl
private final RoomDatabase __db;   // 傳入的數據庫
 
private final EntityInsertionAdapter __insertionAdapterOfUser;  // 處理insert方法
 
private final EntityDeletionOrUpdateAdapter __deletionAdapterOfUser;   // 處理delete方法
 
private final EntityDeletionOrUpdateAdapter __updateAdapterOfUser;  // 處理update方法
在UserDao_Impl的類中除了數據庫RoomDataBase實例外,還有三個成員變量分別爲:__insertionAdapterOfUser、__deletionAdapterOfUser、__updateAdapterOfUser,從名字上能夠看出來他們三個分別對應數據庫增、刪、改的三個操做,咱們以insert操做爲例,查看insert方法:

@Override
public void inertUser(User user) {
  __db.beginTransaction();
  try {
    __insertionAdapterOfUser.insert(user);
    __db.setTransactionSuccessful();
  } finally {
    __db.endTransaction();
  }
}
insert()方法的實現是在__insertionAdapterOfUser中執行的,查看__insertionAdapterOfUser的實現

this.__insertionAdapterOfUser = new EntityInsertionAdapter<User>(__db) {
  @Override
  public String createQuery() {  // 建立SupportSQLiteStatement時傳入的Sql語句
    return "INSERT OR ABORT INTO `user`(`id`,`strength`,`name`,`street`,`state`,`city`,`post_code`) VALUES (nullif(?, 0),?,?,?,?,?,?)";
  }
 
  @Override
  public void bind(SupportSQLiteStatement stmt, User value) {
    stmt.bindLong(1, value.getId());
    stmt.bindLong(2, value.getStrength());
    if (value.getName() == null) {  // 判斷此列是否爲null,部位Null則設置數據
      stmt.bindNull(3);
    } else {
      stmt.bindString(3, value.getName());
    }
    final Address _tmpAddress = value.getAddress();
    if(_tmpAddress != null) {
      if (_tmpAddress.getStreet() == null) {
        stmt.bindNull(4);
      } else {
        stmt.bindString(4, _tmpAddress.getStreet());
      }
      if (_tmpAddress.getState() == null) {
        stmt.bindNull(5);
      } else {
        stmt.bindString(5, _tmpAddress.getState());
      }
      if (_tmpAddress.getCity() == null) {
        stmt.bindNull(6);
      } else {
        stmt.bindString(6, _tmpAddress.getCity());
      }
      stmt.bindLong(7, _tmpAddress.getPostCode());
    } else {
      stmt.bindNull(4);
      stmt.bindNull(5);
      stmt.bindNull(6);
      stmt.bindNull(7);
    }
  }
};
__insertionAdapterOfUser的實例重寫了兩個方法:

createQuery():建立數據庫插入數據的sql語句
bind():綁定數據庫中每一個列對應的值
__insertionAdapterOfUser.insert()
insert()方法中建立SupportSQLiteStatement的實例,並調用bind()完成數據的綁定,而後執行stmt.executeInsert()插入數據

public final void insert(T entity) {
    final SupportSQLiteStatement stmt = acquire();  // 最終建立的是FrameworkSQLiteStatement的包裝的SQLiteStatement實例
    try {
        bind(stmt, entity);  // 綁定要插入的數據
        stmt.executeInsert();   // 提交保存數據,執行
    } finally {
        release(stmt);
    }
}
 
 
@Override
public long executeInsert() {  // 最終執行數據庫的插入操做
    return mDelegate.executeInsert();
}
查尋數據庫
在UserDao_Impl中自動實現了查詢的方法selectUser:

@Override
public User[] selectUser(String name) {
  final String _sql = "SELECT * FROM user WHERE name = ?";
  final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);   //   建立RoomSQLiteQuery 
  int _argIndex = 1;
  if (name == null) {
    _statement.bindNull(_argIndex);
  } else {
    _statement.bindString(_argIndex, name);
  }
  final Cursor _cursor = __db.query(_statement); //   執行查詢反會Cursor
  try {
    final int _cursorIndexOfId = _cursor.getColumnIndexOrThrow("id");
    final int _cursorIndexOfStrength = _cursor.getColumnIndexOrThrow("strength");
    final int _cursorIndexOfName = _cursor.getColumnIndexOrThrow("name");
    final int _cursorIndexOfStreet = _cursor.getColumnIndexOrThrow("street");
    final int _cursorIndexOfState = _cursor.getColumnIndexOrThrow("state");
    final int _cursorIndexOfCity = _cursor.getColumnIndexOrThrow("city");
    final int _cursorIndexOfPostCode = _cursor.getColumnIndexOrThrow("post_code");
    final User[] _result = new User[_cursor.getCount()];
    int _index = 0;
    while(_cursor.moveToNext()) {
      final User _item;
      final Address _tmpAddress;
      if (! (_cursor.isNull(_cursorIndexOfStreet) && _cursor.isNull(_cursorIndexOfState) && _cursor.isNull(_cursorIndexOfCity) && _cursor.isNull(_cursorIndexOfPostCode))) {
        _tmpAddress = new Address();
        final String _tmpStreet;
        _tmpStreet = _cursor.getString(_cursorIndexOfStreet);
        _tmpAddress.setStreet(_tmpStreet);
        final String _tmpState;
        _tmpState = _cursor.getString(_cursorIndexOfState);
        _tmpAddress.setState(_tmpState);
        final String _tmpCity;
        _tmpCity = _cursor.getString(_cursorIndexOfCity);
        _tmpAddress.setCity(_tmpCity);
        final int _tmpPostCode;
        _tmpPostCode = _cursor.getInt(_cursorIndexOfPostCode);
        _tmpAddress.setPostCode(_tmpPostCode);
      }  else  {
        _tmpAddress = null;
      }
      _item = new User();
      final int _tmpId;
      _tmpId = _cursor.getInt(_cursorIndexOfId);
      _item.setId(_tmpId);
      final int _tmpStrength;
      _tmpStrength = _cursor.getInt(_cursorIndexOfStrength);
      _item.setStrength(_tmpStrength);
      final String _tmpName;
      _tmpName = _cursor.getString(_cursorIndexOfName);
      _item.setName(_tmpName);
      _item.setAddress(_tmpAddress);
      _result[_index] = _item;
      _index ++;
    }
    return _result;
  } finally {
    _cursor.close();
    _statement.release();
  }
}
上面執行的也是數據庫的正常操做,先建立了RoomSQLiteQuery的實例,在調用db。query()執行查詢,查詢返回Cursor實例,最終從Cursor中獲取信息轉換爲對象並返回數據。

到此Room的使用和源碼執行流程就到此結束了,本文旨在執行的流程分析,具體的如何使用SQLite數據庫操做的讀者能夠本身點擊源碼查看,不過使用的SQLite的查詢和添加方法和平時使用的不一樣,讀者想分析的話就會找到了,好了,但願本篇文章對想了解和使用Room組件的同窗有所幫助!

相關文章
相關標籤/搜索