如何应对 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 就先写到这里吧~

欢迎三连


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

相关推荐
alexhilton4 天前
Kotlin互斥锁(Mutex):协程的线程安全守护神
android·kotlin·android jetpack
是六一啊i5 天前
Compose 在Row、Column上使用focusRestorer修饰符失效原因
android jetpack
用户060905255226 天前
Compose 主题 MaterialTheme
android jetpack
用户060905255226 天前
Compose 简介和基础使用
android jetpack
用户060905255226 天前
Compose 重组优化
android jetpack
行墨6 天前
Jetpack Compose 深入浅出(一)——预览 @Preview
android jetpack
alexhilton8 天前
突破速度障碍:非阻塞启动画面如何将Android 应用启动时间缩短90%
android·kotlin·android jetpack
Pika9 天前
深入浅出 Compose 测量机制
android·android jetpack·composer
fundroid10 天前
掌握 Compose 性能优化三步法
android·android jetpack