Explore Packman Part 3 – Realm Kotlin

Realm 在 2022 年出推出了全新的 Realm Kotlin SDK,相比 Java SDK,Kotlin SDK 全部由 Kotlin 语言写就,代码库也完全不同。所以理所应当的,Realm Kotlin SDK 和 Kotlin 语言生态中其他功能(协程、挂起方法等)的协作也更加紧密。本文也主要介绍 Realm Kotlin SDK 和 Java SDK 的异同,如果你没有使用过 Realm Java SDK,可以参考《Realm 在 Android 上的应用》

架构的不同点

默认冻结

其他 SDK(比如 Java SDK、Swift SDK 等)提供了实时的对象(object)、查询和 Realm 实例,在底层数据变化时,它们也会自动更新,而 Kotlin SDK 只在写事务中提供实时变化的接口,其他情况下都不再提供。

当我们需要修改对象时,它们必须是实时变化的。我们可以在事务中使用 mutableRealm.findLatest() 方法将一个冻结对象转换为实时对象。实时对象只能在 write 或者 writeBlocking 闭包开启的写事务中被访问。

写事务完成后,写闭包返回的对象重新变为冻结对象,不再自动更新。

val sample: Sample? = realm.query<Sample>()
    .first().find()

// 同步删除一个对象
realm.writeBlocking {
    if (sample != null) {
        findLatest(sample)
        ?.also { 
            // it 为实时对象
            delete(it) 
        }
    }
}

// 异步删除一个查询结果
GlobalScope.launch {
    realm.write {
        query<Sample>()
            .first()
            .find()
            ?.also { 
                // it 为实时对象
                delete(it) 
            }
    }
}

线程安全

Realm 类终于不再和线程绑定,所有的 Realm 实例、对象、查询结果和集合都可以在线程间传递。之前使用 Realm Java SDK 时,因为 Realm 和线程绑定,所以 DiffUtil 是不能使用的。

单例

得益于上面提到的线程安全特性,我们可以多个线程共享一个 Realm 实例,不再需要使用 Realm.close() 方法显式地处理 Realm 生命周期。

跨平台

Realm Java SDK 支持 Android 生态的全部平台(Android、Wear OS 等等),而 Kotlin SDK 通过 Kotlin Multiplatform 除了支持 Android 平台外,还支持了 iOS、macOS 和 JVM。详细列表参考 https://www.mongodb.com/docs/realm/sdk/kotlin/install/#supported-target-environments

架构的相同点

懒加载

Realm 对象默认仍然是懒加载的,这意味着我们可以查询很大数量的对象集合,而不需要从磁盘中读取海量数据,同时也意味着首次访问对象字段时数据一定是最新的。

写事务

和其他 Realm SDK 类似,写事务也会隐式地将磁盘数据更新为最新版本的数据(文件 IO 与内存版本)。

模型类

使用 Realm Kotlin SDK 定义模型类更加简单:

  • Kotlin SDK 不再要求模型类必须是 open
  • Java SDK 使用 KAPT(Kotlin Annotation Processing Tool) 派生代理类(负责与持久化进行交互),而 Kotlin SDK 使用 RealmObject 接口作为 Gradle 插件的标记;
  • Java SDK 要求模型类继承自 RealmObject ,而 Kotlin SDK 要求模型类继承自 RealmObject 接口
// Java SDK
open class ExpenseInfo : RealmObject() {
    @PrimaryKey
    var expenseId: String = UUID.randomUUID().toString()
    var expenseName: String = ""
    var expenseValue: Int = 0
}

// Kotlin SDK
class ExpenseInfo : RealmObject {
    @PrimaryKey
    var expenseId: String = UUID.randomUUID().toString()
    var expenseName: String = ""
    var expenseValue: Int = 0
}

查询

  • Realm Java SDK 提供了两种查询方法:
    • 查询引擎:
    • val tasksQuery = realm.where(ProjectTask::class.java) .greaterThan("priority", 5) .count()
    • Realm 查询语言(Realm Query Language,在 10.4.0 版本新增):
    • val query = realm.where(Student::class.java) .rawPredicate("[email protected] > 0 SORT(year ASCENDING) DISTINCT(name) LIMIT(5)") .findAll()
    • 这种基于字符串的查询语言,约束了从 Realm 中检索数据时的搜索条件。Realm 查询语言可以使用在 Realm 模型类中定义的类名和字段名(通过 @字段名 的方式)。这两种查询方法可以组合使用。
  • Realm Kotlin SDK 引入了一种受苹果的 NSPredicate 启发的查询语言:
  • import io.realm.kotlin.ext.query val filteredByDog = realm.query<Person>("dog.age > $0 AND dog.name BEGINSWITH $1", 7, "Fi") .find()

响应数据变化

