変更に対応する - React Kotlin SDK
項目一覧
最近のアプリは、データが変更されたときに、その変更がどこで行われたかにかかわらずReactを実行できるようになりました。 ユーザーが新しい項目をリストに追加するときに、UI を更新したり、通知を表示したり、メッセージをログに記録したりすることができます。 そのアイテムがアップデートされたら、その可視化状態を変更したり、ネットワークリクエストを起動したりする必要があるかもしれません。 最後に、誰かがアイテムを削除した場合は、UI からそれを削除する必要がある可能性があります。 Realmの通知システムを使用すると、変更の原因となった書込み (write) とはReactに、データ内の変更を監視し、対応することができます。
Kotlin SDK の固定アーキテクチャでは、通知の重要性がさらに高まります。 Kotlin SDK には自動的に更新されるライブ オブジェクトがないため、通知を使用して UI とデータ レイヤーを同期させます。
次のイベントの変更をサブスクライブできます。
SDK は、最大 4 レイヤーの深度にネストされたオブジェクトの通知のみを提供します。 より深くReactされたオブジェクトの変更に対応する必要がある場合は、 キー パス変更リスナー を登録します。 詳細については、このページの「 キー パス変更リスナーの登録」を参照してください。
ユーザー認証状態の変更にReactすることもできます。 詳細については、「認証変更の監視 」を参照してください。
例
このページの例について
このページの例では、2 つの Realm オブジェクトタイプ、 Character
とFellowship
を使用します。
class Character(): RealmObject { var name: String = "" var species: String = "" var age: Int = 0 constructor(name: String, species: String, age: Int) : this() { this.name = name this.species = species this.age = age } } class Fellowship() : RealmObject { var name: String = "" var members: RealmList<Character> = realmListOf() constructor(name: String, members: RealmList<Character>) : this() { this.name = name this.members = members } }
例には、次のサンプル データがあります。
val config = RealmConfiguration.Builder(setOf(Fellowship::class, Character::class)) .name(realmName) .build() val realm = Realm.open(config) val frodo = Character("Frodo", "Hobbit", 51) val samwise = Character("Samwise", "Hobbit", 39) val aragorn = Character("Aragorn", "Dúnedain", 87) val legolas = Character("Legolas", "Elf", 2931) val gimli = Character("Gimli", "Dwarf", 140) val gollum = Character("Gollum", "Hobbit", 589) val fellowshipOfTheRing = Fellowship( "Fellowship of the Ring", realmListOf(frodo, samwise, aragorn, legolas, gimli)) realm.writeBlocking{ this.copyToRealm(fellowshipOfTheRing) this.copyToRealm(gollum) // not in fellowship } realm.close()
クエリ変更リスナーの登録
Realm 内の任意のクエリに通知ハンドラーを登録できます。 まず、 asFlow()を使用してクエリから Kotlin Flow を作成します。 次に、 collect()
メソッドを使用して、そのフローに関するイベントを処理します。 タイプUpdatedResults
のイベントは、次のプロパティを使用してクエリに一致するオブジェクトに対するすべての変更を記録します。
プロパティ | タイプ | 説明 |
insertions | IntArray | このバージョンで追加された新しいコレクション内のインデックス。 |
insertionRanges | 配列未満<ListChangeSet.Range> | このバージョンで追加された新しいコレクション内のインデックスの範囲。 |
changes | IntArray | このバージョンで変更された新しいコレクション内のオブジェクトのインデックス。 |
changeRanges | 配列未満<ListChangeSet.Range> | このバージョンで変更された新しいコレクション内のインデックスの範囲。 |
deletions | IntArray | このバージョンから削除された、コレクションの以前のバージョン内のインデックス。 |
deletionRanges | 配列未満<ListChangeSet.Range> | このバージョンから削除された、コレクションの以前のバージョン内のインデックスの範囲。 |
list | RealmResults< RealmObject としての T<T as RealmObject> | 変更が監視されている結果コレクション。 |
// Listen for changes on whole collection val characters = realm.query(Character::class) // flow.collect() is blocking -- run it in a background context val job = CoroutineScope(Dispatchers.Default).launch { // create a Flow from that collection, then add a listener to the Flow val charactersFlow = characters.asFlow() val subscription = charactersFlow.collect { changes: ResultsChange<Character> -> when (changes) { // UpdatedResults means this change represents an update/insert/delete operation is UpdatedResults -> { changes.insertions // indexes of inserted objects changes.insertionRanges // ranges of inserted objects changes.changes // indexes of modified objects changes.changeRanges // ranges of modified objects changes.deletions // indexes of deleted objects changes.deletionRanges // ranges of deleted objects changes.list // the full collection of objects } else -> { // types other than UpdatedResults are not changes -- ignore them } } } } // Listen for changes on RealmResults val hobbits = realm.query(Character::class, "species == 'Hobbit'") val hobbitJob = CoroutineScope(Dispatchers.Default).launch { val hobbitsFlow = hobbits.asFlow() val hobbitsSubscription = hobbitsFlow.collect { changes: ResultsChange<Character> -> // ... all the same data as above } }
RealmObject 変更リスナーの登録
Realm 内の特定のオブジェクトに通知ハンドラーを登録できます。 Realm は、オブジェクトのプロパティのいずれかが変更されたときにハンドラーに通知します。 単一オブジェクトの変更リスナーを登録するには、 realm.query.first()
を使用してRealmDoubleQueryを取得します。 asFlow()を使用して、そのクエリからフローを生成します。 ハンドラーは、次のサブタイプを使用してオブジェクトの変更を伝達するSingleQueryChange
オブジェクトを受け取ります。
サブタイプ | プロパティ | ノート |
UpdatedObject | ChangedFields 、 obj | フィールド名を isFieldChanged() に渡して、そのフィールドが変更されたかどうかを確認します。 |
DeletedObject | obj | objectは常にオブジェクトの最新バージョンを反映するため、このサブタイプでは常に null 値が返されます。 |
// query for the specific object you intend to listen to val frodo = realm.query(Character::class, "name == 'Frodo'").first() // flow.collect() is blocking -- run it in a background context val job = CoroutineScope(Dispatchers.Default).launch { val frodoFlow = frodo.asFlow() frodoFlow.collect { changes: SingleQueryChange<Character> -> when (changes) { is UpdatedObject -> { changes.changedFields // the changed properties changes.obj // the object in its newest state changes.isFieldChanged("name") // check if a specific field changed in value } is DeletedObject -> { // if the object has been deleted changes.obj // returns null for deleted objects -- always reflects newest state } is InitialObject -> { // Initial event observed on a RealmObject or EmbeddedRealmObject flow. // It contains a reference to the starting object state. changes.obj } is PendingObject -> { // Describes the initial state where a query result does not contain any elements. changes.obj } } } }
コレクション変更リスナーの登録
通知ハンドラーは、 RealmList
、 RealmSet
、またはRealmMap
で登録できます。 Realm は、コレクション項目のいずれかが変更されると、ハンドラーに通知します。 まず、 asFlow()
を使用して コレクションから Kotlin フローを作成します。 次に、 collect()
メソッドを使用して、そのフローに関するイベントを処理します。 ListChange
、 SetChange
、またはMapChange
タイプのイベントは、コレクションに対するすべての変更を記録します。
RealmList
通知にはListchangeインターフェースが実装されています。これにより、RealmList コレクションで発生する可能性のある変更を記述します。 これらの状態はサブクラスによって表されます。
InitialList
UpdatedList
DeletedList
ListchangeSetは、リスト内で発生する可能性のある変更をモデル化するインターフェースであり、プロパティには次のものが含まれます。
プロパティ | タイプ | 説明 |
insertions | IntArray | このバージョンで追加された新しいコレクション内のインデックス。 |
insertionRanges | 配列未満<ListChangeSet.Range> | このバージョンで追加された新しいコレクション内のインデックスの範囲。 |
changes | IntArray | このバージョンで変更された新しいコレクション内のオブジェクトのインデックス。 |
changeRanges | 配列未満<ListChangeSet.Range> | このバージョンで変更された新しいコレクション内のインデックスの範囲。 |
deletions | IntArray | このバージョンから削除された、コレクションの以前のバージョン内のインデックス。 |
deletionRanges | 配列未満<ListChangeSet.Range> | このバージョンから削除された、コレクションの以前のバージョン内のインデックスの範囲。 |
list | RealmResults< RealmObject としての T<T as RealmObject> | 変更が監視されている結果コレクション。 これは、イベントタイプが UpdatedList の場合に提供されます。 |
RealmSet
通知にはSetchangeインターフェースが実装されており、RealmSet コレクションで発生する可能性のある変更を記述します。 これらの状態はサブクラスによって表されます。
InitialSet
UpdatedSet
DeletedSet
SetchangeSetは、セット内で発生する可能性のある変更をモデル化するインターフェースであり、プロパティには次のものが含まれます。
プロパティ | タイプ | 説明 |
deletions | Int | このバージョンの コレクションで削除されたエントリの数。 |
insertions | Int | このバージョンの コレクションに挿入されたエントリの数。 |
set | RealmSet<T> | 変更が監視されているセット。 これは、イベントタイプが UpdatedSet の場合に提供されます。 |
RealmMap
通知には、 RealmMap コレクションで発生する可能性のある変更を記述するMapCheckインターフェースが実装されています。 これらの状態はサブクラスによって表されます。
InitialMap
UpdatedMap
DeletedMap
Map ChangeSetは、マップで発生する可能性のある変更をモデル化するインターフェースであり、プロパティには次のものが含まれます。
プロパティ | タイプ | 説明 |
deletions | Array[K]<K> | このバージョンのマップで削除されたキー。 |
insertions | Array[K]<K> | このバージョンのマップに挿入されたキー。 |
changes | Array[K]<K> | このバージョンのコレクションで値が変更されたキー。 |
set | RealmMap<K,V> | 変更が監視されているマップ。 これは、イベントタイプが UpdatedMap の場合に提供されます。 |
// query for the specific object you intend to listen to val fellowshipOfTheRing = realm.query(Fellowship::class, "name == 'Fellowship of the Ring'").first().find()!! val members = fellowshipOfTheRing.members // flow.collect() is blocking -- run it in a background context val job = CoroutineScope(Dispatchers.Default).launch { val membersFlow = members.asFlow() membersFlow.collect { changes: ListChange<Character> -> when (changes) { is UpdatedList -> { changes.insertions // indexes of inserted objects changes.insertionRanges // ranges of inserted objects changes.changes // indexes of modified objects changes.changeRanges // ranges of modified objects changes.deletions // indexes of deleted objects changes.deletionRanges // ranges of deleted objects changes.list // the full collection of objects } is DeletedList -> { // if the list was deleted } is InitialList -> { // Initial event observed on a RealmList flow. It contains a reference // to the starting list state. changes.list } } } }
キー パス変更リスナーの登録
バージョン 1.13.0 の新機能。
通知ハンドラーを登録するときに、string プロパティ名のオプションのリストを渡して、監視するキー パスを指定できます。
キーパスを指定すると、それらのキーパスに対する変更のみによって通知ブロックがtriggerされます。 その他の変更では通知ブロックはtriggerされません。
次の例では、 age
プロパティのキー パス変更リスナーを登録しています。
runBlocking { // Query for the specific object you intend to listen to. val frodoQuery = realm.query(Character::class, "name == 'Frodo'").first() val observer = async { val frodoFlow = frodoQuery.asFlow(listOf("age")) frodoFlow.collect { changes: SingleQueryChange<Character> -> // Change listener stuff in here. } } // Changing a property whose key path you're not observing does not trigger a notification. realm.writeBlocking { findLatest(frodoObject)!!.species = "Ring Bearer" } // Changing a property whose key path you are observing triggers a notification. realm.writeBlocking { findLatest(frodoObject)!!.age = 52 } // For this example, we send the object change to a Channel where we can verify the // changes we expect. In your application code, you might use the notification to // update the UI or take some other action based on your business logic. channel.receiveOrFail().let { objChange -> assertIs<UpdatedObject<*>>(objChange) assertEquals(1, objChange.changedFields.size) // Because we are observing only the `age` property, the change to // the `species` property does not trigger a notification. // The first notification we receive is a change to the `age` property. assertEquals("age", objChange.changedFields.first()) } observer.cancel() channel.close() }
注意
個別のキー パスでフィルタリングされる同じオブジェクト上の複数の通知トークンは、排他的にフィルタリングされません。 1 つの通知トークンに対して 1 つのキーパスの変更が満たされた場合、そのオブジェクトのすべての通知トークン ブロックが実行されます。
ネストされたキー パスの監視
ドット表記 を使用して、ネストされたキー パスを確認できます。 デフォルトでは、SDK は最大 4 レイヤーにネストされたオブジェクトの通知のみを報告します。 より深くネストされたオブジェクトの変更を監視する必要がある場合は、特定のキー パスを監視します。
次の例では、ネストされたプロパティmembers.age
の更新を監視しています。 ネストされたプロパティに対する変更を監視しているにもかかわらず、SDK は最上位のmembers
プロパティへの変更を報告していることに注意してください。
runBlocking { // Query for the specific object you intend to listen to. val fellowshipQuery = realm.query(Fellowship::class).first() val observer = async { val fellowshipFlow = fellowshipQuery.asFlow(listOf("members.age")) fellowshipFlow.collect { changes: SingleQueryChange<Fellowship> -> // Change listener stuff in here. } } // Changing a property whose nested key path you are observing triggers a notification. val fellowship = fellowshipQuery.find()!! realm.writeBlocking { findLatest(fellowship)!!.members[0].age = 52 } // For this example, we send the object change to a Channel where we can verify the // changes we expect. In your application code, you might use the notification to // update the UI or take some other action based on your business logic. channel.receiveOrFail().let { objChange -> assertIs<UpdatedObject<*>>(objChange) assertEquals(1, objChange.changedFields.size) // While you can watch for updates to a nested property, the notification // only reports the change on the top-level property. In this case, there // was a change to one of the elements in the `members` property, so `members` // is what the notification reports - not `age`. assertEquals("members", objChange.changedFields.first()) } observer.cancel() channel.close() }
ワイルドカードによるキーパスの監視
ワイルドカード( *
)を使用すると、ワイルドカードのレベルですべてのキーパスに対する変更を確認できます。
次の例では、ワイルドカード監視を使用して、 members
プロパティ内の 1 つのレベルのネストされたプロパティに対する変更を確認します。 この例で使用されているモデルに基づいて、この変更リスナーは任意のノードのage
、 species
、またはname
プロパティの変更を報告します。 ネストされたプロパティに対する変更を監視しているにもかかわらず、SDK は最上位のmembers
プロパティへの変更を報告していることに注意してください。
runBlocking { // Query for the specific object you intend to listen to. val fellowshipQuery = realm.query(Fellowship::class).first() val observer = async { // Use a wildcard to observe changes to any key path at the level of the wildcard. val fellowshipFlow = fellowshipQuery.asFlow(listOf("members.*")) fellowshipFlow.collect { changes: SingleQueryChange<Fellowship> -> // Change listener stuff in here. } } // Changing any property at the level of the key path wild card triggers a notification. val fellowship = fellowshipQuery.find()!! realm.writeBlocking { findLatest(fellowship)!!.members[0].age = 52 } // For this example, we send the object change to a Channel where we can verify the // changes we expect. In your application code, you might use the notification to // update the UI or take some other action based on your business logic. channel.receiveOrFail().let { objChange -> assertIs<UpdatedObject<*>>(objChange) assertEquals(1, objChange.changedFields.size) // While you can watch for updates to a nested property, the notification // only reports the change on the top-level property. In this case, there // was a change to one of the elements in the `members` property, so `members` // is what the notification reports - not `age`. assertEquals("members", objChange.changedFields.first()) } observer.cancel() channel.close() }
変更リスナーのサブスクライブを解除します
監視対象のデータへのアップデートに関する通知を受信したくない場合は、変更リスナーからサブスクライブを解除します。変更リスナーのサブスクライブを解除 するには、囲みているコルーチンをキャンセルします。
// query for the specific object you intend to listen to val fellowshipOfTheRing = realm.query(Fellowship::class, "name == 'Fellowship of the Ring'").first().find()!! val members = fellowshipOfTheRing.members // flow.collect() is blocking -- run it in a background context val job = CoroutineScope(Dispatchers.Default).launch { val membersFlow = members.asFlow() membersFlow.collect { changes: ListChange<Character> -> // change listener stuff in here } } job.cancel() // cancel the coroutine containing the listener
通知制限の変更
4 レベルよりも深いネストされたドキュメントの変更では、変更通知はtriggerされません。
5 レベル以上の変更をリッスンする必要があるデータ構造がある場合は、次の回避策があります。
スキーマをリファクタリングしてネストを減らします。
ユーザーがデータを手動で更新できるようにするには、「push-to-refresh」のようなものを追加します。