1、前言

最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面。

Android Architecture组件是Android Jetpack的一部分,它们是一组库,旨在帮助开发者设计健壮、可测试和可维护的应用程序,包含一下组件:

  • 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使用介绍
  • Android Jetpack组件App Startup简析
  • Android Jetpack组件之Hilt使用

本系列文章是各处copy过来的,个人感觉所有的开发者都应该尽早的熟悉Jetpack组件,相信一定会被它的魅力所吸引,最近也在完成一个使用以上所有组件实现的项目,作为对Jetpack组件的项目实践,下面来分析一下每个组件对项目开发的帮助。

2、Room 简介

Room是Google提供的一个ORM库。Room提供了三个主要的组件:

  • @Database:@Database用来注解类,并且注解的类必须是继承自RoomDatabase的抽象类。该类主要作用是创建数据库和创建Daos(data access objects,数据访问对象)。
  • @Entity:@Entity用来注解实体类,@Database通过entities属性引用被@Entity注解的类,并利用该类的所有字段作为表的列名来创建表。
  • @Dao:@Dao用来注解一个接口或者抽象方法,该类的作用是提供访问数据库的方法。在使用@Database注解的类中必须定一个不带参数的方法,这个方法返回使用@Dao注解的类

3、Room数据库使用

数据库的创建

  • 包含数据库持有者,并作为应用程序持久关系数据的基础连接的主要访问点,使用@Database注解,注解类应满足以下条件:
  1. 数据库必须是一个抽象类 RoomDatabase的扩展类
  2. 在注释中包括与数据库关联的实体列表
  3. 必须包含一个具有0个参数且返回带@Dao注释的类的抽象方法
  4. 通过调用 Room.databaseBuilder()或 获取实例Room.inMemoryDatabaseBuilder()创建数据库实例
  5. 使用单例实例化数据库对象
@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
  1. 使用@Entity注解实体类,Room会为实体中定义的每个字段创建一列,如果想避免使用@Ignore注解
  2. Room默认使用类名作为数据库表名,要修改表名使用 @Entity 的 tableName属性
  • 主键
  1. @PrimaryKey :至少定义一个字段作为主键
  2. 如果自增长ID 使用设置@PrimaryKey的 autoGenerate 属性
  3. 使用组合主键 使用@Entity 的@primaryKeys属性
  4. 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 {@PrimaryKeyvar bookId: Int = 0var title: String? = null@ColumnInfo(name = "user_id")var userId: Int = 0
}

嵌套对象@Embedded

  • 使用 @Embedded 注释来表示要分解到表中子字段的对象(此时数据库的列为两个类中所有的字段)
class Address {public var street: String? = nullpublic var state: String? = nullpublic var city: String? = null@ColumnInfo(name = "post_code")public var postCode = 0
}// 在User实体中引入Address
@Embedded
public var address: Address? = null

访问数据库

  • 使用@DAO注解:包含用于访问数据库的方法
@Dao
public interface UserDao {@Insert      // 添加数据注解void insertAll(User... users);@Delete    // 删除数据注解void delete(User user);
}

4、实例实战

  • 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 后再查询结果:此时的赵云已经改为张翼徳了:

  • 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{                                  // 1、根据要查询的字段创建POJO对象           @ColumnInfo(name = "name")public var name: String? = null@ColumnInfo(name = "city")public var city: String? = null
}@Query("SELECT name ,city FROM user")  // 2、查询的结果会映射到创建的对象中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")

5、更新数据库

  • 编写 Migration 的实例。每个 Migration 类指定一个startVersion和endVersion
  • Room运行每个 Migration 类的 migrate() 方法,使用正确的顺序将数据库迁移到更高版本