Realm Java 支持三种类型的通知:

  • Realm 通知:当 Realm 提交了一个写事务时触发;
  • val realm = Realm.getDefaultInstance() val realmListener = RealmChangeListener { // 通知 } realm.addChangeListener(realmListener)
  • 集合通知:当集合内任何 Realm 对象发生变化时触发,包括新增、更新和删除;
  • val dogs = realm.where(Dog::class.java) .findAll() val changeListener = OrderedRealmCollectionChangeListener { collection: RealmResults<Dog>?, changeSet: OrderedCollectionChangeSet -> val deletions = changeSet.deletionRanges for (i in deletions.indices.reversed()) { val range = deletions[i] // 删除 } val insertions = changeSet.insertionRanges for (range in insertions) { // 新增 } val modifications = changeSet.changeRanges for (range in modifications) { // 更新 } } dogs.addChangeListener(changeListener)
  • 对象通知:当 Realm 对象发生变化时触发,包括更新和删除。
  • val dog = realm.where(Dog::class.java) .equalTo("name", "poppy") .findFirst() val listener = RealmObjectChangeListener { changedDog: Dog?, changeSet: ObjectChangeSet? -> if (changeSet!!.isDeleted) { // 删除 } else { for (fieldName in changeSet.changedFields) { // 更新 } } } dog.addChangeListener(listener)

Realm Kotlin 也支持三种类型的通知:

  • 查询
  • val characters = realm.query(Character::class) val job = CoroutineScope(Dispatchers.Default).launch { val charactersFlow = characters.asFlow() val subscription = charactersFlow.collect { changes: ResultsChange<Character> -> when (changes) { // 包括 更新/新增/删除 操作 is UpdatedResults -> { changes.insertions // 新增对象的索引 changes.insertionRanges // 新增对象的范围 changes.changes // 更新对象的索引 changes.changeRanges // 更新对象的范围 changes.deletions // 删除对象的索引 changes.deletionRanges // 新增对象的范围 changes.list // 对象的完整集合 } else -> { // 忽略 } } } }
  • Realm 对象
  • val frodo = realm.query(Character::class, "name == 'Frodo'") .first() val job = CoroutineScope(Dispatchers.Default).launch { val frodoFlow = frodo.asFlow() frodoFlow.collect { changes: SingleQueryChange<Character> -> when (changes) { // 对象有更新 is UpdatedObject -> { changes.changedFields // 更改的字段 changes.obj // 最新状态的对象 changes.isFieldChanged("name") // 检查特定字段的值是否发生变化 } // 对象被删除 is DeletedObject -> { changes.obj // 被删除后返回为 null -- 永远是最新状态 } // RealmObject 或 EmbeddedRealmObject flow 初始化事件 is InitialObject -> { // 包含初始状态下对象的引用 changes.obj } // 查询结果不包含任何元素时 is PendingObject -> { changes.obj } } } }
  • Realm 集合:集合类型包括 RealmListRealmSet, 或 RealmMap
  • // 对想要监听的对象的查询 val fellowshipOfTheRing = realm.query(Fellowship::class, "name == 'Fellowship of the Ring'") .first() .find()!! val members = fellowshipOfTheRing.members val job = CoroutineScope(Dispatchers.Default).launch { val membersFlow = members.asFlow() membersFlow.collect { changes: ListChange<Character> -> when (changes) { is UpdatedList -> { changes.insertions // 新增对象的索引 changes.insertionRanges // 新增对象的范围 changes.changes // 更新对象的索引 changes.changeRanges // 更新对象的范围 changes.deletions // 删除对象的索引 changes.deletionRanges // 新增对象的范围 changes.list // 对象的完整集合 } // 列表被删除 is DeletedList -> { } // RealmList flow 初始化事件,包含了对初始状态的列表的引用 is InitialList -&gt; { changes.list } } }}

两者对比,Kotlin SDK 去除了 Realm 提交写事务时的通知。

级联删除

Realm Java SDK 在 10.0 版本支持了级联删除(终于!),实现方式为内嵌对象(Embedded Object)。

定义内嵌对象

// 定义内嵌对象(不能包含主键) 
class Address() : EmbeddedRealmObject {
    var street: String? = null
    var city: String? = null
    var state: String? = null
    var postalCode: String? = null
}

// 定义包含内嵌对象的对象
class Contact : RealmObject {
    @PrimaryKey
    var _id: ObjectId = ObjectId()
    var name: String = ""
    // 内嵌一个对象(必须是可空的)
    var address: Address? = null
        // 内嵌一组对象(必须是非空的)
    var addresses: RealmList<Address> = realmListOf()
}

// 内嵌类及包含内嵌类的类都必须在 schema 中声明
val config = RealmConfiguration.Builder(
    setOf(Contact::class, Address::class)
)
    .build()
val realm = Realm.open(config)

创建、更新、查询内嵌对象

内嵌对象的创建、更新和查询和普通 Realm 对象类似,这里不再赘述。只是需要注意的是,内嵌对象提供了额外的 EmbeddedRealmObject.parent() 方法,可以很方便地获取其对应的父级对象:

// 直接查询内嵌对象
val queryAddress: Address = realm.query<Address>("state == 'FL'")
        .find()
        .first()

// 获取内嵌对象的父级对象
val getParent: Contact = queryAddress.parent()

// 通过父级对象查询内嵌对象
val queryContactAddresses: RealmResults<Contact> =
    realm.query<Contact>("address.state == 'NY'")
        .sort("name")
        .find()

删除内嵌对象

// 直接删除内嵌对象
realm.write {
    val addressToDelete: Address = this.query<Address>("street == '123 Fake St'")
                .find()
                .first()
    // 删除内嵌对象(将父级对象的字段置空)
    delete(addressToDelete)
}

// 通过父级对象删除内嵌对象
realm.write {
    val propertyToClear: Contact = this.query<Contact>("name == 'Nick Riviera'")
                .find()
                .first()
    // 清除父级对象的字段的值
    propertyToClear.address = null
}

// 删除夫级对象(删除所有内嵌对象)
realm.write {
    val contactToDelete: Contact = this.query<Contact>("name == 'Nick Riviera'")
                .find()
                .first()
    delete(contactToDelete)
}

参考资料