Android Jetpack架构组件之 Room(使用、源码篇)

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
  • 实战:从0搭建Jetpack版的WanAndroid客户端​​​​​​​

上述时Android Architecture所提供的架构组件,本文主要从使用和源码的角度分析Room组件。

2、Room 简介

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

  1. @Database:@Database用来注解类,并且注解的类必须是继承自RoomDatabase的抽象类。该类主要作用是创建数据库和创建Daos(data access objects,数据访问对象)。
  2. @Entity:@Entity用来注解实体类,@Database通过entities属性引用被@Entity注解的类,并利用该类的所有字段作为表的列名来创建表。
  3. @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
  1. 使用 @Entity 的indices 属性,列出要包含在索引或复合索引中的列的名称
@Entity(indices = [Index("nameUser"), Index(value = ["name"])])  // 创建索引
@Entity(indices = [Index("nameUser"), Index(value = ["name"] ,unique = true)]) //唯一索引
  • 外键约束@ForeignKey
  1. 使用@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 
  1. 使用 @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

访问数据库

  • 使用@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)  // 参数为集合可以返回long[]
  1. 数据库添加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
  1. 添加数据结果:

Android Jetpack架构组件之 Room(使用、源码篇)_第1张图片

  • upadte:使用 @Update注解
@Update
public fun update(user: User)     // 可以让此方法返回一个int值,表示数据库中更新的行数  


val user = User()
user.id = 1
user.name = "张翼德"
address.city = "涿郡"
.....
userDao.update(user)
  1. 点击 Update 后再查询结果:此时的赵云已经改为张翼徳了

Android Jetpack架构组件之 Room(使用、源码篇)_第2张图片

  • delete:使用@Delete注解
@Delete 
public fun delete(user: User)    //可以返回一个int值,表示从数据库中删除的行数


val user = User()
user.id = 1      // 要删除的主键 id
userDao.delete(user)
  1. 点击delete后再次查询数据:编号为1的数据已被删除

  • 查询信息 :@Query注解对数据库执行读/写操作
@Query("SELECT * FROM user")
public fun selectAll(): Array    // 查询所有数据

@Query("SELECT * FROM user WHERE name = :name")
public fun selectUser(name:String): Array    // 条件查询
  • 返回列的子集:创建子类在每个属性中使用@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 loadFullName();


val userList = userDao.loadFullName()
for (userTuple in userList) {
    stringBuilder.append(userTuple.name)
            .append("   ")
            .append(userTuple.city)
            .append("\n")
}
  1. 输出的结果:只有name和city两列

  • 范围条件查询 :查询城市中所有用户
@Query("SELECT name ,street FROM user WHERE city IN (:cityArray)")
fun loadUserInCity(cityArray: Array): List


val userList = userDao.loadUserInCity(arrayOf("常山"))  // 查询常山,只会出现赵云不会出现张翼德

  • Observable查询:使用LiveData作为查询方法的返回值,注册观察者后,数据表更改时自动更新UI
@Query("SELECT name ,street FROM user WHERE city IN (:cityArray"))
fun loadUserInCityLive(cityArray: Array): LiveData>



private lateinit var liveData: LiveData>   // 定义一个LiveData
get() {
return userDao.loadUserInCityLive(arrayOf("常山"))
}

val observer = Observer> {      // 定义一个观察者
    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会自动更新;

Android Jetpack架构组件之 Room(使用、源码篇)_第3张图片

  • RxJava 查询 :返回Observable实例可以使用RxJava订阅观察者
@Query("SELECT * FROM user WHERE id = :id LIMIT 1")
fun loadUserRxJava(id:Int) : Flowable


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
    @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列名:

6、引用复杂数据

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是如何实现这些过程的,从源码角度分析数据库。

7、源码分析

数据库的创建和升级

Room数据库实例的创建由Room.databaseBuilder(context.applicationContext,RoomTestData::class.java, "Sample.db").build()开始的,从代码中看出时使用Builder模式创建DataBase,所以我们先看看RoomDatabase.Builde类

  • RoomDatabase.Builder:除了包含Room的实现类、数据库名称的常规设置外,也包含了数据库的升级信息
@NonNull
public Builder 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 getGeneratedImplementation(Class 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 aClass = (Class) 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;
}

上面的代码中执行一下操作:

  1. 创建SupportSQLiteOpenHelper.Callback 的实例并重写方法
  2. 在onCreate()中Sql语句创建user表和room_master_table表
  3. 在dropAllTables()中创建删除数据库的SQL语句
  4. 在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 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(__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的实例重写了两个方法:

  1. createQuery():创建数据库插入数据的sql语句
  2. 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组件的同学有所帮助!

本文使用Room的Demo,欢迎Star

你可能感兴趣的:(Android,Jetpack)