static final Migration MIGRATION_1_2 = new Migration(1, 2) { //由1升级到版本2@Overridepublic 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@Overridepublic 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列名:

6、引用复杂数据

Room提供了在原始类型和目标类型之间进行转换的功能,但不允许实体之间的对象引用,对于其他类型之间的使用需要自定义转换器

使用类型转换器

使用TypeConverter,它将自定义类转换为Room可以保留的已知类型,如:想保存Date类型,而Room无法持久化实例Date却可以实例long,因此提供和long的相互转换

public class Converters {@TypeConverterpublic static Date fromTimestamp(Long value) {return value == null ? null : new Date(value);}@TypeConverterpublic 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是如何实现这些过程的,从源码角度分析数据库。

7、源码分析

数据库的创建和升级

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 TryWithIdenticalCatchestry {@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;
......@Overridepublic 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) {@Overridepublic 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\")");}@Overridepublic 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) {@Overridepublic String createQuery() {  // 创建SupportSQLiteStatement时传入的Sql语句return "INSERT OR ABORT INTO `user`(`id`,`strength`,`name`,`street`,`state`,`city`,`post_code`) VALUES (nullif(?, 0),?,?,?,?,?,?)";}@Overridepublic 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); //   执行查询反会Cursortry {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组件的同学有所帮助!

Android Jetpack组件之 Room使用-源码相关推荐

  1. Android Jetpack组件之Navigation使用-源码

    1.前言 最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面. A ...

  2. Android Jetpack组件之 Paging使用-源码

    1.前言 最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面. A ...

  3. Android Jetpack组件之 LiveData使用-源码

    1.前言 最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面. A ...

  4. Android Jetpack 组件之 Lifecycle源码

    1.前言 最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面. A ...

  5. Android Jetpack组件之Hilt使用

    前言 最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面. And ...

  6. Android Jetpack组件App Startup简析

    1.前言 最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面. A ...

  7. Android Jetpack组件之WorkManger使用介绍

    1.前言 最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面. A ...

  8. Android Jetpack组件之ViewModel使用

    1.前言 最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面. A ...

  9. Android Jetpack 组件之 Lifecycle使用

    1.前言 最近简单看了下google推出的框架Jetpack,感觉此框架的内容可以对平时的开发有很大的帮助,也可以解决很多开发中的问题,对代码的逻辑和UI界面实现深层解耦,打造数据驱动型UI界面. A ...

最新文章

  1. hadoop启动_Mac OS X 上搭建 Hadoop 开发环境指南
  2. ubuntu下制作u盘启动盘
  3. 2016总结 - 我的转型之路
  4. 大话Django之一:安装与启动
  5. sql表中只有子节点的递归_动态规划与静态规划、递归、分治、回溯
  6. springboot配置多个数据库环境
  7. java多线程编程——锁优化
  8. 保护眼睛的电脑设置_干货,怎么设置显示器,才能保护好我们的眼睛?
  9. 信息学奥赛一本通 1232:Crossing River | OpenJudge NOI 4.6 702:Crossing River
  10. ubuntu无法定位软件包问题
  11. TClientDataSet[1]: 浏览测试数据
  12. threejs 热力图做成材质_冷轧钢与热轧钢有什么区别?
  13. 吴恩达神经网络和深度学习-学习笔记-14-学习率衰减
  14. 错误ImportError: 'No module named skimage.io'和ImportError: No module named google.protobuf.internal
  15. java读取外部配置文件_SpringBoot读取外部配置文件的方法
  16. 亿赛通加密软件不显示小锁图标
  17. 我的Android进阶之旅------关于调用Webservice查询火车票时刻表的几个接口介绍
  18. 为何公众号推送会延迟发送_微信公众号客服消息群发和推送功能如何实现?
  19. 互联网数据分析岗实习感受
  20. java抢红包线段分割法_抢红包算法——线段分割法

热门文章

  1. matlab中filter卷积补零,matlab中的卷积——filter,conv之间的区别
  2. python reader循环_python – 多次循环遍历csv.DictReader行
  3. android开发获取应用本身耗电量_别找了,Android常用自动化工具全在这儿了!
  4. clone是深拷贝还是浅拷贝_Cloneable接口的作用与探索理解浅拷贝与深拷贝
  5. jquery之行自加自减
  6. WPF: RenderTransform特效
  7. php 商场收银收费系统,使用的策略模式
  8. Macbook pro安装MacOS系统
  9. dispatchTouchEvent onInterceptTouchEvent onTouchEvent
  10. Python你必须知道的十个库