如何应对 Android 面试官 -> 玩转 Jetpack Room

前言


Android Jetpack 的出现统一了 Android 开发生态,各种三方库逐渐被官方组件所取代。 Room 也同样如此,逐渐取代竞品成为最主流的数据库 ORM 框架。 这当然不仅仅因为其官方身份,更是因为其良好的开发体验,大大降低了 SQLite 的使用门槛。

相对于SQLiteOpenHelper等传统方法,使用Room操作SQLite有以下优势:

1.编译期的SQL语法检查

2.开发高效,避免大量模板代码

3.API设计友好,容易理解

4.可与 LiveData 关联,具备 LiveData Lifecycle 的所有魅力

Room 的使用,主要涉及三个类: Entity、DataBase、DAO

  • Database: 访问底层数据库的入口;
  • Entity: 代表数据库中的表(table),一般用注解;
  • Data Access Object (DAO): 数据库访问者;

这三个组件的概念也出现在其他 ORM 框架中,有过使用经验的同学理解起来并不困难:通过 Database 获取 DAO,然后通过 DAO 查询并获取 entities,最终通过 entities 对数据库 table 中数据进行读写;

用法篇


使用方式我们介绍下基础使用和结合 MVVM 的使用

基础使用

导入依赖

kotlin 复制代码
// 下面是 ROOM依赖相关的代码
def room_version = "xxx"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version" // use kapt for Kotlin

// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"

我们先来了解下它的简单使用,遵循 Entity、DAO、DataBase

我们先来声明 Entity

typescript 复制代码
@Entity
public class Student {

    // 主键 SQL 唯一的 autoGenerate 自增长
    @PrimaryKey(autoGenerate = true)
    private int uid;

    @ColumnInfo(name = "name")
    private String name;

    @ColumnInfo(name = "pwd")
    private String password;

    @ColumnInfo(name = "address")
    private int address;

    public Student(String name, String password, int address) {
        this.name = name;
        this.password = password;
        this.address = address;
    }

    public int getUid() {
        return uid;
    }

