Manage Sync Subscriptions - Kotlin SDK
On this page
- Prerequisites
- Subscriptions Overview
- Subscribe to Object Types
- Initial Subscriptions
- Manage Subscriptions in Your Client App
- About the Examples on This Page
- Subscribe to Queries
- Subscribe to a Query
- Update a Query Subscription
- Wait for a Query Subscription to Sync
- Manually Manage Subscriptions
- Add a Subscription
- Wait for Subscription Changes to Sync
- Subscription Set State
- Update Subscriptions with a New Query
- Remove Subscriptions
- Flexible Sync RQL Requirements and Limitations
- Indexed Queryable Fields Subscription Requirements
- Unsupported Query Operators in Flexible Sync
- List Queries
- Embedded or Linked Objects
- Performance Considerations
- API Efficiency
- Group Updates for Improved Performance
uses subscriptions and permissions to determine which data to sync with your App. This page details how to manage subscriptions for Flexible Sync.
For more information, refer to Flexible Sync in the App Services documentation.
Prerequisites
Before you can work with Flexible Sync subscriptions, you must set up your App to use Device Sync and configure Flexible Sync on the backend.
To do this, complete the steps outlined in the Add Device Sync to Your App procedure.
Subscriptions Overview
When you configure Flexible Sync on the backend, you specify which fields your client application can query using subscriptions.
Each subscription corresponds to a query on queryable fields for a specific object type. See Queryable Fields in the App Services documentation for more information.
For each query subscription, Realm looks for data matching the query. Data matching the subscription, where the user has the appropriate permissions, syncs between clients and the backend application.
You can construct queries with Realm Query Language.
Important
Flexible Sync does not support all the operators available in Realm Query Language. See Flexible Sync RQL Limitations for details.
Subscribe to Object Types
Subscription sets are based on object type. You might have multiple subscriptions if you have many types of Realm objects. You can also have multiple subscriptions on the same object type.
However, if you use linked objects, asymmetric objects, or geospatial data in your app, refer the following sections for additional information:
Linked Objects
If your app uses linked objects, you must add both the object itself and its linked object to the subscription set to see the actual linked object.
When your subscription results contain an object with a property that links to an object not contained in the results, the link appears to be null. There is no way to distinguish whether that property's value is null or whether the object it links to exists but is out of view of the query subscription.
Asymmetric Objects
If your app uses Data Ingest to unidirectionally sync asymmetric objects, you cannot create subscriptions for those objects. If your app contains asymmetric objects and non-asymmetric objects in the same realm, you can add Flexible Sync subscription queries for the non-asymmetric objects.
Geospatial Data
Changed in version 1.13.0: Geospatial data supported in Atlas Device Sync
In Kotlin SDK version 1.13.0 and later, you can create subscriptions to geospatial queries. If you try to subscribe to a geospatial query with an older version of the SDK, you will receive a server error with a compensating write.
For more information, refer to Query Geospatial Data.
Initial Subscriptions
You must have at least one subscription before you can read from or write to the realm.
Manage Subscriptions in Your Client App
In the client application, you add, update, and remove subscriptions to specific queries on the queryable fields. This determines which data syncs to the client device.
You can:
Add subscriptions with an optional subscription name:
In Kotlin SDK version 1.10.0 and later, you can use
.subscribe()
to subscribe to aRealmQuery
or aRealmResults
. This automatically adds the subscription to the subscription set.Manually add a subscription to the subscription set with the
subscriptions
API. Use this API if you need more control over subscriptions for performance optimization or business logic reasons. See the Performance Considerations section for more information.
React to subscription state
Update subscriptions with new queries
Remove individual subscriptions or all subscriptions for an object type
About the Examples on This Page
The examples on this page use a data set for a task list app.
The two Realm object types are Team
and Task
:
class Task : RealmObject { var _id: ObjectId = ObjectId() var taskName: String = "" var assignee: String? = null var completed: Boolean = false var progressMinutes: Int = 0 var dueDate: RealmInstant? = null } class Team : RealmObject { var _id: ObjectId = ObjectId() var teamName: String = "" var tasks: RealmList<Task>? = realmListOf() var members: RealmList<String> = realmListOf() }
The examples on this page also assume you have an authorized user and a Flexible Sync SyncConfiguration():
// Login with authorized user and define a Flexible Sync SyncConfiguration val app = App.create(YOUR_APP_ID) val user = app.login(credentials) val flexSyncConfig = SyncConfiguration.Builder(user, setOf(Task::class, Team::class)) .initialSubscriptions { // Define the initial subscription set for the realm ... } .build() // Open the synced realm and manage subscriptions val realm = Realm.open(flexSyncConfig) Log.v("Successfully opened realm: ${realm.configuration}")
Subscribe to Queries
New in version 1.10.0.
To simplify subscription management, Realm Kotlin SDK version 1.10.0
adds an experimental
.subscribe()
API to subscribe to a RealmQuery
or RealmResults
set. This API abstracts
away the details of manually adding and removing subscriptions through subscription sets.
You can:
Automatically subscribe to a query with an optional name
Update a named subscription with a new query
If you need more control over subscriptions for performance optimization or
business logic reasons, you can
manually manage the subscription set
using the subscriptions
API. See the
Performance Considerations
section for more information.
Subscribe to a Query
You can .subscribe()
to a query to create a subscription
for objects matching a specific query:
// Subscribe to a specific query val realmResults = realm.query<Task>("progressMinutes >= $0", 60) .subscribe() // Subscribe to all objects of a specific type val realmQuery = realm.query<Team>() realmQuery.subscribe()
This creates an unnamed subscription and adds it to the
MutableSubscriptionSet
, instead of requiring you to
manually add the subscription to the
subscription set.
Subscribe to a Query with a Name
If your app works with multiple subscriptions or if you want to update a subscription, you may want to add a name when you subscribe to a query. You can later use this name to update the subscription's query or remove the query by name.
To add a name to the subscription, pass a string when you call .subscribe()
:
// Add a subscription named "team_developer_education" val results = realm.query<Team>("teamName == $0", "Developer Education") .subscribe("team_developer_education")
Update a Query Subscription
You can update a named query subscription with a new query
by setting updateExisting
to true
:
// Create a subscription named "bob_smith_teams" val results = realm.query<Team>("$0 IN members", "Bob Smith") .subscribe("bob_smith_teams") // Add another subscription with the same name with `updateExisting` set to true // to replace the existing subscription val updateResults = realm.query<Team>("$0 IN members AND teamName == $1", "Bob Smith", "QA") .subscribe("bob_smith_teams", updateExisting = true)
This updates the subscription automatically, instead of requiring you to manually update the subscription in the subscription set.
Wait for a Query Subscription to Sync
When you subscribe to a query's results set, that set does not contain objects until it syncs. If your app creates objects, you may not need to download synced data before the user works with it. However, if your app requires data from the server before the user can work with it, you can specify that the app should wait for data to sync. This blocks app execution until data syncs from the server.
val results = realm.query<Team>("$0 IN members", "Bob Smith") .subscribe("bob_smith_teams", updateExisting = false, WaitForSync.ALWAYS) // After waiting for sync, the results set contains all the objects // that match the query - in our case, 1 println("The number of teams that have Bob Smith as a member is ${results.size}")
This option uses the WaitForSync enum, whose values are:
FIRST_TIME
: (Default) Wait to download matching objects when your app initially creates the subscription. Otherwise, return without waiting for new downloads. The app must have an internet connection to download the data when you initially add the subscription. You can optionally specify atimeout
value.ALWAYS
: Wait to download matching objects every time the.subscribe()
method is called. The app must have an internet connection to download the data. You can optionally specify atimeout
value.NEVER
: Never wait to download matching objects. The app needs an internet connection for the user to authenticate the first time the app launches but can open offline on subsequent launches using cached credentials.
Manually Manage Subscriptions
In the client application, use the
subscriptions
API to manually manage a set of subscriptions. With this API,
you can add, update, or remove specific queries on
queryable fields. These queries determine which data syncs to the client device.
In Kotlin SDK version 1.10.0 and later, you can automatically add subscriptions
with the .subscribe() API.
This API adds subscriptions to a MutableSubscriptionSet
directly from a
RealmQuery
or a RealmResults
.
Add a Subscription
You can add an unnamed subscription or a named subscription.
Tip
Specify a Subscription Name
Always specify a subscription name if your application uses multiple subscriptions. This makes your subscriptions easier to look up, update, and delete elsewhere in your app.
To manually create a subscription, add the subscription in a subscriptions update block. You append each new subscription to the client's Realm subscriptions.
realm.subscriptions.update { add( realm.query<Task>("progressMinutes >= $0",60) ) }
You can also specify a subscription name when you create the subscription:
// Add a subscription named "team_dev_ed" realm.subscriptions.update { realm -> add( realm.query<Team>("teamName == $0", "Developer Education"), name = "team_dev_ed" ) }
Important
Object Links
You must add both an object and its linked object to the subscription set to see a linked object.
If your subscription results contain an object with a property that links to an object not contained in the results, the link appears to be null. There is no way to distinguish whether that property's value is null or whether the object it links to exists but is not available to the client due to missing subscriptions.
Bootstrap the Realm with Initial Subscriptions
You must have at least one subscription before you can read from or write to the realm. You can bootstrap a realm with an initial subscription set when you open it with the SyncConfiguration(). See Open a Synced Realm for more information.
Pass the initialSubscriptions
parameter with the
subscription queries you want to use to bootstrap the realm:
// Bootstrap the realm with an initial query to subscribe to val flexSyncConfig = SyncConfiguration.Builder(user, setOf(Team::class, Task::class)) .initialSubscriptions { realm -> add( realm.query<Team>("$0 IN members", "Bob Smith"), "bob_smith_teams" ) } .build()
If your app needs to rerun this initial subscription every
time the app starts, you can pass an additional parameter:
rerunOnOpen
. This is a boolean that denotes whether the
initial subscription should re-run every time the
app starts. You might need to do this to re-run dynamic time
ranges or other queries that require a re-computation of
static variables for the subscription.
In this example, we only want incomplete tasks. With
rerunOnOpen
set to true
, the query dynamically
recalculates the relevant objects to sync based on the
desired query results every time the app starts:
// `rerunOnOpen` lets the app recalculate this query every time the app opens val rerunOnOpenConfig = SyncConfiguration.Builder(user, setOf(Team::class, Task::class)) .initialSubscriptions(rerunOnOpen = true) { realm -> add( realm.query<Team>("completed == $0", false) ) } .build()
Wait for Subscription Changes to Sync
Writing an update to the subscription set locally is only one component of changing a subscription. After the local subscription change, the client synchronizes with the server to resolve any updates to the data due to the subscription change. This could mean adding or removing data from the synced realm.
Use the SyncConfiguration.waitForInitialRemoteData() builder method to force your application to block until client subscription data synchronizes to the backend before opening the realm:
// Update the list of subscriptions realm.subscriptions.update { add( realm.query<Team>("$0 IN members", "Jane Doe"), "jane_doe_teams" ) } // Wait for subscription to fully synchronize changes realm.subscriptions.waitForSynchronization(Duration.parse("10s"))
You can also use SubscriptionSet.waitForSynchronization() to delay execution until subscription sync completes after instantiating a sync connection.
Subscription Set State
Use the SubscriptionSet.state property to read the current state of the subscription set.
SUPERCEDED
(sic -- note alternate spelling) is a SubscriptionSetState
that can occur when another thread writes a subscription on a different instance
of the subscription set. If the state becomes SUPERCEDED
, you must obtain a
new instance of the subscription set before you can write to it.
Note
Subscription State "Complete"
The subscription set state "complete" does not mean "sync is done" or "all documents have been synced". "Complete" means the following two things have happened:
The subscription has become the active subscription set that is currently being synchronized with the server.
The documents that matched the subscription at the time the subscription was sent to the server are now on the local device. Note that this does not necessarily include all documents that currently match the subscription.
The Realm SDK does not provide a way to check whether all documents that match a subscription have synced to the device.
Update Subscriptions with a New Query
You can update subscriptions using SubscriptionSet.update().
In this example, we use MutableSubscriptionSet.add().
to update the query for the subscription named "bob_smith_teams"
.
You must set the updateExisting
parameter to true
to update
a subscription with add()
:
// Create a subscription named "bob_smith_teams" realm.subscriptions.update { add( realm.query<Team>("$0 IN members", "Bob Smith"), "bob_smith_teams" ) } // Set `updateExisting` to true to replace the existing // "bob_smith_teams" subscription realm.subscriptions.update { add( realm.query<Team>("$0 IN members AND $1 IN members", "Bob Smith", "Jane Doe"), "bob_smith_teams", updateExisting = true ) }
You cannot update subscriptions created without a name. However, you can look up unnamed subscriptions by their query, remove them from the subscription set, then add a new subscription with an updated query:
// Search for the subscription by query val subscription = realm.subscriptions.findByQuery( realm.query<Team>("teamName == $0", "Developer Education") ) // Remove the returned subscription and add the updated query if (subscription != null) { realm.subscriptions.update { remove(subscription) add( realm.query<Team>("teamName == $0", "DevEd"), "team_developer_education" ) } }
Remove Subscriptions
To remove subscriptions, you can:
Remove a single subscription query
Remove all subscriptions to a specific object type
Remove all subscriptions
Remove all unnamed subscriptions
When you remove a subscription query, Realm asynchronously removes the synced data that matched the query from the client device.
Remove a Single Subscription
You can remove a specific subscription query
using MutableSubscriptionSet.remove().
You can either look up the subscription by name, then pass the returned
subscription to remove()
, or pass the subscription name directly to
remove()
:
realm.subscriptions.update { add( realm.query<Team>("$0 IN members", "Bob Smith"), "bob_smith_teams" ) } // Wait for synchronization to complete before updating subscriptions realm.subscriptions.waitForSynchronization(Duration.parse("10s")) // Remove subscription by name realm.subscriptions.update { remove("bob_smith_teams") }
Remove All Subscriptions to an Object Type
If you want to remove all subscriptions to a specific object type, pass a class to the MutableSubscriptionSet.removeAll(). method:
realm.subscriptions.update { add( realm.query<Team>("$0 IN members", "Bob Smith"), "bob_smith_teams") } // Wait for synchronization to complete before updating subscriptions realm.subscriptions.waitForSynchronization(Duration.parse("10s")) // Remove all subscriptions to type Team realm.subscriptions.update { removeAll(Team::class) }
Remove All Subscriptions
To remove all subscriptions from the subscription set, use MutableSubscriptionSet.removeAll(). with no arguments:
Warning
If you remove all subscriptions and do not add a new one, you'll get an error. A realm opened with a flexible sync configuration needs at least one subscription to sync with the server.
// Remove all subscriptions realm.subscriptions.update { removeAll() }
Remove All Unnamed Subscriptions
New in version 1.10.0.
You may want to remove unnamed subscriptions that are transient or dynamically generated, but leave named subscriptions in place.
You can remove all unnamed (anonymous) subscriptions from the subscription set by
setting anonymousOnly
to true
when you call the removeAll
method:
// Remove all unnamed (anonymous) subscriptions realm.subscriptions.update { removeAll(anonymousOnly = true) }
Flexible Sync RQL Requirements and Limitations
Indexed Queryable Fields Subscription Requirements
Adding an indexed queryable field to
your App can improve performance for simple queries on data that is strongly
partitioned. For example, an app where queries strongly map data to a device,
store, or user, such as user_id == $0, “641374b03725038381d2e1fb”
, is
a good candidate for an indexed queryable field. However, an indexed
queryable field has specific requirements for use in a query subscription:
The indexed queryable field must be used in every subscription query. It cannot be missing from the query.
The indexed queryable field must use an
==
orIN
comparison against a constant at least once in the subscription query. For example,user_id == $0, "641374b03725038381d2e1fb"
orstore_id IN $0, {1,2,3}
.
You can optionally include an AND
comparison as long as the indexed
queryable field is directly compared against a constant using ==
or IN
at least once. For example, store_id IN {1,2,3} AND region=="Northeast"
or store_id == 1 AND (active_promotions < 5 OR num_employees < 10)
.
Invalid Flexible Sync queries on an indexed queryable field include queries where:
The indexed queryable field does not use
AND
with the rest of the query. For examplestore_id IN {1,2,3} OR region=="Northeast"
is invalid because it usesOR
instead ofAND
. Similarly,store_id == 1 AND active_promotions < 5 OR num_employees < 10
is invalid because theAND
only applies to the term next to it, not the entire query.The indexed queryable field is not used in an equality operator. For example
store_id > 2 AND region=="Northeast"
is invalid because it uses only the>
operator with the indexed queryable field and does not have an equality comparison.The query is missing the indexed queryable field entirely. For example,
region=="Northeast
ortruepredicate
are invalid because they do not contain the indexed queryable field.
Unsupported Query Operators in Flexible Sync
Flexible Sync has some limitations when using RQL operators. When you write the query subscription that determines which data to sync, the server does not support these query operators. However, you can still use the full range of RQL features to query the synced data set in the client application.
Operator Type | Unsupported Operators |
---|---|
Aggregate Operators | @avg , @count , @max , @min , @sum |
Query Suffixes | DISTINCT , SORT , LIMIT |
Case insensitive queries ([c]
) cannot use indexes effectively.
As a result, case insensitive queries are not recommended, since they could lead to
performance problems.
Flexible Sync only supports @count
for array fields.
List Queries
Flexible Sync supports querying lists using the IN
operator.
You can query a list of constants to see if it contains the value of a queryable field:
// Query a constant list for a queryable field value "priority IN { 1, 2, 3 }"
If a queryable field has an array value, you can query to see if it contains a constant value:
// Query an array-valued queryable field for a constant value "'comedy' IN genres"
Warning
You cannot compare two lists with each other in a Flexible Sync query. Note that this is valid Realm Query Language syntax outside of Flexible Sync queries.
// Invalid Flexible Sync query. Do not do this! "{'comedy', 'horror', 'suspense'} IN genres" // Another invalid Flexible Sync query. Do not do this! "ANY {'comedy', 'horror', 'suspense'} != ANY genres"
Embedded or Linked Objects
Flexible Sync does not support querying on properties in Embedded Objects
or links. For example, obj1.field == "foo"
.
Performance Considerations
API Efficiency
Managing multiple subscriptions with the .subscribe()
API described in the Subscribe to Queries section
is less efficient than performing batch updates when you
manually manage subscriptions through the subscription set API. For better performance when
making multiple subscription changes, use the subscriptions.update
API described in the
Manually Manage Subscriptions section.
Group Updates for Improved Performance
Every write transaction for a subscription set has a performance cost. If you need to make multiple updates to a Realm object during a session, consider keeping edited objects in memory until all changes are complete. This improves sync performance by only writing the complete and updated object to your realm instead of every change.