Manage Sync Subscriptions - Flutter SDK
On this page
- Prerequisites
- Align Subscriptions with Backend App
- Subscribe to Queries
- Subscribe to a Query
- Wait for a Query Subscription to Sync
- Unsubscribe from a Query
- Manually Manage Subscriptions
- Get Subscriptions
- Add a Query to the Set of Subscriptions
- Update Subscriptions with a New Query
- Remove Subscriptions
- Wait for Subscription Changes to Sync
- Subscription State
- Flexible Sync RQL Requirements and Limitations
- Indexed Queryable Fields Subscription Requirements
- Unsupported Query Operators in Flexible Sync
- List Queries
- Embedded or Linked Objects
- Query Size Limit
- Performance Considerations
- API Efficiency
- Group Updates for Improved Performance
Atlas Device Sync with Flexible Sync uses subscriptions and permissions to determine which data to sync between Atlas and your app.
You can add, update, and remove query subscriptions to determine which data syncs to the client device.
Prerequisites
Note
Flexible Sync Prerequisites
Enabling Flexible Sync in your App requires a non-sharded Atlas cluster running MongoDB 5.0 or greater
To use Flexible Sync in a Flutter application:
Align Subscriptions with Backend App
Your client-side subscription queries must align with the Device Sync configuration in your backend App Services App.
You subscription queries can either:
Query all objects of a type. Create the query using Realm.all().
Query objects that match backend App's queryable fields. Create the query using Realm.query() and a Realm Query Language query that includes one or more queryable fields. The Realm SDK throws an error if you try to create a subscription using fields that are not queryable.
To learn more about configuring queryable fields, refer to Queryable Fields in the App Services documentation.
To learn more about the limitations of using Realm Query Language with Flexible Sync, refer to the Flexible Sync RQL Limitations section.
Subscribe to Queries
New in version v1.6.0.
Flutter v1.6.0 adds experimental APIs that subscribe to and unsubscribe from a query's results. These APIs abstract away the details of manually adding and removing subscriptions.
For all subscriptions, you need an authenticated user and a synced realm.
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. Refer to the
Performance Considerations
section on this page for more information.
Subscribe to a Query
You can subscribe to the RealmResults
of a query using the
subscribe()
method. When called, the SDK creates the new subscription and adds it to the
MutableSubscriptionSet
, similar to
manually creating subscription.
You can optionally pass a unique subscription name for the query. If you add a subscription with the same name as an existing subscription, the SDK overwrites the existing subscription.
If you do not pass a subscription name, the name is set to null
, and
the subscription identifier is based on the query string. This means that
every time your query string changes, subscribe()
creates a new subscription.
To subscribe to a query, pass the following arguments to subscribe()
:
RealmResults query
: Required. ARealmResults
object that you can create using the Realm Query Language.String name
: Optional. Name for the subscription that you can refer to.bool update
: Optional. When true, adding a subscription with an existing name replaces the existing query with the new query. When false, the SDK throws an exception for duplicate subscriptions. Only use with named subscriptions.
In the following example, we subscribe to two new named queries.
final boatQuery = realm.all<Boat>(); final bigPlaneQuery = realm.query<Plane>("numSeats > 100"); final boatSubscription = await boatQuery.subscribe(name: "boats"); final planeSubscription = await bigPlaneQuery.subscribe(name: "big-planes");
Tip
Specify a Subscription Name
We recommend that you always specify a subscription name, especially if your application uses multiple subscriptions. This makes finding and managing your subscriptions easier.
Wait for a Query Subscription to Sync
When you subscribe to a query's results, the results do not contain objects until synced data is downloaded. When you do need to wait for synced objects to finish downloading, configure the waitForSyncMode option.
This example uses the firstTime
option, which is the default behavior.
A subscription with firstTime
behavior only waits for sync to finish when a
subscription is first created.
final bigPlaneQuery = realm.query<Plane>("numSeats > 100"); final planeSubscription = await bigPlaneQuery.subscribe( name: "firstTimeSync", waitForSyncMode: WaitForSyncMode.firstTime, );
The other supported waitForSyncMode
options are:
always
: Wait to download matching objects every time your app launches. The app must have an internet connection at every launch.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.
You can optionally specify a cancellationToken to limit how long the sync download runs:
final bigPlaneQuery = realm.query<Plane>("numSeats > 200"); final planeSubscription = await bigPlaneQuery.subscribe( name: "alwaysWaitSync", waitForSyncMode: WaitForSyncMode.always, cancellationToken: TimeoutCancellationToken(Duration(seconds: 5)), );
Unsubscribe from a Query
Subscriptions persist across user sessions unless you unsubscribe from them. You can unsubscribe from a query's results using unsubscribe().
This removes the subscription from the list of active subscriptions, similar to
manually removing a subscription.
Note that the results list may still contain objects after calling unsubscribe()
if another subscription exists that contains overlapping objects.
When you call unsubscribe()
on a query, the SDK removes any
subscriptions with queries that exactly match the one you call
unsubscribe()
on. This method returns before objects matching the
removed subscription are deleted from the realm. Sync continues in the
background based on the new set of subscriptions.
planeQuery.unsubscribe(); trainQuery.unsubscribe();
Manually Manage Subscriptions
When configuring Flexible Sync on the backend, you specify which fields your client application can query. In the client application, use the Realm.subscriptions property to manage a set of subscriptions to specific queries on queryable fields.
You can do the following with your subscriptions:
Get a list of all subscriptions
Add subscriptions
Check subscription state
Update a subscription with a new query
Remove subscriptions
When the data matches the subscription, and the authenticated user has the appropriate permissions, Device Sync syncs the backend data with the client app.
Subscription sets persist across sessions even if you no longer include the subscription in your code. Subscription information is stored in the synced realm's database file. You must explicitly remove a subscription for it to stop attempting to sync matching data.
You can specify a string name for your subscription. If you do not give your
subscription a name, the name is set to null
.
When you create a subscription, Realm looks for data matching a query on a specific object type. In your Flexible Sync subscriptions, you can have subscriptions on several different object types or several queries on the same object type.
Get Subscriptions
When using Flexible Sync, you can access a
SubscriptionSet, a
collection of subscriptions, through the Realm.subscriptions
property.
You can use this subscriptions set to add queries to this list of subscriptions and update existing subscriptions, as shown in the examples below.
final subscriptions = realm.subscriptions;
Add a Query to the Set of Subscriptions
You must perform all mutations on the subscriptions set within an update block. To create an update block, call SubscriptionSet.update().
The update block callback function includes a MutableSubscriptionSet() object as an argument.
You can modify your method on the SubscriptionSet
to add a query to the subscription.
Important
Flexible Sync does not support all the operators available in Realm Query Language. See Flexible Sync RQL Limitations for details.
The MutableSubscriptionSet.add()
method takes three arguments:
RealmResults query
: Required. ARealmResults
object that you can create using the Realm Query Language query.String name
: Optional. Name for the subscription that you can refer to.bool update
: Optional. When true, adding a subscription with an existing name replaces the existing query with the new query. Only use with named subscriptions.
Note
Duplicate Subscriptions
If you add a duplicate unnamed subscription with the same query, Realm automatically removes it; adding an identical named subscription is a no-op. Therefore, in both cases, duplicate subscriptions are ignored.
You can add a single query, or batch multiple queries within a SubscriptionSet.update
block. Performing query updates is an expensive operation on the server. We
strongly advise designing your application to minimize subscription updates.
You can do this by creating all subscriptions in a single update block
the first time the user launches the app and batching any follow-up changes
to the subscription set.
In the example below, we subscribe to two queries.
final planeQuery = realm.all<Plane>(); final longTrainQuery = realm.query<Train>("numCars >= 5"); realm.subscriptions.update((MutableSubscriptionSet mutableSubscriptions) { mutableSubscriptions.add(planeQuery, name: "planes"); mutableSubscriptions.add(longTrainQuery, name: 'long-trains', update: true); }); await realm.subscriptions.waitForSynchronization();
Update Subscriptions with a New Query
You can update a named subscription with a new query. To update a subscription's
query, open an update block with SubscriptionSet.update()
.
In the callback function of the update block, pass the following arguments to MutableSubscriptionSet.add()
:
The new query
The name of the subscription that you want to update
update: true
You cannot update an unnamed subscription. Alternatively, you can delete the unnamed subscription, and create a new subscription with the desired query.
In the following example, long trains are re-defined to be any trains that have more than 10 cars.
final longerTrainQuery = realm.query<Train>("numCars > 10"); realm.subscriptions.update((MutableSubscriptionSet mutableSubscriptions) { mutableSubscriptions.add(longerTrainQuery, name: 'long-trains', update: true); });
Remove Subscriptions
To remove subscriptions from the subscription set, you can:
Remove a single subscription with the given query
Remove a single subscription with the given name
Remove a single subscription with the subscription reference
Remove all subscriptions for a Realm object type
Remove all subscriptions
When you remove a subscription query, the server also removes synced data from the client device.
Remove a Subscription by Query
Within an update block, you can remove a specific subscription by query.
Open an update block with SubscriptionSet.update()
. Pass the Subscription
to
MutableSubscriptionSet.removeByQuery().
In the following example, the subscription for all Plane
objects is removed.
realm.subscriptions.update((MutableSubscriptionSet mutableSubscriptions) { mutableSubscriptions.removeByQuery(realm.all<Plane>()); });
Remove a Subscription by Name
Within an update block, you can remove a specific subscription by name. Pass the name to MutableSubscriptionSet.removeByName()
realm.subscriptions.update((MutableSubscriptionSet mutableSubscriptions) { mutableSubscriptions.removeByName('long-trains'); });
Remove a Subscription by Reference
You can remove a subscription if you have a reference to its Subscription object.
Within a subscription update block, pass the Subscription
reference to
MutableSubscriptionSet.remove().
final sub = realm.subscriptions[0]; realm.subscriptions.update((MutableSubscriptionSet mutableSubscriptions) { mutableSubscriptions.remove(sub); });
Remove All Subscriptions for an Object Type
You can remove all subscriptions for a given Realm object type. Within a subscription update block, call MutableSubscriptionSet.removeByType().
realm.subscriptions.update((MutableSubscriptionSet mutableSubscriptions) { mutableSubscriptions.removeByType<Train>(); });
Remove All Subscriptions
Within a subscription update block, you can remove all unnamed subscriptions from the subscriptions set with MutableSubscriptionSet.clear().
realm.subscriptions.update((MutableSubscriptionSet mutableSubscriptions) { mutableSubscriptions.clear(); });
Wait for Subscription Changes to Sync
Mutating the subscription set within an update block is only one part of changing a subscription. After the local subscription change, the realm synchronizes with the server to resolve any updates to the data due to the subscription change. This includes adding or removing data from the synced realm.
Use Realm.subscriptions.waitForSynchronization() to wait for the server to acknowledge this set of subscriptions. If the server rejects the change, and an exception is thrown.
An exception may occur if:
You subscribe to an unsupported query. Subscribing to an unsupported query will pause synchronization. To resume synchronization, remove the unsupported query.
You are performing an invalid action, such as adding an object that does not match a subscription. This triggers a client reset: data is erased from the realm, and a new copy of the data is created without any subscriptions in the set.
await realm.subscriptions.waitForSynchronization();
Subscription State
Use the Realm.subscriptions.state property to read the current state of the subscription set.
The superseded
state is a SubscriptionSetState that can occur when another thread updates a
subscription on a different instance of the subscription set. If the state
becomes superseded
, you must obtain a new instance of the subscription set
before you can update 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.
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"
.
Query Size Limit
The size limit for any given query subscription in your subscription set is 256 kB. Exceeding this limit results in a LimitsExceeded Error.
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.