    public void setUid(int uid) {
        this.uid = uid;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public int getAddress() {
        return address;
    }

    public void setAddress(int address) {
        this.address = address;
    }

    @Override
    public String toString() {
        return "Student{" +
                "uid=" + uid +
                ", name='" + name + '\'' +
                ", password='" + password + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

这样 Entity 我们就声明好了;@ColumnInfo 注解可有可无,没有就会使用我们声明的属性来创建列名,声明了就用我们声明的来创建列名;

接下来,我们来声明 Dao,用来做增、删、改、查操作;

less 复制代码
@Dao
public interface StudentDao {

    @Insert
    void insert(Student... students);

    @Delete
    void delete(Student student);

    @Update
    void update(Student student);

    @Query("select * from Student")
    List<Student> getAll();

    // 查询一条记录
    @Query("select * from Student where name like:name")
    Student findByName(String name);

    // 数组查询 多个记录
    @Query("select * from Student where uid in(:userIds)")
    List<Student> getAllId(int[] userIds);

    // 就是只查询 name pwd 给 StudentTuple 类接收
    @Query("select name,pwd from Student")
    StudentTuple getRecord();
}

PS:Dao 的声明必须要用 interface来声明;

如果我们只想查询数据库表中的 某x个字段,那么我们需要声明一个类来接收相关字段,切这个类中声明的字段要和 Entity 中保持一致;

typescript 复制代码
public class StudentTuple {

    @ColumnInfo(name = "name")
    public String name;

    @ColumnInfo(name="pwd")
    public String password;

    public StudentTuple(String name, String password) {
        this.name = name;
        this.password = password;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "StudentTuple{" +
                "name='" + name + '\'' +
                ", password='" + password + '\'' +
                '}';
    }

}

这个类 @Entity 不能加,加了就是一张表了;

接下来,我们来声明 Database

scala 复制代码
@Database(entities = { Student.class }, version = 1, exportSchema = false)
public abstract class AppDataBase extends RoomDatabase {
    // 暴露dao
    public abstract StudentDao userDao();
}
  1. 必须要用抽象类

  2. 必须要继承 RoomDatabase

  3. 要写 exportSchema = false 导出模式,防止后续升级的异常

通过 APT 技术帮我们生成具体的实现类,所以 Dao 和 Database 都需要抽象的类;

接下来,我们在 Activity 中调用;

scala 复制代码
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        // 数据库的操作应该是在子线程
        DbTest t = new DbTest();
        t.start();
    }
    
    public class DbTest extends Thread {
        @Override
        public void run() {
            // 核心逻辑1 数据库的操作都在这里
            AppDataBase marsDB = Room.databaseBuilder(getApplicationContext(),
                    AppDataBase.class,
                    "MarsDB")
                    // 可以设置强制主线程,默认是让你用子线程
                    // .allowMainThreadQueries()
                    .build();

            // 执行增、删、改、查操作
            StudentDao dao = marsDB.userDao();
            // 增
            dao.insert(new Student("Mars0", "123", 1));
            dao.insert(new Student("Mars1", "456", 2));
            dao.insert(new Student("Mars2", "789", 3));
            dao.insert(new Student("Mars3", "111", 4));

            // 查询全部数据
            List<Student> all = dao.getAll();
            Log.d("Mars", "run: all.toString(): " + all.toString());

            Log.i("Mars", "--------------------------");

            // 查询名字为 Mars3 的一条数据
            Student mars3 = dao.findByName("Mars3");
            Log.d("Mars", "run: mars.toString(): " + mars.toString());

            Log.i("Mars", "--------------------------");

            // 查询 2 3 4 uid的三条数据
            List<Student> allID = dao.getAllId(new int[]{2, 3, 4});
            Log.d("Mars", "run: allID.toString(): " + allID.toString());

            Log.i("Mars", "--------------------------");

            // 查询student表里面的数据  到  StudentTuple里面去
            StudentTuple record = dao.getRecord();
            Log.d("Derry", "run: record.toString(): " + record.toString());
            
            // 更新
            dao.update(new Student("Mars3", "112", 4))
            
            // 删除
            dao.delete(new Student("Mars3", "112", 4))
        }
    }
}

增、删、改、查的基础使用就是这些,相对我们直接操作 SQLite 要方便很多;

Jetpack Room 结合 MVVM 使用

我们先来看下 MVVM 的标准架构图,可以看到 Room 通过 Repository 连接到 Model 层中的 Room,我们就按照这个架构图来看下如何结合 MVVM 使用;

老三件,Entity、Dao、Database,我们可以直接复用前面基础使用中创建的;Database 我们稍微改造下,不再 Activity 中进行数据库的创建了,我们可以改成单例的模式,以及可以在主线程中创建数据库;

scala 复制代码
@Database(entities = {Student.class}, version = 2, exportSchema = false)
public abstract class AppDatabase  extends RoomDatabase {
    
    /** 单例属性 */
    private static AppDatabase instance;
    
    // 对外暴露唯一实例
    public static synchronized AppDatabase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(context.getApplicationContext(),
                    AppDatabase.class
                    , "MarsDB")
                    // 可以强制在主线程运行数据库操作
                    .allowMainThreadQueries()
                    .build();
        }
        return instance;
    }

    public abstract StudentDao studentDao();
}

支持 LiveData

Dao 中也需要改造下,支持 LiveData,主要是查询操作;

less 复制代码
@Dao
public interface StudentDao {

    @Insert
    void insert(Student... students);

    @Delete
    void delete(Student student);

    @Update
    void update(Student student);

    @Query("select * from Student order by uid")
    LiveData<List<Student>> getAll();

    // 查询一条记录
    @Query("select * from Student where name like:name")
    LiveData<Student> findByName(String name);

    // 数组查询 多个记录
    @Query("select * from Student where uid in(:userIds)")
    LiveData<List<Student>> getAllId(int[] userIds);

    // 就是只查询 name pwd 给 StudentTuple 类接收
    @Query("select name,pwd from Student")
    LiveData<StudentTuple> getRecord();
}

针对查询操作,只需要将返回值改成 LiveData<> 进行包裹,就可以完美支持 LiveData 了;

支持 Repository

我们可以简单的来支持下 Repository 层,商业环境中,它应该是一个独立的模块,支持各个业务的 DAO 和 Net 的操作,内部根据业务判断 dao 和 net 的逻辑;本期我们为了体验 MVVM 的结合,设计的比较简单

scss 复制代码
public class StudentRepository {

    private StudentDao studentDao; // 用户操作 只面向DAO

