diff --git a/AppServicesUsageSamples/README.md b/AppServicesUsageSamples/README.md
index 16c1ec3..575c210 100644
--- a/AppServicesUsageSamples/README.md
+++ b/AppServicesUsageSamples/README.md
@@ -18,6 +18,10 @@ This sample demonstrates user presence detection with App Services.
A showcase for the different client reset resolution strategies with an sync error handling example.
+### [Modelling unstructured data](apps/dynamic-data/README.md)
+
+A showcase for storing and synchronizing unstructured data through collections in mixed properties.
+
## Demo app structure
The project has been structured in two main folders:
@@ -52,4 +56,4 @@ The `realm-cli import` command will prompt for the app configuration details, th
After deploying the Atlas apps, you will need to update [Constants.kt](demo/src/main/java/io/realm/appservicesusagesamples/Constants.kt) with their app ids.
-Once you have completed these steps, you would be able to run the samples using the Kotlin demo app.
\ No newline at end of file
+Once you have completed these steps, you would be able to run the samples using the Kotlin demo app.
diff --git a/AppServicesUsageSamples/Screenshots/dynamic-data-browser.png b/AppServicesUsageSamples/Screenshots/dynamic-data-browser.png
new file mode 100644
index 0000000..ea9c65e
Binary files /dev/null and b/AppServicesUsageSamples/Screenshots/dynamic-data-browser.png differ
diff --git a/AppServicesUsageSamples/apps/dynamic-data/.mdb/meta.json b/AppServicesUsageSamples/apps/dynamic-data/.mdb/meta.json
new file mode 100644
index 0000000..39af777
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/.mdb/meta.json
@@ -0,0 +1,7 @@
+{
+ "config_version": 20230101,
+ "app_id": "6655920fc1ed444018db297b",
+ "group_id": "64c37d22e47344686a325451",
+ "client_app_id": "dynamic-data-fmzmcph",
+ "last_pulled": 1717066261
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/README.md b/AppServicesUsageSamples/apps/dynamic-data/README.md
new file mode 100644
index 0000000..83601ca
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/README.md
@@ -0,0 +1,15 @@
+# Dynamic data
+
+With the newly added ability to store collections in mixed properties, you can now store and synchronized
+data without pre-known schema.
+
+This app just holds a single data class that mixes strictly typed properties and a single mixed property
+that will be used as an entry point for the potentially deeply nested, dynamic data.
+
+The Kotlin UI shows each entity and associated 'configuration' mixed property in a tree view. There is
+currently no update options in the UI, so data has to be added through the Atlas UI in the
+"Data Service" section:
+
+![alt text](../../Screenshots/dynamic-data-browser.png "Atlas collection browser")
+
+Updates will be reflected in the Kotlin UI.
diff --git a/AppServicesUsageSamples/apps/dynamic-data/auth/custom_user_data.json b/AppServicesUsageSamples/apps/dynamic-data/auth/custom_user_data.json
new file mode 100644
index 0000000..a82d0fb
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/auth/custom_user_data.json
@@ -0,0 +1,3 @@
+{
+ "enabled": false
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/auth/providers.json b/AppServicesUsageSamples/apps/dynamic-data/auth/providers.json
new file mode 100644
index 0000000..dff103b
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/auth/providers.json
@@ -0,0 +1,12 @@
+{
+ "anon-user": {
+ "name": "anon-user",
+ "type": "anon-user",
+ "disabled": false
+ },
+ "api-key": {
+ "name": "api-key",
+ "type": "api-key",
+ "disabled": true
+ }
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/data_sources/mongodb-atlas/config.json b/AppServicesUsageSamples/apps/dynamic-data/data_sources/mongodb-atlas/config.json
new file mode 100644
index 0000000..9913676
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/data_sources/mongodb-atlas/config.json
@@ -0,0 +1,10 @@
+{
+ "name": "mongodb-atlas",
+ "type": "mongodb-atlas",
+ "config": {
+ "clusterName": "Cluster0",
+ "readPreference": "primary",
+ "wireProtocolEnabled": false
+ },
+ "version": 1
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/data_sources/mongodb-atlas/default_rule.json b/AppServicesUsageSamples/apps/dynamic-data/data_sources/mongodb-atlas/default_rule.json
new file mode 100644
index 0000000..1d5545f
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/data_sources/mongodb-atlas/default_rule.json
@@ -0,0 +1,17 @@
+{
+ "roles": [
+ {
+ "name": "readAndWriteAll",
+ "apply_when": {},
+ "document_filters": {
+ "read": true,
+ "write": true
+ },
+ "insert": true,
+ "delete": true,
+ "search": true,
+ "read": true,
+ "write": true
+ }
+ ]
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/data_sources/mongodb-atlas/dynamic_data/DynamicDataEntity/schema.json b/AppServicesUsageSamples/apps/dynamic-data/data_sources/mongodb-atlas/dynamic_data/DynamicDataEntity/schema.json
new file mode 100644
index 0000000..25b6afc
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/data_sources/mongodb-atlas/dynamic_data/DynamicDataEntity/schema.json
@@ -0,0 +1,19 @@
+{
+ "title": "DynamicDataEntity",
+ "type": "object",
+ "required": [
+ "_id",
+ "name"
+ ],
+ "properties": {
+ "_id": {
+ "bsonType": "objectId"
+ },
+ "configuration": {
+ "bsonType": "mixed"
+ },
+ "name": {
+ "bsonType": "string"
+ }
+ }
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/environments/development.json b/AppServicesUsageSamples/apps/dynamic-data/environments/development.json
new file mode 100644
index 0000000..ad7e98e
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/environments/development.json
@@ -0,0 +1,3 @@
+{
+ "values": {}
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/environments/no-environment.json b/AppServicesUsageSamples/apps/dynamic-data/environments/no-environment.json
new file mode 100644
index 0000000..ad7e98e
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/environments/no-environment.json
@@ -0,0 +1,3 @@
+{
+ "values": {}
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/environments/production.json b/AppServicesUsageSamples/apps/dynamic-data/environments/production.json
new file mode 100644
index 0000000..ad7e98e
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/environments/production.json
@@ -0,0 +1,3 @@
+{
+ "values": {}
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/environments/qa.json b/AppServicesUsageSamples/apps/dynamic-data/environments/qa.json
new file mode 100644
index 0000000..ad7e98e
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/environments/qa.json
@@ -0,0 +1,3 @@
+{
+ "values": {}
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/environments/testing.json b/AppServicesUsageSamples/apps/dynamic-data/environments/testing.json
new file mode 100644
index 0000000..ad7e98e
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/environments/testing.json
@@ -0,0 +1,3 @@
+{
+ "values": {}
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/functions/config.json b/AppServicesUsageSamples/apps/dynamic-data/functions/config.json
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/functions/config.json
@@ -0,0 +1 @@
+[]
diff --git a/AppServicesUsageSamples/apps/dynamic-data/graphql/config.json b/AppServicesUsageSamples/apps/dynamic-data/graphql/config.json
new file mode 100644
index 0000000..406b1ab
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/graphql/config.json
@@ -0,0 +1,4 @@
+{
+ "use_natural_pluralization": true,
+ "disable_schema_introspection": false
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/graphql/validation_settings.json b/AppServicesUsageSamples/apps/dynamic-data/graphql/validation_settings.json
new file mode 100644
index 0000000..a112d6f
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/graphql/validation_settings.json
@@ -0,0 +1,6 @@
+{
+ "read_validation_action": "ERROR",
+ "read_validation_level": "STRICT",
+ "write_validation_action": "ERROR",
+ "write_validation_level": "STRICT"
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/hosting/config.json b/AppServicesUsageSamples/apps/dynamic-data/hosting/config.json
new file mode 100644
index 0000000..a82d0fb
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/hosting/config.json
@@ -0,0 +1,3 @@
+{
+ "enabled": false
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/https_endpoints/config.json b/AppServicesUsageSamples/apps/dynamic-data/https_endpoints/config.json
new file mode 100644
index 0000000..fe51488
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/https_endpoints/config.json
@@ -0,0 +1 @@
+[]
diff --git a/AppServicesUsageSamples/apps/dynamic-data/root_config.json b/AppServicesUsageSamples/apps/dynamic-data/root_config.json
new file mode 100644
index 0000000..81530e3
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/root_config.json
@@ -0,0 +1,5 @@
+{
+ "name": "dynamic-data",
+ "provider_region": "aws-eu-central-1",
+ "deployment_model": "LOCAL"
+}
diff --git a/AppServicesUsageSamples/apps/dynamic-data/sync/config.json b/AppServicesUsageSamples/apps/dynamic-data/sync/config.json
new file mode 100644
index 0000000..9b7dcf6
--- /dev/null
+++ b/AppServicesUsageSamples/apps/dynamic-data/sync/config.json
@@ -0,0 +1,9 @@
+{
+ "type": "flexible",
+ "state": "enabled",
+ "development_mode_enabled": true,
+ "service_name": "mongodb-atlas",
+ "database_name": "dynamic_data",
+ "client_max_offline_days": 30,
+ "is_recovery_mode_disabled": false
+}
diff --git a/AppServicesUsageSamples/demo/build.gradle.kts b/AppServicesUsageSamples/demo/build.gradle.kts
index 92273e7..e92cfd7 100644
--- a/AppServicesUsageSamples/demo/build.gradle.kts
+++ b/AppServicesUsageSamples/demo/build.gradle.kts
@@ -5,13 +5,13 @@ plugins {
// alias(libs.plugins.realm.kotlin)
id(libs.plugins.realm.kotlin.get().pluginId)
alias(libs.plugins.kotlin.serialization)
-// alias(libs.plugins.jetbrainsCompose)
+ alias(libs.plugins.jetbrainsCompose)
alias(libs.plugins.compose.compiler)
}
android {
namespace = "io.realm.appservicesusagesamples"
- compileSdk = 33
+ compileSdk = 34
defaultConfig {
applicationId = "io.realm.appservicesusagesamples"
@@ -76,6 +76,10 @@ dependencies {
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
+ implementation(libs.bonsai)
+ // Compile only dependency of libs.bonsai, so need to include it explicitly
+ implementation(compose.materialIconsExtended)
+
implementation(libs.realm.library.sync)
debugImplementation(composeBom)
diff --git a/AppServicesUsageSamples/demo/src/main/AndroidManifest.xml b/AppServicesUsageSamples/demo/src/main/AndroidManifest.xml
index b708f50..ea003fb 100644
--- a/AppServicesUsageSamples/demo/src/main/AndroidManifest.xml
+++ b/AppServicesUsageSamples/demo/src/main/AndroidManifest.xml
@@ -27,6 +27,7 @@
+
-
\ No newline at end of file
+
diff --git a/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/AppServicesUsageSamplesApp.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/AppServicesUsageSamplesApp.kt
index 2cff5a5..4846b51 100644
--- a/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/AppServicesUsageSamplesApp.kt
+++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/AppServicesUsageSamplesApp.kt
@@ -17,6 +17,7 @@
package io.realm.appservicesusagesamples
import android.app.Application
+import io.realm.appservicesusagesamples.dynamicdata.dynamicDataViewModel
import io.realm.appservicesusagesamples.errorhandling.errorHandlingModule
import io.realm.appservicesusagesamples.propertyencryption.propertyEncryptionModule
import io.realm.appservicesusagesamples.presence.presenceDetectionModule
@@ -42,6 +43,7 @@ class AppServicesUsageSamplesApp: Application() {
propertyEncryptionModule,
presenceDetectionModule,
errorHandlingModule,
+ dynamicDataViewModel,
)
}
}
diff --git a/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/Constants.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/Constants.kt
index 9a969fd..f805e63 100644
--- a/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/Constants.kt
+++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/Constants.kt
@@ -21,3 +21,4 @@ package io.realm.appservicesusagesamples
const val PROPERTY_ENCRYPTION_APP_ID = ""
const val USER_PRESENCE_APP_ID = ""
const val ERROR_HANDLING_APP_ID = ""
+const val DYNAMIC_DATA_APP_ID = ""
diff --git a/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/DependencyInjection.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/DependencyInjection.kt
index 42b2874..2a171de 100644
--- a/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/DependencyInjection.kt
+++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/DependencyInjection.kt
@@ -16,6 +16,7 @@
*/
package io.realm.appservicesusagesamples
+import io.realm.appservicesusagesamples.dynamicdata.DynamicDataActivity
import io.realm.appservicesusagesamples.propertyencryption.PropertyEncryptionActivity
import io.realm.appservicesusagesamples.ui.SampleSelectorScreenViewModel
import io.realm.appservicesusagesamples.presence.PresenceDetectionActivity
@@ -23,6 +24,7 @@ import io.realm.appservicesusagesamples.ui.EntryView
import io.realm.appservicesusagesamples.ui.buttonSelector
import io.realm.appservicesusagesamples.ui.errorHandlingSelector
import io.realm.kotlin.mongodb.App
+import io.realm.kotlin.mongodb.AppConfiguration
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.dsl.module
@@ -52,6 +54,13 @@ enum class Demos(
ERROR_HANDLING(
appId = ERROR_HANDLING_APP_ID,
addView = errorHandlingSelector,
+ ),
+ DYNAMIC_DATA(
+ appId = DYNAMIC_DATA_APP_ID,
+ addView = buttonSelector(
+ "Dynamic data",
+ DynamicDataActivity::class.java,
+ )
);
val qualifier = named(appId)
diff --git a/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/DependencyInjection.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/DependencyInjection.kt
new file mode 100644
index 0000000..22d98b2
--- /dev/null
+++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/DependencyInjection.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024 Realm Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.realm.appservicesusagesamples.dynamicdata
+
+import io.realm.appservicesusagesamples.Demos
+import io.realm.appservicesusagesamples.dynamicdata.ui.DynamicDataViewModel
+import io.realm.appservicesusagesamples.presence.ui.UserStatusListViewModel
+import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.module
+
+val dynamicDataViewModel = module {
+ viewModel {
+ DynamicDataViewModel(
+ app = get(qualifier = Demos.DYNAMIC_DATA.qualifier),
+ )
+ }
+}
diff --git a/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/DynamicDataActivity.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/DynamicDataActivity.kt
new file mode 100644
index 0000000..15ccbd6
--- /dev/null
+++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/DynamicDataActivity.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2024 Realm Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.realm.appservicesusagesamples.dynamicdata
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import io.realm.appservicesusagesamples.dynamicdata.ui.DynamicDataScreen
+import io.realm.appservicesusagesamples.dynamicdata.ui.DynamicDataViewModel
+import io.realm.appservicesusagesamples.ui.theme.AppServicesUsageSamplesTheme
+import org.koin.android.scope.AndroidScopeComponent
+import org.koin.androidx.scope.activityRetainedScope
+import org.koin.androidx.viewmodel.ext.android.viewModel
+import org.koin.core.scope.Scope
+
+/**
+ * Activity that hosts the views that would demo property level encryption.
+ */
+class DynamicDataActivity : ComponentActivity(), AndroidScopeComponent {
+ override val scope: Scope by activityRetainedScope()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ val viewModel: DynamicDataViewModel by viewModel()
+
+
+ setContent {
+ AppServicesUsageSamplesTheme {
+ DynamicDataScreen(
+ viewModel,
+ )
+ }
+ }
+ }
+}
diff --git a/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/models/DynamicDataEntity.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/models/DynamicDataEntity.kt
new file mode 100644
index 0000000..7f742b2
--- /dev/null
+++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/models/DynamicDataEntity.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 Realm Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
+ * or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.realm.appservicesusagesamples.dynamicdata.models
+
+import io.realm.kotlin.types.RealmAny
+import io.realm.kotlin.types.RealmObject
+import io.realm.kotlin.types.annotations.PersistedName
+import io.realm.kotlin.types.annotations.PrimaryKey
+import org.mongodb.kbson.BsonObjectId
+import org.mongodb.kbson.ObjectId
+
+class DynamicDataEntity: RealmObject {
+ @PersistedName("_id")
+ @PrimaryKey
+ var id: ObjectId = BsonObjectId()
+
+ var name: String = ""
+
+ var configuration: RealmAny? = null
+}
diff --git a/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/ui/DynamicDataScreen.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/ui/DynamicDataScreen.kt
new file mode 100644
index 0000000..7a86eac
--- /dev/null
+++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/ui/DynamicDataScreen.kt
@@ -0,0 +1,95 @@
+package io.realm.appservicesusagesamples.dynamicdata.ui
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Divider
+import androidx.compose.material3.ElevatedCard
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import cafe.adriel.bonsai.core.Bonsai
+import cafe.adriel.bonsai.core.node.Branch
+import cafe.adriel.bonsai.core.node.Leaf
+import cafe.adriel.bonsai.core.tree.Tree
+import cafe.adriel.bonsai.core.tree.TreeScope
+import io.realm.appservicesusagesamples.dynamicdata.models.DynamicDataEntity
+import io.realm.kotlin.types.RealmAny
+import io.realm.kotlin.types.RealmDictionary
+import io.realm.kotlin.types.RealmList
+
+@Composable
+fun DynamicDataScreen(
+ viewModel: DynamicDataViewModel,
+) {
+ val entities: List by viewModel.instances.observeAsState(emptyList())
+
+ Column {
+ entities.forEach { item ->
+ Column {
+ ElevatedCard(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp, horizontal = 16.dp),
+ ) {
+ Text(
+ text = "Name: ${item.name}",
+ modifier =
+ Modifier
+ .padding(start = 4.dp)
+ .align(Alignment.CenterHorizontally),
+ )
+ Text(
+ text = "Id: ${item.id.toHexString()}",
+ modifier = Modifier
+ .padding(start = 4.dp)
+ .align(Alignment.CenterHorizontally),
+ )
+ Divider()
+ item.configuration?.let {
+ Bonsai(tree = Tree { RealmAnyNode("Configuration", it) })
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun TreeScope.RealmAnyNode(
+ key: String?,
+ element: RealmAny?,
+) {
+ when (element?.type) {
+ RealmAny.Type.LIST -> RealmAnyListNode("$key: ${element.type}", element.asList())
+ RealmAny.Type.DICTIONARY -> RealmAnyDictionaryNode(
+ "$key: ${element.type}",
+ element.asDictionary()
+ )
+
+ else -> RealmAnyPrimitiveNode("$key", element)
+ }
+}
+
+@Composable
+fun TreeScope.RealmAnyListNode(property: String?, element: RealmList) {
+ Branch(content = element, name = "$property") {
+ element.forEachIndexed { index, element -> RealmAnyNode(key = "$index", element = element) }
+ }
+}
+
+@Composable
+fun TreeScope.RealmAnyDictionaryNode(property: String?, element: RealmDictionary) {
+ Branch(content = element, name = "$property") {
+ element.forEach { (key, value) -> RealmAnyNode(key = "$key", element = value) }
+ }
+}
+
+@Composable
+fun TreeScope.RealmAnyPrimitiveNode(key: String?, element: RealmAny?) {
+ Leaf(content = element, name = "$key: $element")
+}
diff --git a/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/ui/DynamicDataViewModel.kt b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/ui/DynamicDataViewModel.kt
new file mode 100644
index 0000000..dfbba76
--- /dev/null
+++ b/AppServicesUsageSamples/demo/src/main/java/io/realm/appservicesusagesamples/dynamicdata/ui/DynamicDataViewModel.kt
@@ -0,0 +1,91 @@
+package io.realm.appservicesusagesamples.dynamicdata.ui
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import io.realm.appservicesusagesamples.dynamicdata.models.DynamicDataEntity
+import io.realm.kotlin.Realm
+import io.realm.kotlin.ext.query
+import io.realm.kotlin.ext.realmAnyDictionaryOf
+import io.realm.kotlin.ext.realmAnyListOf
+import io.realm.kotlin.ext.realmDictionaryOf
+import io.realm.kotlin.mongodb.App
+import io.realm.kotlin.mongodb.Credentials
+import io.realm.kotlin.mongodb.User
+import io.realm.kotlin.mongodb.sync.SyncConfiguration
+import io.realm.kotlin.types.RealmAny
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import org.mongodb.kbson.ObjectId
+
+class DynamicDataViewModel(
+ private val app: App,
+): ViewModel() {
+ lateinit var user: User
+ private lateinit var realm: Realm
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ app.login(Credentials.anonymous())
+ .let { user ->
+ this@DynamicDataViewModel.user = user
+
+ val syncConfig = SyncConfiguration
+ .Builder(app.currentUser!!, setOf(DynamicDataEntity::class))
+ .initialSubscriptions {
+ add(it.query())
+ }
+
+ .waitForInitialRemoteData()
+ .initialData {
+ if (this.query().find().isEmpty()) {
+ val client1 = copyToRealm(DynamicDataEntity().apply {
+ name = "Client 1"
+ configuration = RealmAny.Companion.create("Hello, World!")
+ })
+ copyToRealm(DynamicDataEntity().apply {
+ name = "Client 2"
+ configuration = realmAnyListOf(
+ realmDictionaryOf("key1" to realmAnyListOf(1, ObjectId(), "Nested collection")),
+ "String",
+ client1
+ )
+ })
+ copyToRealm(DynamicDataEntity().apply {
+ name = "Client 3"
+ configuration = realmAnyDictionaryOf(
+ "key1" to realmAnyListOf(
+ 1,
+ ObjectId(),
+ "Nested collection"
+ )
+ )
+ } )
+ }
+ }
+ .build()
+ Realm.deleteRealm(syncConfig)
+
+ realm = Realm.open(syncConfig)
+
+ val job = async {
+ realm.query()
+ .sort("name")
+ .asFlow()
+ .collect {
+ instances.postValue(it.list)
+ }
+ }
+
+ addCloseable {
+ job.cancel()
+ realm.close()
+ }
+ }
+ }
+ }
+
+ val instances: MutableLiveData> by lazy {
+ MutableLiveData>()
+ }
+}
diff --git a/AppServicesUsageSamples/gradle/libs.versions.toml b/AppServicesUsageSamples/gradle/libs.versions.toml
index c5df532..b38a033 100644
--- a/AppServicesUsageSamples/gradle/libs.versions.toml
+++ b/AppServicesUsageSamples/gradle/libs.versions.toml
@@ -14,6 +14,8 @@ androidxEspresso = "3.5.1"
androidxTestExt = "1.1.5"
kotlinxCoroutines = "1.6.4"
androidxTestRunner = "1.5.1"
+compose-plugin = "1.6.10"
+
[libraries]
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppCompat" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidxComposeBom" }
@@ -25,6 +27,7 @@ androidx-lifecycle-runtimeCompose = { group = "androidx.lifecycle", name = "life
androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
androidx-compose-ui-tooling-core = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
+bonsai = { group = "cafe.adriel.bonsai", name = "bonsai-core", version = "1.2.0" }
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
koin-android = { group = "io.insert-koin", name = "koin-android",version.ref = "koin"}
koin-androidx-compose = { group = "io.insert-koin", name = "koin-androidx-compose" ,version.ref = "koinCompose"}
@@ -41,3 +44,4 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
realm-kotlin = { id = "io.realm.kotlin", version.ref = "realm" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }