From c721187737323f3fe41e5e25fbe45a3f169c6aca Mon Sep 17 00:00:00 2001 From: cbullinger Date: Fri, 28 Jul 2023 11:45:18 -0400 Subject: [PATCH] Initial draft --- .../mongodb/realm/realmkmmapp/CreateTest.kt | 46 +++++ .../mongodb/realm/realmkmmapp/SchemaTest.kt | 80 +++++++-- source/sdk/kotlin/sync.txt | 1 + source/sdk/kotlin/sync/background-sync.txt | 160 ++++++++++++++++++ 4 files changed, 277 insertions(+), 10 deletions(-) create mode 100644 source/sdk/kotlin/sync/background-sync.txt diff --git a/examples/kotlin/shared/src/commonTest/kotlin/com/mongodb/realm/realmkmmapp/CreateTest.kt b/examples/kotlin/shared/src/commonTest/kotlin/com/mongodb/realm/realmkmmapp/CreateTest.kt index 2effcc15c2a..d0d00c86ef6 100644 --- a/examples/kotlin/shared/src/commonTest/kotlin/com/mongodb/realm/realmkmmapp/CreateTest.kt +++ b/examples/kotlin/shared/src/commonTest/kotlin/com/mongodb/realm/realmkmmapp/CreateTest.kt @@ -4,13 +4,16 @@ import io.realm.kotlin.Realm import io.realm.kotlin.RealmConfiguration import io.realm.kotlin.ext.query import io.realm.kotlin.ext.realmDictionaryOf +import io.realm.kotlin.ext.realmSetOf import io.realm.kotlin.internal.platform.runBlocking import io.realm.kotlin.query.RealmResults import io.realm.kotlin.types.RealmDictionary import io.realm.kotlin.types.RealmObject +import io.realm.kotlin.types.RealmSet import org.mongodb.kbson.ObjectId import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue // :replace-start: { // "terms": { @@ -26,6 +29,17 @@ class CreateTest_Frog : RealmObject { } // :snippet-end: +// :snippet-start: define-realm-set +class CreateTest_Author : RealmObject { + var name: String = "" +} +class CreateTest_Book : RealmObject { + var _id: ObjectId = ObjectId() + var title: String = "" + var authors: RealmSet = realmSetOf() +} +// :snippet-end + class CreateTest: RealmTest() { @Test fun createRealmDictionaryType() { @@ -106,6 +120,38 @@ class CreateTest: RealmTest() { realm.close() } } + + @Test + fun createRealmSet() { + runBlocking { + val config = RealmConfiguration.Builder( + schema = setOf(CreateTest_Book::class, CreateTest_Author::class) + ) + .inMemory() + .directory("/tmp/") + .build() + val realm = Realm.open(config) + Log.v("Successfully opened realm: ${realm.configuration.name}") + + // :snippet-start: create-set + realm.write { + copyToRealm(CreateTest_Book().apply { + title = "The Hobbit" + authors = realmSetOf() + }) + } + realm.write { + val books = query("$0 == title", "The Hobbit").find().first() + val author = copyToRealm(CreateTest_Author().apply { name = "J.R.R. Tolkien" }) + books.authors.add(author) + assertTrue(books.authors.contains(author)) + assertEquals(1, books.authors.size) + } + + realm.close() + } + + } } // :replace-end: \ No newline at end of file diff --git a/examples/kotlin/shared/src/commonTest/kotlin/com/mongodb/realm/realmkmmapp/SchemaTest.kt b/examples/kotlin/shared/src/commonTest/kotlin/com/mongodb/realm/realmkmmapp/SchemaTest.kt index 26fd176fc99..1810bc42512 100644 --- a/examples/kotlin/shared/src/commonTest/kotlin/com/mongodb/realm/realmkmmapp/SchemaTest.kt +++ b/examples/kotlin/shared/src/commonTest/kotlin/com/mongodb/realm/realmkmmapp/SchemaTest.kt @@ -16,11 +16,13 @@ import io.realm.kotlin.types.annotations.PersistedName import io.realm.kotlin.types.annotations.PrimaryKey import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.datetime.Instant import org.mongodb.kbson.ObjectId import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertFalse // :replace-start: { // "terms": { @@ -222,10 +224,11 @@ class SchemaTest: RealmTest() { } } @Test - @kotlin.test.Ignore // ignored until bugfix in 1.7.1 release fun createRealmSetTypes() { runBlocking { val config = RealmConfiguration.Builder(setOf(Frog2::class, Snack::class)) + .inMemory() + .directory("/tmp/") .build() val realm = Realm.open(config) Log.v("Successfully opened realm: ${realm.configuration.path}") @@ -233,14 +236,14 @@ class SchemaTest: RealmTest() { // :snippet-start: add-item-to-realm-set realm.write { // Create a Frog object named 'Kermit' that will have a RealmSet of favorite snacks - val frog = this.copyToRealm(Frog2().apply { + val frog = copyToRealm(Frog2().apply { name = "Kermit" }) // Get the RealmSet of favorite snacks from the Frog object we just created val set = frog.favoriteSnacks // Create a Snack object for the Frog to add to Kermit's favorite snacks - val fliesSnack = this.copyToRealm(Snack().apply { + val fliesSnack = copyToRealm(Snack().apply { name = "flies" }) @@ -252,21 +255,20 @@ class SchemaTest: RealmTest() { // :snippet-start: add-all-to-realm-set realm.write { - val myFrog: Frog2 = realm.query("name = 'Kermit'").first().find()!! + val myFrog = query("name = 'Kermit'").first().find()!! val set = findLatest(myFrog)!!.favoriteSnacks - val cricketsSnack = this.copyToRealm(Snack().apply { + val cricketsSnack = copyToRealm(Snack().apply { name = "crickets" }) - val earthWormsSnack = this.copyToRealm(Snack().apply { + val earthWormsSnack = copyToRealm(Snack().apply { name = "earthworms" }) - val waxWormsSnack = this.copyToRealm(Snack().apply { + val waxWormsSnack = copyToRealm(Snack().apply { name = "waxworms" }) set.addAll(setOf(cricketsSnack, earthWormsSnack, waxWormsSnack)) - println("$set") // :remove: assertEquals(4, set.size) // :remove: // :uncomment-start: //} @@ -283,14 +285,23 @@ class SchemaTest: RealmTest() { // :snippet-end: // :snippet-start: remove-item-from-set - val fliesSnack = realm.query("name = 'flies'").first().find() + val fliesSnack = query("name = 'flies'").first().find() set.remove(fliesSnack) // :snippet-end: + assertFalse(set.contains(fliesSnack)) + assertEquals(3, set.size) + } + delay(1000) + realm.write { + val myFrog: Frog2 = query("name = 'Kermit'").find().first() + val set = myFrog.favoriteSnacks // :snippet-start: remove-multiple-items-from-set - set.removeAll(set) + set.removeAll(myFrog.favoriteSnacks) // :snippet-end: + // test set is empty + assertEquals(0, set.size) } @@ -301,6 +312,7 @@ class SchemaTest: RealmTest() { ?.asFlow() ?.collect() { // Listen for changes to the RealmSet + } } // :snippet-end: @@ -309,5 +321,53 @@ class SchemaTest: RealmTest() { Realm.deleteRealm(config) } } + + @Test + fun createRealmSetTypes2() { + runBlocking { + val config = RealmConfiguration.Builder(setOf(Frog2::class, Snack::class)) + .directory("/tmp/") + .inMemory() + .build() + val realm = Realm.open(config) + Log.v("Successfully opened realm: ${realm.configuration.path}") + + // :snippet-start: add-item-to-realm-set + realm.write { + // Create a Frog object named 'Kermit' that will have a RealmSet of favorite snacks + val frog = copyToRealm(Frog2().apply { + name = "Kermit" + }) + // Get the RealmSet of favorite snacks from the Frog object we just created + val set = frog.favoriteSnacks + set.removeAll(set) + + // Create a Snack object for the Frog to add to Kermit's favorite snacks + val fliesSnack = copyToRealm(Snack().apply { + name = "flies" + }) + val cricketsSnack = copyToRealm(Snack().apply { + name = "crickets" + }) + val earthWormsSnack = copyToRealm(Snack().apply { + name = "earthworms" + }) + val waxWormsSnack = copyToRealm(Snack().apply { + name = "waxworms" + }) + + set.addAll(setOf(fliesSnack, cricketsSnack, earthWormsSnack, waxWormsSnack)) + + // :snippet-start: remove-multiple-items-from-set + set.removeAll(set) + // :snippet-end: + // test set is empty + assertEquals(0, set.size) + } + + realm.close() + Realm.deleteRealm(config) + } + } } // :replace-end: \ No newline at end of file diff --git a/source/sdk/kotlin/sync.txt b/source/sdk/kotlin/sync.txt index e96856daa79..e1b79cec5e2 100644 --- a/source/sdk/kotlin/sync.txt +++ b/source/sdk/kotlin/sync.txt @@ -14,6 +14,7 @@ Device Sync - Kotlin SDK Handle Sync Errors Set the Client Log Level Stream Data to Atlas + Sync Data in the Background Partition-Based Sync .. contents:: On this page diff --git a/source/sdk/kotlin/sync/background-sync.txt b/source/sdk/kotlin/sync/background-sync.txt new file mode 100644 index 00000000000..74be3a3e2bb --- /dev/null +++ b/source/sdk/kotlin/sync/background-sync.txt @@ -0,0 +1,160 @@ +.. _kotlin-background-sync: + +======================================== +Sync Data in the Background - Kotlin SDK +======================================== + +.. contents:: On this page + :local: + :backlinks: none + :depth: 2 + :class: singlecol + + +If you need to sync data when your app isn't running, you can sync realms +in a background process. + +Prerequisites +------------- + +To get started with background synchronization, you need to add the +following dependencies to your Android application: + +- :android:`androidx.work:work-runtime ` + to enqueue jobs +- :android:`androidx.concurrent:concurrent-futures ` + to return job results from a background worker + +Example +------- + +Background sync requires two things: + +- Synchronization logic +- A scheduled job that periodically performs the sync logic + +Synchronization Logic +~~~~~~~~~~~~~~~~~~~~~ + +First, write the custom logic that synchronizes your realm. Treat this +logic as a standalone connection to your backend. As a result, you'll +need to: + +- Get the Realm sync configuration for your app +- Authenticate a user to open the realm. You can use a user's cached + credentials if the user recently used the app. + +Open the realm, then use `SyncSession.downloadAllServerChanges() <{+kotlin-sync-prefix+}io.realm.kotlin.mongodb.sync/-sync-session/download-all-server-changes.html>`__ +and `SyncSession.uploadAllLocalChanges() <{+kotlin-sync-prefix+}io.realm.kotlin.mongodb.sync/-sync-session/upload-all-local-changes.html>`__ +to synchronize the realm fully with the backend. For more information, see +:ref:`Manage Sync Sessions `. + +You can execute this logic as a background process using a subclass of +:android:`CoroutineWorker `. +Put your synchronization logic in the ``doWork()`` method of your worker. + +.. code-block:: kotlin + :caption: Example RealmBackgroundWorker.kt + + package com.mongodb.app.worker + + import android.annotation.SuppressLint + import android.content.Context + import androidx.concurrent.futures.ResolvableFuture + import androidx.work.CoroutineWorker + import androidx.work.WorkerParameters + import com.mongodb.app.app + import com.mongodb.app.data.RealmSyncRepository + import io.realm.kotlin.Realm + import io.realm.kotlin.mongodb.syncSession + + class RealmBackgroundWorker(context: Context, workerParams: WorkerParameters) : + CoroutineWorker(context, workerParams) { + + private lateinit var future: ResolvableFuture + @SuppressLint("RestrictedApi") + override suspend fun doWork(): Result { + future = ResolvableFuture.create() + + // Get the realm configuration for your app + val syncRepository = RealmSyncRepository { session, error -> + future.setException(error) + } + val config = syncRepository.getRealmConfiguration() + + // Check if user is logged-in + if (app.currentUser?.loggedIn == true) { + val realm = Realm.open(config) + try { + realm.syncSession.downloadAllServerChanges() + realm.syncSession.uploadAllLocalChanges() + } catch (e: InterruptedException) { + e.printStackTrace() + } + } + return future.get() + } + companion object { + const val UNIQUE_WORK_NAME = "RealmBackgroundWorker" + } + } + +Worker +~~~~~~ + +To create a worker that periodically performs background sync: + +1. Create a set of :android:`Constraints ` + that specify the conditions required for your worker. Because synchronizing + a realm uses data, you should consider only downloading changes in the + background when the device is *not*: + + - Low on battery + - Using a metered data source + +2. Specify how frequently your worker should execute. Your repeat interval + depends on how frequently data updates in the realm and how often users + open your application: + + - If the realm frequently updates throughout the day, consider setting a repeat + interval of 1-3 hours. + - If the realm only updates a small number of times each day, it's best to + set a higher repeat interval and only background sync once or twice a day. + +3. Enqueue your worker with the Android OS. Assign it a unique identifier + so that you can update the job in the future. + +.. tip:: + + You can create the background sync job inside an Application subclass in + your app to guarantee that the logic only executes once every time your + application runs. + + +.. code-block:: kotlin + :caption: Example worker + + // Define any constraints for the background job + val constraints: Constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresBatteryNotLow(true) + .build() + + // Define the frequency of the background job + val backgroundRealmSync = + PeriodicWorkRequestBuilder( + // Repeat every 12 hours + 12, TimeUnit.HOURS, + // Execute job at any point during that 12-hour period + 12, TimeUnit.HOURS + ) + .setConstraints(constraints) + .build() + + // Enqueue the work job, replacing it with the most recent + // version if we update it + WorkManager.getInstance(this).enqueueUniquePeriodicWork( + RealmBackgroundWorker.UNIQUE_WORK_NAME, + ExistingPeriodicWorkPolicy.UPDATE, + backgroundRealmSync + )