    public StudentRepository(Context context) {
        AppDatabase database = AppDatabase.getInstance(context);
        studentDao = database.studentDao();
    }

    // 下面代码是:提供一些API给ViewModel使用
    
    // 增
    void insert(Student... students) {
        studentDao.insert(students);
    }

    // 删
    void delete(Student student) {
        studentDao.delete(student);
    }

    // 改
    void update(Student student) {
        studentDao.update(student);
    }

    // 查 关联 LiveData 暴露环节
    LiveData<List<Student>> getAllLiveDataStudent() {
        return studentDao.getAll();
    }
}

结合 ViewModel

接下来我们来声明 ViewModel,由架构图可以看到,ViewModel 操作的是 Repository;

scss 复制代码
public class StudentViewModel extends AndroidViewModel {

    private StudentRepository studentRepository; // 定义仓库 ViewModel 只操作仓库
    private LiveData<List<Student>> liveDataAllStudent; // 触发改变的 LiveData 的数据
    
    public StudentViewModel(Application application) {
        super(application);
        studentRepository = new StudentRepository(application);
        if (liveDataAllStudent == null) {
            liveDataAllStudent = studentRepository.getAllLiveDataStudent(); // 使用 LiveData 关联 Room
        }
    }

    // 增
    void insert(Student... students) {
        studentRepository.insert(students);
    }

    // 删
    void delete(Student student) {
        studentRepository.delete(student);
    }

    // 改
    void update(Student student) {
        studentRepository.update(student);
    }

    // 查 关联 LiveData
    LiveData<List<Student>> getAllLiveDataStudent() {
        if (liveDataAllStudent == null) {
            liveDataAllStudent = studentRepository.getAllLiveDataStudent(); // 使用 LiveData 关联 Room
        }
        return liveDataAllStudent;
    }
}

接下来,我们声明 Activity 来创建 ViewModel

java 复制代码
public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView;
    private StudentViewModel studentViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        // 创建 RV
        recyclerView = findViewById(R.id.recyclerView);
        
        // 创建 ViewModel
        studentViewModel = new ViewModelProvider(getViewModelStore(), new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(StudentViewModel.class);
        // 观察者,如果使用了 DataBinding 以及 BindingAdapter 之后,这个 observe 的逻辑就可以删掉了
        studentViewModel.getAllLiveDataStudent().observe(this, new Observer<List<Student>>() {
            @Override
            public void onChanged(List<Student> students) {
                // 更新UI
                recyclerView.setAdapter(new GoodsAdapter(MainActivity.this, students));
            }
        });


        // 模拟 仓库
        new Thread()  {
            @Override
            public void run() {
                super.run();
                try {
                    Thread.sleep(6000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 默认给数据库 ROOM 增加数据
                for (int i = 0; i < 50; i++) {
                    studentViewModel.insert(new Student("Mars", "123", 1));
                }
            }
        }.start();


        // 模拟仓库 数据库数据被修改了,一旦数据库被修改,那么数据会驱动 UI 发生改变
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 50; i++) {
                    try {
                        Thread.sleep(1000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    // 每隔一秒、更新一次,内部会 setValue,就会回调到 onChange 方法;
                    studentViewModel.update(new Student(6, "Mars" + i, "123", 1));
                }
            }
        }.start();
    }
}

运行之后,可以看到的效果就是,界面每隔 1s 更新一下;

源码篇


本质上是通过 APT 技术,帮我们生成了 Dao 和 Database 的具体实现;

为什么,使用了 LiveData 之后,就能自动的更新 UI 了,我们进入源码看下:

当我们执行编译之后,可以看到 Dao 有了实现,我们点击这个可以进入到实现层;

java 复制代码
public LiveData<List<Student>> getAllLiveDataStudent() {
  final String _sql = "select * from Student order by uid";
  final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 0);
  return new ComputableLiveData<List<Student>>(__db.getQueryExecutor()) {
    private Observer _observer;

    @Override
    protected List<Student> compute() {
      if (_observer == null) {
        _observer = new Observer("Student") {
          @Override
          public void onInvalidated(@NonNull Set<String> tables) {
            // 核心逻辑就在这里 Oberver 监听,Room 会把改变的数据发送过来,收到变化之后会进行刷新实现
            invalidate();
          }
        };
        __db.getInvalidationTracker().addWeakObserver(_observer);
      }
      final Cursor _cursor = __db.query(_statement);
      try {
        final List<Student> _result = new ArrayList<Student>(_cursor.getCount());
        while(_cursor.moveToNext()) {
          final Student _item;
          _item = __entityCursorConverter_comExampleRoomdemo02Student(_cursor);
          _result.add(_item);
        }
        return _result;
      } finally {
        _cursor.close();
      }
    }

    @Override
    protected void finalize() {
      _statement.release();
    }
  }.getLiveData();
}

核心逻辑就在这里 Oberver 监听,Room 会把改变的数据发送过来,收到变化之后会进行刷新实现,我们进入这个 invalidate 看下:

java 复制代码
final Runnable mInvalidationRunnable = new Runnable() {
    @MainThread
    @Override
    public void run() {
        boolean isActive = mLiveData.hasActiveObservers();
        if (mInvalid.compareAndSet(false, true)) {
            if (isActive) {
                // 核心逻辑,最终执行了 mRefreshRunnable 这个 runnable
                mExecutor.execute(mRefreshRunnable);
            }
        }
    }
};


public void invalidate() {
    ArchTaskExecutor.getInstance().executeOnMainThread(mInvalidationRunnable);
}

核心逻辑,最终执行了 mRefreshRunnable 这个 runnable,我们进入这个 Runnable 看下:

java 复制代码
final Runnable mRefreshRunnable = new Runnable() {
    @WorkerThread
    @Override
    public void run() {
        boolean computed;
        do {
            computed = false;
            if (mComputing.compareAndSet(false, true)) {
                try {
                    T value = null;
                    while (mInvalid.compareAndSet(true, false)) {
                        computed = true;
                        value = compute();
                    }
                    if (computed) {
                        // 核心逻辑,直接调用了 postValue 
                        mLiveData.postValue(value);
                    }
                } finally {
                    mComputing.set(false);
                }
            }
        } while (computed && mInvalid.get());
    }
};

最终调用了 LiveData 的 postValue 发送数据;

升级篇


Room 的数据库升级相对比较复杂;

暴力升级

scss 复制代码
public static synchronized AppDatabase getInstance(Context context) {
        if (instance == null) {
            instance = Room.databaseBuilder(context.getApplicationContext(),
                    AppDatabase.class
                    , "MarsDB")
                    // 可以强制在主线程运行数据库操作
                    .allowMainThreadQueries()
                    // 暴力升级,强制执行(数据会丢失)(慎用)
                    .fallbackToDestructiveMigration()
                    .build();
        }
        return instance;
    }

fallbackToDestructiveMigration() 会强制升级数据库,但是可能会导致数据丢失,谨慎使用吧;

稳定升级

Room 提供了稳定升级的接口 .addMigrations()

这个接口需要我们实现 Migration,并实现具体的 migrate 逻辑;

java 复制代码
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
        @Override
        public void migrate(SupportSQLiteDatabase database) {
            // 在这里用 SQL 脚本完成数据的变化
            database.execSQL("alter table student add column flag integer not null default 1");
        }
    };

然后将 MIGRATION_1_2 传入到 addMigrations() 这个方法中;

数据库一般是不建议降级的,这块大家在使用过程中也尽量不要降级处理吧;

好了,room 就先写到这里吧~

欢迎三连


来都来了,点个关注,点个赞吧,你的支持是我最大的动力~

相关推荐
刘龙超2 天前
如何应对 Android 面试官 -> 玩转 Jetpack ViewModel
android jetpack
alexhilton3 天前
为什么你的App总是忘记所有事情
android·kotlin·android jetpack
刘龙超3 天前
如何应对 Android 面试官 -> 玩转 Jetpack DataBinding
android jetpack
雨白4 天前
Jetpack系列(四):精通WorkManager,让后台任务不再失控
android·android jetpack
刘龙超5 天前
如何应对 Android 面试官 -> 玩转 JetPack ViewBinding
android jetpack
顾林海5 天前
ViewModel 销毁时机详解
android·面试·android jetpack
雨白5 天前
Jetpack系列(三):Room数据库——从增删改查到数据库平滑升级
android·android jetpack
雨白6 天前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
刘龙超8 天前
如何应对 Android 面试官 -> 玩转 JetPack LiveData
android jetpack