diff --git a/example/flutter/objectbox_demo/lib/main.dart b/example/flutter/objectbox_demo/lib/main.dart
index 3678d8d4b..4229a7fcc 100644
--- a/example/flutter/objectbox_demo/lib/main.dart
+++ b/example/flutter/objectbox_demo/lib/main.dart
@@ -1,11 +1,13 @@
+import 'dart:async';
+import 'dart:io';
+
import 'package:flutter/material.dart';
-import 'package:objectbox/objectbox.dart';
import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart';
-import 'objectbox.g.dart';
+import 'package:objectbox/objectbox.dart';
import 'package:objectbox/observable.dart';
-import 'dart:async';
-import 'dart:io';
+
+import 'objectbox.g.dart';
@Entity()
class Note {
diff --git a/example/flutter/objectbox_demo_sync/.gitignore b/example/flutter/objectbox_demo_sync/.gitignore
new file mode 100644
index 000000000..6285fbbd7
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/.gitignore
@@ -0,0 +1,38 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins*
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Web related
+lib/generated_plugin_registrant.dart
+
+# Exceptions to above rules.
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
+
+objectbox
diff --git a/example/flutter/objectbox_demo_sync/.metadata b/example/flutter/objectbox_demo_sync/.metadata
new file mode 100644
index 000000000..21ebdba99
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/.metadata
@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: a4d5266b769e92286a48f6b6714e538f2c4578dc
+ channel: master
+
+project_type: app
diff --git a/example/flutter/objectbox_demo_sync/README.md b/example/flutter/objectbox_demo_sync/README.md
new file mode 100644
index 000000000..d5141ddb3
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/README.md
@@ -0,0 +1,5 @@
+# objectbox_demo
+
+## Getting Started
+
+This project contains the Flutter version of the main example from the [objectbox-examples](https://github.com/objectbox/objectbox-examples) repository.
diff --git a/example/flutter/objectbox_demo_sync/android/.gitignore b/example/flutter/objectbox_demo_sync/android/.gitignore
new file mode 100644
index 000000000..bc2100d8f
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/.gitignore
@@ -0,0 +1,7 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
diff --git a/example/flutter/objectbox_demo_sync/android/app/build.gradle b/example/flutter/objectbox_demo_sync/android/app/build.gradle
new file mode 100644
index 000000000..97d10d66b
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/app/build.gradle
@@ -0,0 +1,66 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion 28
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+
+ defaultConfig {
+ applicationId "com.example.objectbox_demo"
+ minSdkVersion 16
+ targetSdkVersion 28
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ testImplementation 'junit:junit:4.12'
+ androidTestImplementation 'androidx.test:runner:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
+}
diff --git a/example/flutter/objectbox_demo_sync/android/app/src/debug/AndroidManifest.xml b/example/flutter/objectbox_demo_sync/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 000000000..75d39438e
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/example/flutter/objectbox_demo_sync/android/app/src/main/AndroidManifest.xml b/example/flutter/objectbox_demo_sync/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..fb9b3e129
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/flutter/objectbox_demo_sync/android/app/src/main/kotlin/com/example/objectbox_demo/MainActivity.kt b/example/flutter/objectbox_demo_sync/android/app/src/main/kotlin/com/example/objectbox_demo/MainActivity.kt
new file mode 100644
index 000000000..cc868d541
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/app/src/main/kotlin/com/example/objectbox_demo/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.example.objectbox_demo
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/example/flutter/objectbox_demo_sync/android/app/src/main/res/drawable/launch_background.xml b/example/flutter/objectbox_demo_sync/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 000000000..304732f88
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 000000000..db77bb4b7
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 000000000..17987b79b
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..09d439148
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..d5f1c8d34
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 000000000..4d6372eeb
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/example/flutter/objectbox_demo_sync/android/app/src/main/res/values/styles.xml b/example/flutter/objectbox_demo_sync/android/app/src/main/res/values/styles.xml
new file mode 100644
index 000000000..c745a5ef2
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/example/flutter/objectbox_demo_sync/android/app/src/profile/AndroidManifest.xml b/example/flutter/objectbox_demo_sync/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 000000000..75d39438e
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/example/flutter/objectbox_demo_sync/android/build.gradle b/example/flutter/objectbox_demo_sync/android/build.gradle
new file mode 100644
index 000000000..3100ad2d5
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+ ext.kotlin_version = '1.3.50'
+ repositories {
+ google()
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:3.5.0'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/example/flutter/objectbox_demo_sync/android/gradle.properties b/example/flutter/objectbox_demo_sync/android/gradle.properties
new file mode 100644
index 000000000..38c8d4544
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.enableR8=true
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/example/flutter/objectbox_demo_sync/android/gradle/wrapper/gradle-wrapper.properties b/example/flutter/objectbox_demo_sync/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..296b146b7
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
diff --git a/example/flutter/objectbox_demo_sync/android/settings.gradle b/example/flutter/objectbox_demo_sync/android/settings.gradle
new file mode 100644
index 000000000..5a2f14fb1
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/android/settings.gradle
@@ -0,0 +1,15 @@
+include ':app'
+
+def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
+
+def plugins = new Properties()
+def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
+if (pluginsFile.exists()) {
+ pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
+}
+
+plugins.each { name, path ->
+ def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
+ include ":$name"
+ project(":$name").projectDir = pluginDirectory
+}
diff --git a/example/flutter/objectbox_demo_sync/ios/.gitignore b/example/flutter/objectbox_demo_sync/ios/.gitignore
new file mode 100644
index 000000000..e96ef602b
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/.gitignore
@@ -0,0 +1,32 @@
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/example/flutter/objectbox_demo_sync/ios/Flutter/AppFrameworkInfo.plist b/example/flutter/objectbox_demo_sync/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 000000000..6b4c0f78a
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 8.0
+
+
diff --git a/example/flutter/objectbox_demo_sync/ios/Flutter/Debug.xcconfig b/example/flutter/objectbox_demo_sync/ios/Flutter/Debug.xcconfig
new file mode 100644
index 000000000..592ceee85
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Flutter/Debug.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/example/flutter/objectbox_demo_sync/ios/Flutter/Release.xcconfig b/example/flutter/objectbox_demo_sync/ios/Flutter/Release.xcconfig
new file mode 100644
index 000000000..592ceee85
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Flutter/Release.xcconfig
@@ -0,0 +1 @@
+#include "Generated.xcconfig"
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner.xcodeproj/project.pbxproj b/example/flutter/objectbox_demo_sync/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 000000000..886775291
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,518 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 46;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; };
+ 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; };
+ 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */,
+ 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */,
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */,
+ 3B80C3941E831B6300D905FE /* App.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B80C3931E831B6300D905FE /* App.framework */,
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEBA1CF902C7004384FC /* Flutter.framework */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 97C146F11CF9000F007C117D /* Supporting Files */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ 97C146F11CF9000F007C117D /* Supporting Files */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ name = "Supporting Files";
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1020;
+ ORGANIZATIONNAME = "The Chromium Authors";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 3.2";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin";
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.objectboxDemo;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 8.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.objectboxDemo;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ FRAMEWORK_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(PROJECT_DIR)/Flutter",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.objectboxDemo;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/flutter/objectbox_demo_sync/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..1d526a16e
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/flutter/objectbox_demo_sync/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 000000000..a28140cfd
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,91 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/flutter/objectbox_demo_sync/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 000000000..1d526a16e
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/AppDelegate.swift b/example/flutter/objectbox_demo_sync/ios/Runner/AppDelegate.swift
new file mode 100644
index 000000000..70693e4a8
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 000000000..d36b1fab2
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 000000000..dc9ada472
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 000000000..28c6bf030
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 000000000..2ccbfd967
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 000000000..f091b6b0b
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 000000000..4cde12118
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 000000000..d0ef06e7e
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 000000000..dcdc2306c
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 000000000..2ccbfd967
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 000000000..c8f9ed8f5
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 000000000..a6d6b8609
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 000000000..a6d6b8609
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 000000000..75b2d164a
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 000000000..c4df70d39
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 000000000..6a84f41e1
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 000000000..d0e1f5853
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 000000000..0bedcf2fd
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 000000000..9da19eaca
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 000000000..9da19eaca
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 000000000..9da19eaca
Binary files /dev/null and b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 000000000..89c2725b7
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/flutter/objectbox_demo_sync/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 000000000..f2e259c7c
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Base.lproj/Main.storyboard b/example/flutter/objectbox_demo_sync/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 000000000..f3c28516f
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Info.plist b/example/flutter/objectbox_demo_sync/ios/Runner/Info.plist
new file mode 100644
index 000000000..43cf18a9a
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner/Info.plist
@@ -0,0 +1,45 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ objectbox_demo
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
diff --git a/example/flutter/objectbox_demo_sync/ios/Runner/Runner-Bridging-Header.h b/example/flutter/objectbox_demo_sync/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 000000000..7335fdf90
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
\ No newline at end of file
diff --git a/example/flutter/objectbox_demo_sync/lib/main.dart b/example/flutter/objectbox_demo_sync/lib/main.dart
new file mode 100644
index 000000000..cf11f0c7c
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/lib/main.dart
@@ -0,0 +1,227 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:intl/intl.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:objectbox/objectbox.dart';
+import 'package:objectbox/observable.dart';
+
+import 'objectbox.g.dart';
+
+@Entity()
+@Sync()
+class Note {
+ @Id()
+ int id;
+
+ String text;
+ String comment;
+ int date;
+
+ Note();
+
+ Note.construct(this.text) {
+ date = DateTime.now().millisecondsSinceEpoch;
+ print('constructed date: $date');
+ }
+
+ String get dateFormat => DateFormat('dd.MM.yyyy hh:mm:ss')
+ .format(DateTime.fromMillisecondsSinceEpoch(date));
+}
+
+void main() => runApp(MyApp());
+
+class MyApp extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'OB Example',
+ theme: ThemeData(primarySwatch: Colors.blue),
+ home: MyHomePage(title: 'OB Example'),
+ );
+ }
+}
+
+class MyHomePage extends StatefulWidget {
+ MyHomePage({Key key, this.title}) : super(key: key);
+
+ final String title;
+
+ @override
+ _MyHomePageState createState() => _MyHomePageState();
+}
+
+class ViewModel {
+ Store _store;
+ Box _box;
+ Query _query;
+
+ ViewModel(Directory dir) {
+ _store = Store(getObjectBoxModel(), directory: dir.path + '/objectbox');
+ _box = Box(_store);
+
+ final dateProp = Note_.date;
+
+ _query = _box.query().order(dateProp, flags: Order.descending).build();
+
+ // TODO configure actual sync server address and authentication
+ // 10.0.2.2 is your host PC if an app is run in an Android emulator.
+ // 127.0.0.1 is your host PC if an app is run in an iOS simulator.
+ // For other options, see objectbox/lib/src/sync.dart
+ final syncClient =
+ Sync.client(_store, 'ws://10.0.2.2:9999', SyncCredentials.none());
+ syncClient.start();
+ }
+
+ void addNote(Note note) => _box.put(note);
+
+ void removeNote(Note note) => _box.remove(note.id);
+
+ // Note: using query.findStream() and sync.client() in the same app is
+ // currently not supported so this app is currently not working and only
+ // servers as an example on how and when to start a sync client.
+ // Stream> get queryStream => _query.findStream();
+ Stream> get queryStream => Stream>.empty();
+
+ List get allNotes => _query.find();
+
+ void dispose() {
+ _query.close();
+ _store.close();
+ }
+}
+
+class _MyHomePageState extends State {
+ final _noteInputController = TextEditingController();
+ final _listController = StreamController>(sync: true);
+ Stream> _stream;
+ ViewModel _vm;
+
+ void _addNote() {
+ if (_noteInputController.text.isEmpty) return;
+ _vm.addNote(Note.construct(_noteInputController.text));
+ _noteInputController.text = '';
+ }
+
+ @override
+ void initState() {
+ super.initState();
+
+ getApplicationDocumentsDirectory().then((dir) {
+ _vm = ViewModel(dir);
+ _stream = _listController.stream;
+
+ setState(() {});
+
+ _listController.add(_vm.allNotes);
+ _listController.addStream(_vm.queryStream);
+ });
+ }
+
+ @override
+ void dispose() {
+ _noteInputController.dispose();
+ _listController.close();
+ _vm.dispose();
+ super.dispose();
+ }
+
+ GestureDetector Function(BuildContext, int) _itemBuilder(List notes) {
+ return (BuildContext context, int index) {
+ return GestureDetector(
+ onTap: () => _vm.removeNote(notes[index]),
+ child: Row(
+ children: [
+ Expanded(
+ child: Container(
+ child: Padding(
+ padding:
+ EdgeInsets.symmetric(vertical: 18.0, horizontal: 10.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ notes[index].text,
+ style: TextStyle(
+ fontSize: 15.0,
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.only(top: 5.0),
+ child: Text(
+ 'Added on ${notes[index].dateFormat}',
+ style: TextStyle(
+ fontSize: 12.0,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ decoration: BoxDecoration(
+ border: Border(bottom: BorderSide(color: Colors.black12))),
+ ),
+ ),
+ ],
+ ),
+ );
+ };
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ title: Text(widget.title),
+ ),
+ body: Column(children: [
+ Padding(
+ padding: EdgeInsets.all(20.0),
+ child: Row(
+ children: [
+ Expanded(
+ child: Column(
+ children: [
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: 10.0),
+ child: TextField(
+ decoration:
+ InputDecoration(hintText: 'Enter a new note'),
+ controller: _noteInputController,
+ onSubmitted: (value) => _addNote(),
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.only(top: 10.0, right: 10.0),
+ child: Align(
+ alignment: Alignment.centerRight,
+ child: Text(
+ 'Tap a note to remove it',
+ style: TextStyle(
+ fontSize: 11.0,
+ color: Colors.grey,
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ )
+ ],
+ ),
+ ),
+ Expanded(
+ child: StreamBuilder>(
+ stream: _stream,
+ builder: (context, snapshot) {
+ return ListView.builder(
+ shrinkWrap: true,
+ padding: EdgeInsets.symmetric(horizontal: 20.0),
+ itemCount: snapshot.hasData ? snapshot.data.length : 0,
+ itemBuilder: _itemBuilder(snapshot.data));
+ }))
+ ]),
+ );
+ }
+}
diff --git a/example/flutter/objectbox_demo_sync/lib/objectbox-model.json b/example/flutter/objectbox_demo_sync/lib/objectbox-model.json
new file mode 100644
index 000000000..61204203a
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/lib/objectbox-model.json
@@ -0,0 +1,47 @@
+{
+ "_note1": "KEEP THIS FILE! Check it into a version control system (VCS) like git.",
+ "_note2": "ObjectBox manages crucial IDs for your object model. See docs for details.",
+ "_note3": "If you have VCS merge conflicts, you must resolve them according to ObjectBox docs.",
+ "entities": [
+ {
+ "id": "1:2802681814019499133",
+ "lastPropertyId": "4:6451339597165131221",
+ "name": "Note",
+ "flags": 2,
+ "properties": [
+ {
+ "id": "1:3178873177797362769",
+ "name": "id",
+ "type": 6,
+ "flags": 1
+ },
+ {
+ "id": "2:4285343053028527696",
+ "name": "text",
+ "type": 9
+ },
+ {
+ "id": "3:2606273611209948020",
+ "name": "comment",
+ "type": 9
+ },
+ {
+ "id": "4:6451339597165131221",
+ "name": "date",
+ "type": 6
+ }
+ ]
+ }
+ ],
+ "lastEntityId": "1:2802681814019499133",
+ "lastIndexId": "0:0",
+ "lastRelationId": "0:0",
+ "lastSequenceId": "0:0",
+ "modelVersion": 5,
+ "modelVersionParserMinimum": 5,
+ "retiredEntityUids": [],
+ "retiredIndexUids": [],
+ "retiredPropertyUids": [],
+ "retiredRelationUids": [],
+ "version": 1
+}
\ No newline at end of file
diff --git a/example/flutter/objectbox_demo_sync/pubspec.yaml b/example/flutter/objectbox_demo_sync/pubspec.yaml
new file mode 100644
index 000000000..a43f24ccf
--- /dev/null
+++ b/example/flutter/objectbox_demo_sync/pubspec.yaml
@@ -0,0 +1,33 @@
+name: objectbox_demo
+description: An example project for the objectbox-dart binding.
+version: 0.3.0+1
+
+environment:
+ sdk: ">=2.1.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ cupertino_icons: ^0.1.2
+ path_provider: any
+ intl: any
+ objectbox: ^0.8.1
+ objectbox_sync_flutter_libs: ^0.8.1
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ build_runner: ^1.0.0
+ objectbox_generator: ^0.8.1
+
+flutter:
+ uses-material-design: true
+
+# Note: these overrides are only for ObjectBox internal development, don't use them in your app.
+dependency_overrides:
+ objectbox:
+ path: ../../..
+ objectbox_generator:
+ path: ../../../generator
+ objectbox_sync_flutter_libs:
+ path: ../../../sync_flutter_libs
\ No newline at end of file
diff --git a/flutter_libs/android/build.gradle b/flutter_libs/android/build.gradle
index 6dcc6bbc2..36c67690e 100644
--- a/flutter_libs/android/build.gradle
+++ b/flutter_libs/android/build.gradle
@@ -12,5 +12,5 @@ android {
dependencies {
// https://github.com/objectbox/objectbox-java/releases
- implementation "io.objectbox:objectbox-android:2.7.1"
+ implementation "io.objectbox:objectbox-android:2.8.0"
}
diff --git a/generator/integration-tests/basics/1.dart b/generator/integration-tests/basics/1.dart
index 0a42f411b..bcd2fc74e 100644
--- a/generator/integration-tests/basics/1.dart
+++ b/generator/integration-tests/basics/1.dart
@@ -4,10 +4,13 @@ import 'lib/objectbox.g.dart';
import 'package:test/test.dart';
import '../test_env.dart';
import '../common.dart';
+import 'package:objectbox/src/bindings/constants.dart';
void main() {
TestEnv env;
+ final jsonModel = readModelJson('lib');
final defs = getObjectBoxModel();
+ final model = defs.model;
setUp(() {
env = TestEnv(defs);
@@ -17,11 +20,19 @@ void main() {
env.close();
});
- commonModelTests(defs, readModelJson('lib'));
+ commonModelTests(defs, jsonModel);
test('project must be generated properly', () {
expect(TestEnv.dir.existsSync(), true);
expect(File('lib/objectbox.g.dart').existsSync(), true);
expect(File('lib/objectbox-model.json').existsSync(), true);
});
+
+ test('sync annotation', () {
+ expect(entity(model, 'A').flags, equals(0));
+ expect(entity(jsonModel, 'A').flags, equals(0));
+
+ expect(entity(model, 'D').flags, equals(OBXEntityFlag.SYNC_ENABLED));
+ expect(entity(jsonModel, 'D').flags, equals(OBXEntityFlag.SYNC_ENABLED));
+ });
}
diff --git a/generator/integration-tests/basics/lib/lib.dart b/generator/integration-tests/basics/lib/lib.dart
index 279de42bc..99aad18c8 100644
--- a/generator/integration-tests/basics/lib/lib.dart
+++ b/generator/integration-tests/basics/lib/lib.dart
@@ -18,3 +18,12 @@ class B {
B();
}
+
+@Entity()
+@Sync()
+class D {
+ @Id()
+ int id;
+
+ D();
+}
diff --git a/generator/integration-tests/common.dart b/generator/integration-tests/common.dart
index 7585f6590..8509e1bab 100644
--- a/generator/integration-tests/common.dart
+++ b/generator/integration-tests/common.dart
@@ -69,3 +69,7 @@ commonModelTests(ModelDefinition defs, ModelInfo jsonModel) {
// testLastId(defs.model.lastRelationId, defs.model.entities.map((el) => ...), jsonModel.retiredRelationUids);
// });
}
+
+ModelEntity entity(ModelInfo model, String name) {
+ return model.entities.firstWhere((ModelEntity e) => e.name == name);
+}
diff --git a/generator/lib/src/code_builder.dart b/generator/lib/src/code_builder.dart
index 607c688a7..a8c8dfcec 100644
--- a/generator/lib/src/code_builder.dart
+++ b/generator/lib/src/code_builder.dart
@@ -146,28 +146,30 @@ class CodeBuilder extends Builder {
IdUid mergeEntity(ModelInfo modelInfo, ModelEntity entity) {
// 'readEntity' only contains the entity info directly read from the annotations and Dart source (i.e. with missing ID, lastPropertyId etc.)
// 'entityInModel' is the entity from the model with all correct id/uid, lastPropertyId etc.
- final entityInModel = modelInfo.findSameEntity(entity);
+ var entityInModel = modelInfo.findSameEntity(entity);
if (entityInModel == null) {
log.info('Found new entity ${entity.name}');
// in case the entity is created (i.e. when its given UID or name that does not yet exist), we are done, as nothing needs to be merged
- final createdEntity = modelInfo.addEntity(entity);
- return createdEntity.id;
- }
-
- entityInModel.name = entity.name;
+ entityInModel = modelInfo.addEntity(entity);
- // here, the entity was found already and entityInModel and readEntity might differ, i.e. conflicts need to be resolved, so merge all properties first
- entity.properties.forEach((p) => mergeProperty(entityInModel, p));
-
- // then remove all properties not present anymore in readEntity
- entityInModel.properties
- .where((p) => entity.findSameProperty(p) == null)
- .forEach((p) {
- log.warning(
- 'Property ${entity.name}.${p.name}(${p.id.toString()}) not found in the code, removing from the model');
- entityInModel.removeProperty(p);
- });
+ } else {
+ entityInModel.name = entity.name;
+ entityInModel.flags = entity.flags;
+
+ // here, the entity was found already and entityInModel and readEntity might differ, i.e. conflicts need to be resolved, so merge all properties first
+ entity.properties.forEach((p) => mergeProperty(entityInModel, p));
+
+ // then remove all properties not present anymore in readEntity
+ entityInModel.properties
+ .where((p) => entity.findSameProperty(p) == null)
+ .forEach((p) {
+ log.warning(
+ 'Property ${entity.name}.${p.name}(${p.id
+ .toString()}) not found in the code, removing from the model');
+ entityInModel.removeProperty(p);
+ });
+ }
return entityInModel.id;
}
diff --git a/generator/lib/src/entity_resolver.dart b/generator/lib/src/entity_resolver.dart
index dd533b62d..4690fb1f6 100644
--- a/generator/lib/src/entity_resolver.dart
+++ b/generator/lib/src/entity_resolver.dart
@@ -21,6 +21,7 @@ class EntityResolver extends Builder {
final _propertyChecker = const TypeChecker.fromRuntime(obx.Property);
final _idChecker = const TypeChecker.fromRuntime(obx.Id);
final _transientChecker = const TypeChecker.fromRuntime(obx.Transient);
+ final _syncChecker = const TypeChecker.fromRuntime(obx.Sync);
@override
FutureOr build(BuildStep buildStep) async {
@@ -49,16 +50,22 @@ class EntityResolver extends Builder {
throw InvalidGenerationSourceError(
"in target ${elementBare.name}: annotated element isn't a class");
}
+
var element = elementBare as ClassElement;
// process basic entity (note that allModels.createEntity is not used, as the entity will be merged)
- final readEntity = ModelEntity(IdUid.empty(), null, element.name, [], null);
+ final entity = ModelEntity(IdUid.empty(), null, element.name, 0, [], null);
var entityUid = annotation.read('uid');
if (entityUid != null && !entityUid.isNull) {
- readEntity.id.uid = entityUid.intValue;
+ entity.id.uid = entityUid.intValue;
+ }
+
+ if (_syncChecker.hasAnnotationOfExact(element)) {
+ entity.flags |= OBXEntityFlag.SYNC_ENABLED;
}
- log.info('entity ${readEntity.name}(${readEntity.id})');
+ log.info('entity ${entity.name}(${entity.id}), sync=' +
+ (entity.hasFlag(OBXEntityFlag.SYNC_ENABLED) ? 'ON' : 'OFF'));
// getters, ... (anything else?)
final readOnlyFields = {};
@@ -134,9 +141,9 @@ class EntityResolver extends Builder {
// create property (do not use readEntity.createProperty in order to avoid generating new ids)
final prop =
- ModelProperty(IdUid.empty(), f.name, fieldType, flags, readEntity);
+ ModelProperty(IdUid.empty(), f.name, fieldType, flags, entity);
if (propUid != null) prop.id.uid = propUid;
- readEntity.properties.add(prop);
+ entity.properties.add(prop);
log.info(
' property ${prop.name}(${prop.id}) type:${prop.type} flags:${prop.flags}');
@@ -148,6 +155,6 @@ class EntityResolver extends Builder {
'in target ${elementBare.name}: has no properties annotated with @Id');
}
- return readEntity;
+ return entity;
}
}
diff --git a/lib/integration_test.dart b/lib/integration_test.dart
index 5442ffdbc..78f667cdf 100644
--- a/lib/integration_test.dart
+++ b/lib/integration_test.dart
@@ -22,7 +22,7 @@ class IntegrationTest {
final property = ModelProperty(
IdUid(1, int64_max - 1), 'id', OBXPropertyType.Long, 0, null);
final entity =
- ModelEntity(IdUid(1, int64_max), null, 'entity', [], modelInfo);
+ ModelEntity(IdUid(1, int64_max), null, 'entity', 0, [], modelInfo);
property.entity = entity;
entity.properties.add(property);
entity.lastPropertyId = property.id;
diff --git a/lib/objectbox.dart b/lib/objectbox.dart
index e3b693bcf..ac65ae0c7 100644
--- a/lib/objectbox.dart
+++ b/lib/objectbox.dart
@@ -12,3 +12,4 @@ export 'src/model.dart';
export 'src/modelinfo/index.dart';
export 'src/query/query.dart';
export 'src/store.dart';
+export 'src/sync.dart';
diff --git a/lib/src/annotations.dart b/lib/src/annotations.dart
index aff1fd12b..fb5c61500 100644
--- a/lib/src/annotations.dart
+++ b/lib/src/annotations.dart
@@ -1,5 +1,6 @@
class Entity {
final int uid;
+
const Entity({this.uid});
}
@@ -13,14 +14,22 @@ class Entity {
/// Use OBXPropertyType and OBXPropertyFlag values, resp. for type and flag.
class Property {
final int uid, type, flag;
+
const Property({this.type, this.flag, this.uid});
}
class Id {
final int uid;
+
const Id({this.uid});
}
class Transient {
const Transient();
}
+
+/// See Sync() in sync.dart.
+/// Defining a class with the same name here would cause a duplicate export.
+// class Sync {
+// const Sync();
+// }
diff --git a/lib/src/bindings/bindings.dart b/lib/src/bindings/bindings.dart
index 16d852586..6f32eba0d 100644
--- a/lib/src/bindings/bindings.dart
+++ b/lib/src/bindings/bindings.dart
@@ -43,6 +43,7 @@ class _ObjectBoxBindings {
Pointer Function(Pointer model) obx_model_error_message;
int Function(Pointer model, Pointer name, int entity_id,
int entity_uid) obx_model_entity;
+ int Function(Pointer model, int flags) obx_model_entity_flags;
int Function(Pointer model, Pointer name, int type,
int property_id, int property_uid) obx_model_property;
int Function(Pointer model, int flags) obx_model_property_flags;
@@ -213,6 +214,19 @@ class _ObjectBoxBindings {
obx_bytes_array_t obx_bytes_array;
obx_bytes_array_set_t obx_bytes_array_set;
+ // Sync
+ int Function() obx_sync_available;
+ obx_sync_native_t obx_sync;
+ obx_fn_nullary_dart obx_sync_close;
+ obx_sync_credentials_dart_t obx_sync_credentials;
+ obx_fn_nullary_dart obx_sync_state;
+ obx_fn_unary_dart obx_sync_request_updates_mode;
+ obx_fn_nullary_dart obx_sync_start;
+ obx_fn_nullary_dart obx_sync_stop;
+ obx_fn_unary_dart obx_sync_updates_request;
+ obx_fn_nullary_dart obx_sync_updates_cancel;
+ obx_fn_binary_dart> obx_sync_outgoing_message_count;
+
// TODO return .asFunction() -> requires properly determined static return type
Pointer> _fn(String name) {
return lib.lookup>(name);
@@ -297,11 +311,18 @@ class _ObjectBoxBindings {
.asFunction();
obx_model_entity =
_fn('obx_model_entity').asFunction();
+
+ // TODO remove try-catch after an update to objectbox-c v0.11.0
+ try {
+ obx_model_entity_flags =
+ _fn('obx_model_entity_flags').asFunction();
+ } catch (e) {
+ obx_model_entity_flags = (_, __) => 0;
+ }
obx_model_property =
_fn('obx_model_property').asFunction();
obx_model_property_flags =
- _fn('obx_model_property_flags')
- .asFunction();
+ _fn('obx_model_property_flags').asFunction();
obx_model_entity_last_property_id =
_fn(
'obx_model_entity_last_property_id')
@@ -596,6 +617,42 @@ class _ObjectBoxBindings {
obx_bytes_array_set =
_fn>('obx_bytes_array_set')
.asFunction();
+
+ // Sync
+ // TODO remove try-catch after an update to objectbox-c v0.11.0
+ try {
+ obx_sync_available =
+ _fn('obx_sync_available').asFunction();
+ } catch (e) {
+ obx_sync_available = () => 0;
+ }
+ try {
+ obx_sync = _fn('obx_sync').asFunction();
+ obx_sync_close =
+ _fn('obx_sync_close').asFunction();
+ obx_sync_credentials =
+ _fn('obx_sync_credentials')
+ .asFunction();
+ obx_sync_state =
+ _fn('obx_sync_state').asFunction();
+ obx_sync_request_updates_mode =
+ _fn>('obx_sync_request_updates_mode')
+ .asFunction();
+ obx_sync_start =
+ _fn('obx_sync_start').asFunction();
+ obx_sync_stop = _fn('obx_sync_stop').asFunction();
+ obx_sync_updates_request =
+ _fn>('obx_sync_updates_request')
+ .asFunction();
+ obx_sync_updates_cancel =
+ _fn('obx_sync_updates_cancel').asFunction();
+ obx_sync_outgoing_message_count =
+ _fn>>(
+ 'obx_sync_outgoing_message_count')
+ .asFunction();
+ } catch (e) {
+ // sync functions may be undefined when in non-sync lib
+ }
}
}
diff --git a/lib/src/bindings/constants.dart b/lib/src/bindings/constants.dart
index 741f53ac1..75765490a 100644
--- a/lib/src/bindings/constants.dart
+++ b/lib/src/bindings/constants.dart
@@ -14,6 +14,11 @@ class OBXPropertyType {
static const int StringVector = 30;
}
+// see objectbox.h for more info
+class OBXEntityFlag {
+ static const int SYNC_ENABLED = 2;
+}
+
// see objectbox.h for more info
class OBXPropertyFlag {
static const int ID = 1;
@@ -91,3 +96,43 @@ class OBXError {
/// A requested schema object (e.g. entity or property) was not found in the schema
static const int OBX_ERROR_SCHEMA_OBJECT_NOT_FOUND = 10503;
}
+
+class OBXSyncCredentialsType {
+ static const int NONE = 0;
+ static const int SHARED_SECRET = 1;
+ static const int GOOGLE_AUTH = 2;
+}
+
+// TODO sync prefix
+class OBXRequestUpdatesMode {
+ /// no updates by default, obx_sync_updates_request() must be called manually
+ static const int MANUAL = 0;
+
+ /// same as calling obx_sync_updates_request(sync, TRUE)
+ /// default mode unless overridden by obx_sync_request_updates_mode
+ static const int AUTO = 1;
+
+ /// same as calling obx_sync_updates_request(sync, FALSE)
+ static const int AUTO_NO_PUSHES = 2;
+}
+
+class OBXSyncState {
+ static const int CREATED = 1;
+ static const int STARTED = 2;
+ static const int CONNECTED = 3;
+ static const int LOGGED_IN = 4;
+ static const int DISCONNECTED = 5;
+ static const int STOPPED = 6;
+ static const int DEAD = 7;
+}
+
+class OBXSyncCode {
+ static const int OK = 20;
+ static const int REQ_REJECTED = 40;
+ static const int CREDENTIALS_REJECTED = 43;
+ static const int UNKNOWN = 50;
+ static const int AUTH_UNREACHABLE = 53;
+ static const int BAD_VERSION = 55;
+ static const int CLIENT_ID_TAKEN = 61;
+ static const int TX_VIOLATED_UNIQUE = 71;
+}
diff --git a/lib/src/bindings/signatures.dart b/lib/src/bindings/signatures.dart
index a0bbf70f8..c7601eb29 100644
--- a/lib/src/bindings/signatures.dart
+++ b/lib/src/bindings/signatures.dart
@@ -6,6 +6,19 @@ import 'structs.dart';
// ignore_for_file: non_constant_identifier_names
+// typedefs for common signatures for different "classes", like store, box, ...
+// obx_err fn(objectPtr)
+typedef obx_fn_nullary_native = Int32 Function(Pointer obj);
+typedef obx_fn_nullary_dart = int Function(Pointer obj);
+// obx_err fn(void* objectPtr, Arg1 arg)
+typedef obx_fn_unary_native = Int32 Function(Pointer obj, Arg1 arg);
+typedef obx_fn_unary_dart = int Function(Pointer obj, Arg1 arg);
+// obx_err fn(void* objectPtr, Arg1 arg1, Arg2 arg2)
+typedef obx_fn_binary_native = Int32 Function(
+ Pointer obj, Arg1 arg1, Arg2 arg2);
+typedef obx_fn_binary_dart = int Function(
+ Pointer obj, Arg1 arg1, Arg2 arg2);
+
// common functions
typedef obx_version_native_t = Void Function(
Pointer major, Pointer minor, Pointer patch);
@@ -35,7 +48,7 @@ typedef obx_model_entity_native_t = Int32 Function(Pointer model,
Pointer name, Uint32 entity_id, Uint64 entity_uid);
typedef obx_model_property_native_t = Int32 Function(Pointer model,
Pointer name, Uint32 type, Uint32 property_id, Uint64 property_uid);
-typedef obx_model_property_flags_native_t = Int32 Function(
+typedef obx_model_flags_native_t = Int32 Function(
Pointer model, Uint32 flags);
typedef obx_model_entity_last_property_id_native_t = Int32 Function(
Pointer model, Uint32 property_id, Uint64 property_uid);
@@ -266,3 +279,12 @@ typedef obx_bytes_array_set_t = Ret Function(
obx_qb_param_alias_dart_t obx_qb_param_alias;
*/
+
+// Sync
+typedef obx_sync_available_native_t = Uint8 Function();
+typedef obx_sync_native_t = Pointer Function(
+ Pointer store, Pointer serverUri);
+typedef obx_sync_credentials_native_t = Int32 Function(
+ Pointer sync, Int32 type, Pointer data, IntPtr size);
+typedef obx_sync_credentials_dart_t = int Function(
+ Pointer sync, int type, Pointer data, int size);
diff --git a/lib/src/model.dart b/lib/src/model.dart
index e6ababf2b..79180d870 100644
--- a/lib/src/model.dart
+++ b/lib/src/model.dart
@@ -46,6 +46,10 @@ class Model {
free(name);
}
+ if (entity.flags != 0) {
+ _check(bindings.obx_model_entity_flags(_cModel, entity.flags));
+ }
+
// add all properties
entity.properties.forEach(addProperty);
diff --git a/lib/src/modelinfo/modelentity.dart b/lib/src/modelinfo/modelentity.dart
index c7240cd44..4db18dbb5 100644
--- a/lib/src/modelinfo/modelentity.dart
+++ b/lib/src/modelinfo/modelentity.dart
@@ -9,6 +9,7 @@ import 'package:objectbox/src/bindings/constants.dart';
class ModelEntity {
IdUid id, lastPropertyId;
String name;
+ int flags;
List properties;
ModelProperty idProperty;
ModelInfo _model;
@@ -16,8 +17,8 @@ class ModelEntity {
ModelInfo get model =>
(_model == null) ? throw Exception('model is null') : _model;
- ModelEntity(
- this.id, this.lastPropertyId, this.name, this.properties, this._model) {
+ ModelEntity(this.id, this.lastPropertyId, this.name, this.flags,
+ this.properties, this._model) {
validate();
}
@@ -27,6 +28,7 @@ class ModelEntity {
id = IdUid.fromString(data['id']);
lastPropertyId = IdUid.fromString(data['lastPropertyId']);
name = data['name'];
+ flags = data['flags'] ?? 0;
properties = data['properties']
.map((p) => ModelProperty.fromMap(p, this, check: check))
.toList();
@@ -83,6 +85,7 @@ class ModelEntity {
ret['lastPropertyId'] =
lastPropertyId == null ? null : lastPropertyId.toString();
ret['name'] = name;
+ if (flags != 0) ret['flags'] = flags;
ret['properties'] = properties.map((p) => p.toMap()).toList();
return ret;
}
@@ -150,4 +153,8 @@ class ModelEntity {
}
return false;
}
+
+ bool hasFlag(int flag) {
+ return (flags & flag) == flag;
+ }
}
diff --git a/lib/src/modelinfo/modelinfo.dart b/lib/src/modelinfo/modelinfo.dart
index 3b42ea895..7f437a8f8 100644
--- a/lib/src/modelinfo/modelinfo.dart
+++ b/lib/src/modelinfo/modelinfo.dart
@@ -183,7 +183,7 @@ class ModelInfo {
}
final uniqueUid = uid == 0 ? generateUid() : uid;
- var entity = ModelEntity(IdUid(id, uniqueUid), null, name, [], this);
+ var entity = ModelEntity(IdUid(id, uniqueUid), null, name, 0, [], this);
entities.add(entity);
lastEntityId = entity.id;
return entity;
diff --git a/lib/src/modelinfo/modelproperty.dart b/lib/src/modelinfo/modelproperty.dart
index 717014750..032bb60b6 100644
--- a/lib/src/modelinfo/modelproperty.dart
+++ b/lib/src/modelinfo/modelproperty.dart
@@ -42,4 +42,8 @@ class ModelProperty {
bool containsUid(int searched) {
return id.uid == searched;
}
+
+ bool hasFlag(int flag) {
+ return (flags & flag) == flag;
+ }
}
diff --git a/lib/src/observable.dart b/lib/src/observable.dart
index e081e515c..4250305d0 100644
--- a/lib/src/observable.dart
+++ b/lib/src/observable.dart
@@ -35,6 +35,8 @@ class _Observable {
}
static void subscribe(Store store) {
+ syncOrObserversExclusive.mark(store);
+
final callback = Pointer.fromFunction(_anyCallback);
final storePtr = store.ptr;
_anyObserver[storePtr.address] =
@@ -53,18 +55,19 @@ class _Observable {
StoreCloseObserver.removeListener(store, _anyObserver[storeAddress]);
bindings.obx_observer_close(_anyObserver[storeAddress]);
_anyObserver.remove(storeAddress);
+ syncOrObserversExclusive.unmark(store);
}
+
+ static bool isSubscribed(Store store) =>
+ _Observable._anyObserver.containsKey(store.ptr.address);
}
extension Streamable on Query {
void _setup() {
- final storePtr = store.ptr;
-
- if (!_Observable._anyObserver.containsKey(storePtr)) {
+ if (!_Observable.isSubscribed(store)) {
_Observable.subscribe(store);
}
-
- final storeAddress = storePtr.address;
+ final storeAddress = store.ptr.address;
_Observable._any[storeAddress] ??= {};
_Observable._any[storeAddress][entityId] ??= (u, _, __) {
diff --git a/lib/src/store.dart b/lib/src/store.dart
index 1f63dc64c..9c44170c1 100644
--- a/lib/src/store.dart
+++ b/lib/src/store.dart
@@ -6,6 +6,7 @@ import 'modelinfo/index.dart';
import 'model.dart';
import 'common.dart';
import 'util.dart';
+import 'sync.dart';
enum TxMode {
Read,
@@ -127,6 +128,10 @@ class Store {
}
}
+ /// Return an existing SyncClient associated with the store or null if not available.
+ /// See [Sync.client()] to create one first.
+ SyncClient syncClient() => SyncClientsStorage[this];
+
/// The low-level pointer to this store.
Pointer get ptr => _cStore;
}
diff --git a/lib/src/sync.dart b/lib/src/sync.dart
new file mode 100644
index 000000000..61d26374b
--- /dev/null
+++ b/lib/src/sync.dart
@@ -0,0 +1,245 @@
+import 'dart:ffi';
+import 'dart:typed_data' show Uint8List;
+import 'dart:convert' show utf8;
+
+import 'package:ffi/ffi.dart';
+
+import 'store.dart';
+import 'util.dart';
+import 'bindings/bindings.dart';
+import 'bindings/constants.dart';
+import 'bindings/helpers.dart';
+import 'bindings/structs.dart';
+
+/// Credentials used to authenticate a sync client against a server.
+class SyncCredentials {
+ final int _type;
+ final Uint8List _data;
+
+ Uint8List get data => _data;
+
+ SyncCredentials(this._type, this._data);
+
+ SyncCredentials._(this._type, String data)
+ : _data = Uint8List.fromList(utf8.encode(data));
+
+ SyncCredentials.none()
+ : _type = OBXSyncCredentialsType.NONE,
+ _data = Uint8List(0);
+
+ SyncCredentials.sharedSecretUint8List(this._data)
+ : _type = OBXSyncCredentialsType.SHARED_SECRET;
+
+ SyncCredentials.sharedSecretString(String data)
+ : this._(OBXSyncCredentialsType.SHARED_SECRET, data);
+
+ SyncCredentials.googleAuthUint8List(this._data)
+ : _type = OBXSyncCredentialsType.GOOGLE_AUTH;
+
+ SyncCredentials.googleAuthString(String data)
+ : this._(OBXSyncCredentialsType.GOOGLE_AUTH, data);
+}
+
+enum SyncState {
+ unknown,
+ created,
+ started,
+ connected,
+ loggedIn,
+ disconnected,
+ stopped,
+ dead
+}
+
+enum SyncRequestUpdatesMode {
+ /// no updates by default, [SyncClient.requestUpdates()] must be called manually
+ manual,
+
+ /// same as calling [SyncClient.requestUpdates(true)]
+ /// default mode unless overridden by [SyncClient.setRequestUpdatesMode()]
+ auto,
+
+ /// same as calling [SyncClient.requestUpdates(false)]
+ autoNoPushes
+}
+
+/// Sync client is used to provide ObjectBox Sync client capabilities to your application.
+class SyncClient {
+ final Store _store;
+ Pointer _cSync;
+
+ /// The low-level pointer to this box.
+ Pointer get ptr => (_cSync.address != 0)
+ ? _cSync
+ : throw Exception('SyncClient already closed');
+
+ /// Creates a sync client associated with the given store and options.
+ /// This does not initiate any connection attempts yet: call start() to do so.
+ SyncClient(this._store, String serverUri, SyncCredentials creds) {
+ if (!Sync.isAvailable()) {
+ throw Exception(
+ 'Sync is not available in the loaded ObjectBox runtime library. '
+ 'Please visit https://objectbox.io/sync/ for options.');
+ }
+
+ final cServerUri = Utf8.toUtf8(serverUri);
+ try {
+ _cSync = checkObxPtr(bindings.obx_sync(_store.ptr, cServerUri),
+ 'failed to create sync client');
+ } finally {
+ free(cServerUri);
+ }
+
+ setCredentials(creds);
+ }
+
+ /// Closes and cleans up all resources used by this sync client.
+ /// It can no longer be used afterwards, make a new sync client instead.
+ /// Does nothing if this sync client has already been closed.
+ void close() {
+ final err = bindings.obx_sync_close(_cSync);
+ _cSync = nullptr;
+ SyncClientsStorage.remove(_store);
+ StoreCloseObserver.removeListener(_store, this);
+ syncOrObserversExclusive.unmark(_store);
+ checkObx(err);
+ }
+
+ /// Returns if this sync client is closed and can no longer be used.
+ bool isClosed() {
+ return _cSync.address == 0;
+ }
+
+ /// Gets the current sync client state.
+ SyncState state() {
+ final state = bindings.obx_sync_state(ptr);
+ switch (state) {
+ case OBXSyncState.CREATED:
+ return SyncState.created;
+ case OBXSyncState.STARTED:
+ return SyncState.started;
+ case OBXSyncState.CONNECTED:
+ return SyncState.connected;
+ case OBXSyncState.LOGGED_IN:
+ return SyncState.loggedIn;
+ case OBXSyncState.DISCONNECTED:
+ return SyncState.disconnected;
+ case OBXSyncState.STOPPED:
+ return SyncState.stopped;
+ case OBXSyncState.DEAD:
+ return SyncState.dead;
+ default:
+ return SyncState.unknown;
+ }
+ }
+
+ /// Configure authentication credentials.
+ /// The accepted [SyncCredentials] type depends on your sync-server configuration.
+ void setCredentials(SyncCredentials creds) {
+ final cCreds = OBX_bytes.managedCopyOf(creds._data);
+ try {
+ checkObx(bindings.obx_sync_credentials(
+ ptr,
+ creds._type,
+ creds._type == OBXSyncCredentialsType.NONE ? nullptr : cCreds.ref.ptr,
+ cCreds.ref.length));
+ } finally {
+ OBX_bytes.freeManaged(cCreds);
+ }
+ }
+
+ /// Configures how sync updates are received from the server.
+ /// If automatic sync updates are turned off, they will need to be requested manually.
+ void setRequestUpdatesMode(SyncRequestUpdatesMode mode) {
+ int cMode;
+ switch (mode) {
+ case SyncRequestUpdatesMode.manual:
+ cMode = OBXRequestUpdatesMode.MANUAL;
+ break;
+ case SyncRequestUpdatesMode.auto:
+ cMode = OBXRequestUpdatesMode.AUTO;
+ break;
+ case SyncRequestUpdatesMode.autoNoPushes:
+ cMode = OBXRequestUpdatesMode.AUTO_NO_PUSHES;
+ break;
+ default:
+ throw Exception('Unknown mode argument: ' + mode.toString());
+ }
+ checkObx(bindings.obx_sync_request_updates_mode(ptr, cMode));
+ }
+
+ /// Once the sync client is configured, you can "start" it to initiate synchronization.
+ /// This method triggers communication in the background and will return immediately.
+ /// If the synchronization destination is reachable, this background thread will connect to the server,
+ /// log in (authenticate) and, depending on "update request mode", start syncing data.
+ /// If the device, network or server is currently offline, connection attempts will be retried later using
+ /// increasing backoff intervals.
+ /// If you haven't set the credentials in the options during construction, call [setCredentials()] before start().
+ void start() {
+ checkObx(bindings.obx_sync_start(ptr));
+ }
+
+ /// Stops this sync client. Does nothing if it is already stopped.
+ void stop() {
+ checkObx(bindings.obx_sync_stop(ptr));
+ }
+
+ /// Request updates since we last synchronized our database.
+ /// Additionally, you can subscribe for future pushes from the server, to let
+ /// it send us future updates as they come in.
+ /// Call [cancelUpdates()] to stop the updates.
+ bool requestUpdates(bool subscribeForFuturePushes) {
+ return checkObxSuccess(bindings.obx_sync_updates_request(
+ ptr, subscribeForFuturePushes ? 1 : 0));
+ }
+
+ /// Cancel updates from the server so that it will stop sending updates.
+ /// See also [requestUpdates()].
+ bool cancelUpdates() {
+ return checkObxSuccess(bindings.obx_sync_updates_cancel(ptr));
+ }
+
+ /// Count the number of messages in the outgoing queue, i.e. those waiting to be sent to the server.
+ /// Note: This calls uses a (read) transaction internally: 1) it's not just a "cheap" return of a single number.
+ /// While this will still be fast, avoid calling this function excessively.
+ /// 2) the result follows transaction view semantics, thus it may not always match the actual value.
+ int outgoingMessageCount({int limit = 0}) {
+ final count = allocate();
+ try {
+ checkObx(bindings.obx_sync_outgoing_message_count(ptr, limit, count));
+ return count.value;
+ } finally {
+ free(count);
+ }
+ }
+}
+
+/// [ObjectBox Sync](https://objectbox.io/sync/) makes data available on other devices.
+///
+/// Start building a sync client using [Sync.client()] and connect to a remote server.
+class Sync {
+ /// Sync() annotation enables synchronization for an entity.
+ const Sync();
+
+ /// Returns true if the loaded ObjectBox native library supports Sync.
+ static bool isAvailable() {
+ return bindings.obx_sync_available() != 0;
+ }
+
+ /// Creates a sync client associated with the given store and configures it with the given options.
+ /// This does not initiate any connection attempts yet: call [SyncClient.start()] to do so.
+ /// Before start(), you can still configure some aspects of the sync client, e.g. its "request update" mode.
+ /// Note: While you may not interact with SyncClient directly after start(), you need to hold on to the object.
+ /// Make sure the SyncClient is not destroyed and thus synchronization can keep running in the background.
+ static SyncClient client(
+ Store store, String serverUri, SyncCredentials creds) {
+ if (SyncClientsStorage.containsKey(store)) {
+ throw Exception('Only one sync client can be active for a store');
+ }
+ syncOrObserversExclusive.mark(store);
+ final client = SyncClient(store, serverUri, creds);
+ SyncClientsStorage[store] = client;
+ StoreCloseObserver.addListener(store, client, client.close);
+ return client;
+ }
+}
diff --git a/lib/src/util.dart b/lib/src/util.dart
index 755f017b1..9a3280ce0 100644
--- a/lib/src/util.dart
+++ b/lib/src/util.dart
@@ -1,4 +1,5 @@
import 'store.dart';
+import 'sync.dart';
bool listContains(List list, T item) =>
list.indexWhere((x) => x == item) != -1;
@@ -38,3 +39,26 @@ class StoreCloseObserver {
return listeners;
}
}
+
+/// Global internal storage of sync clients - one client per store.
+final Map SyncClientsStorage = {};
+
+// Currently, either SyncClient or Observers can be used at the same time.
+// TODO: lift this condition after #142 is fixed.
+class SyncOrObserversExclusive {
+ final _map = {};
+
+ void mark(Store store) {
+ if (_map.containsKey(store)) {
+ throw Exception(
+ 'Using observers/query streams in combination with SyncClient is currently not supported');
+ }
+ _map[store] = true;
+ }
+
+ void unmark(Store store) {
+ _map.remove(store);
+ }
+}
+
+final syncOrObserversExclusive = SyncOrObserversExclusive();
diff --git a/sync_flutter_libs/.gitignore b/sync_flutter_libs/.gitignore
new file mode 100644
index 000000000..ab1ff08fc
--- /dev/null
+++ b/sync_flutter_libs/.gitignore
@@ -0,0 +1,101 @@
+# Miscellaneous
+*.class
+*.lock
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# Visual Studio Code related
+.classpath
+.project
+.settings/
+.vscode/
+
+# packages file containing multi-root paths
+.packages.generated
+
+# Flutter/Dart/Pub related
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+**/generated_plugin_registrant.dart
+.packages
+.pub-cache/
+.pub/
+build/
+flutter_*.png
+linked_*.ds
+unlinked.ds
+unlinked_spec.ds
+
+# Android related
+**/android/**/gradle-wrapper.jar
+**/android/.gradle
+**/android/captures/
+**/android/gradlew
+**/android/gradlew.bat
+**/android/local.properties
+**/android/**/GeneratedPluginRegistrant.java
+**/android/key.properties
+*.jks
+
+# iOS/XCode related
+**/ios/**/*.mode1v3
+**/ios/**/*.mode2v3
+**/ios/**/*.moved-aside
+**/ios/**/*.pbxuser
+**/ios/**/*.perspectivev3
+**/ios/**/*sync/
+**/ios/**/.sconsign.dblite
+**/ios/**/.tags*
+**/ios/**/.vagrant/
+**/ios/**/DerivedData/
+**/ios/**/Icon?
+**/ios/**/Pods/
+**/ios/**/.symlinks/
+**/ios/**/profile
+**/ios/**/xcuserdata
+**/ios/.generated/
+**/ios/Flutter/.last_build_id
+**/ios/Flutter/App.framework
+**/ios/Flutter/Flutter.framework
+**/ios/Flutter/Flutter.podspec
+**/ios/Flutter/Generated.xcconfig
+**/ios/Flutter/app.flx
+**/ios/Flutter/app.zip
+**/ios/Flutter/flutter_assets/
+**/ios/Flutter/flutter_export_environment.sh
+**/ios/ServiceDefinitions.json
+**/ios/Runner/GeneratedPluginRegistrant.*
+
+# macOS
+**/macos/Flutter/GeneratedPluginRegistrant.swift
+**/macos/Flutter/Flutter-Debug.xcconfig
+**/macos/Flutter/Flutter-Release.xcconfig
+**/macos/Flutter/Flutter-Profile.xcconfig
+
+# Coverage
+coverage/
+
+# Symbols
+app.*.symbols
+
+# Exceptions to above rules.
+!**/ios/**/default.mode1v3
+!**/ios/**/default.mode2v3
+!**/ios/**/default.pbxuser
+!**/ios/**/default.perspectivev3
+!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
+!/dev/ci/**/Gemfile.lock
\ No newline at end of file
diff --git a/sync_flutter_libs/CHANGELOG.md b/sync_flutter_libs/CHANGELOG.md
new file mode 100644
index 000000000..5bb8cfa4e
--- /dev/null
+++ b/sync_flutter_libs/CHANGELOG.md
@@ -0,0 +1 @@
+See [ObjectBox changelog](https://pub.dev/packages/objectbox/changelog).
\ No newline at end of file
diff --git a/sync_flutter_libs/LICENSE b/sync_flutter_libs/LICENSE
new file mode 100644
index 000000000..261eeb9e9
--- /dev/null
+++ b/sync_flutter_libs/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/sync_flutter_libs/README.md b/sync_flutter_libs/README.md
new file mode 100644
index 000000000..1bead157d
--- /dev/null
+++ b/sync_flutter_libs/README.md
@@ -0,0 +1,7 @@
+ObjectBox (with [Sync](https://objectbox.io/sync)) libraries for Flutter
+===========================================
+This package provides native ObjectBox library, including [Sync](https://objectbox.io/sync) client, as a flutter plugin for supported platforms.
+You should add this package as a dependency when using [ObjectBox](https://pub.dev/packages/objectbox) with Flutter.
+
+See package [ObjectBox](https://pub.dev/packages/objectbox) for more details and information how to use ObjectBox it.
+
diff --git a/sync_flutter_libs/android/.gitignore b/sync_flutter_libs/android/.gitignore
new file mode 100644
index 000000000..c6cbe562a
--- /dev/null
+++ b/sync_flutter_libs/android/.gitignore
@@ -0,0 +1,8 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
diff --git a/sync_flutter_libs/android/README.md b/sync_flutter_libs/android/README.md
new file mode 100644
index 000000000..09bf2262c
--- /dev/null
+++ b/sync_flutter_libs/android/README.md
@@ -0,0 +1,4 @@
+Contents of this folder is based on `flutter create --template=plugin`.
+It was reduced to the minimum that works for library inclusion by client apps.
+
+Notably, the package depends on `io.objectbox:objectbox-android`, a native ObjectBox library distribution.
\ No newline at end of file
diff --git a/sync_flutter_libs/android/build.gradle b/sync_flutter_libs/android/build.gradle
new file mode 100644
index 000000000..3346d3933
--- /dev/null
+++ b/sync_flutter_libs/android/build.gradle
@@ -0,0 +1,16 @@
+apply plugin: 'com.android.library'
+android {
+ compileSdkVersion 28
+
+ defaultConfig {
+ minSdkVersion 16
+ }
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+}
+
+dependencies {
+ // https://github.com/objectbox/objectbox-java/releases
+ implementation "io.objectbox:objectbox-android:2.8.0-sync"
+}
diff --git a/sync_flutter_libs/android/settings.gradle b/sync_flutter_libs/android/settings.gradle
new file mode 100644
index 000000000..55c707b41
--- /dev/null
+++ b/sync_flutter_libs/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'objectbox_sync_flutter_libs'
diff --git a/sync_flutter_libs/android/src/main/AndroidManifest.xml b/sync_flutter_libs/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..30de5b159
--- /dev/null
+++ b/sync_flutter_libs/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/sync_flutter_libs/android/src/main/java/io/objectbox/flutter/ObjectBoxFlutterPlugin.java b/sync_flutter_libs/android/src/main/java/io/objectbox/flutter/ObjectBoxFlutterPlugin.java
new file mode 100644
index 000000000..ad9bfb501
--- /dev/null
+++ b/sync_flutter_libs/android/src/main/java/io/objectbox/flutter/ObjectBoxFlutterPlugin.java
@@ -0,0 +1,53 @@
+package io.objectbox.flutter;
+
+import androidx.annotation.NonNull;
+
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
+import io.flutter.plugin.common.MethodChannel.Result;
+import io.flutter.plugin.common.PluginRegistry.Registrar;
+
+/** ObjectBoxFlutterPlugin */
+public class ObjectBoxFlutterPlugin implements FlutterPlugin, MethodCallHandler {
+ /// The MethodChannel that will the communication between Flutter and native Android
+ ///
+ /// This local reference serves to register the plugin with the Flutter Engine and unregister it
+ /// when the Flutter Engine is detached from the Activity
+ private MethodChannel channel;
+
+ @Override
+ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
+ channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "objectbox");
+ channel.setMethodCallHandler(this);
+ }
+
+ // This static function is optional and equivalent to onAttachedToEngine. It supports the old
+ // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
+ // plugin registration via this function while apps migrate to use the new Android APIs
+ // post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
+ //
+ // It is encouraged to share logic between onAttachedToEngine and registerWith to keep
+ // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
+ // depending on the user's project. onAttachedToEngine or registerWith must both be defined
+ // in the same class.
+ public static void registerWith(Registrar registrar) {
+ final MethodChannel channel = new MethodChannel(registrar.messenger(), "objectbox");
+ channel.setMethodCallHandler(new ObjectBoxFlutterPlugin());
+ }
+
+ @Override
+ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
+ if (call.method.equals("getPlatformVersion")) {
+ result.success("Android " + android.os.Build.VERSION.RELEASE);
+ } else {
+ result.notImplemented();
+ }
+ }
+
+ @Override
+ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
+ channel.setMethodCallHandler(null);
+ }
+}
diff --git a/sync_flutter_libs/example/README.md b/sync_flutter_libs/example/README.md
new file mode 100644
index 000000000..c3a4a8844
--- /dev/null
+++ b/sync_flutter_libs/example/README.md
@@ -0,0 +1 @@
+See [ObjectBox](https://pub.dev/packages/objectbox).
\ No newline at end of file
diff --git a/sync_flutter_libs/ios/.gitignore b/sync_flutter_libs/ios/.gitignore
new file mode 100644
index 000000000..5110b001f
--- /dev/null
+++ b/sync_flutter_libs/ios/.gitignore
@@ -0,0 +1,3 @@
+# NOTE: comment out before publishing - the binaries need to be uploaded
+Carthage
+*.zip
\ No newline at end of file
diff --git a/sync_flutter_libs/ios/Classes/ObjectBoxFlutterPlugin.h b/sync_flutter_libs/ios/Classes/ObjectBoxFlutterPlugin.h
new file mode 100644
index 000000000..324f488de
--- /dev/null
+++ b/sync_flutter_libs/ios/Classes/ObjectBoxFlutterPlugin.h
@@ -0,0 +1,4 @@
+#import
+
+@interface ObjectBoxFlutterPlugin : NSObject
+@end
diff --git a/sync_flutter_libs/ios/Classes/ObjectBoxFlutterPlugin.m b/sync_flutter_libs/ios/Classes/ObjectBoxFlutterPlugin.m
new file mode 100644
index 000000000..7fb96a956
--- /dev/null
+++ b/sync_flutter_libs/ios/Classes/ObjectBoxFlutterPlugin.m
@@ -0,0 +1,15 @@
+#import "ObjectBoxFlutterPlugin.h"
+#if __has_include()
+#import
+#else
+// Support project import fallback if the generated compatibility header
+// is not copied when this plugin is created as a library.
+// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
+#import "objectbox_sync_flutter_libs-Swift.h"
+#endif
+
+@implementation ObjectBoxFlutterPlugin
++ (void)registerWithRegistrar:(NSObject*)registrar {
+ [SwiftObjectBoxFlutterPlugin registerWithRegistrar:registrar];
+}
+@end
diff --git a/sync_flutter_libs/ios/Classes/SwiftObjectboxPlugin.swift b/sync_flutter_libs/ios/Classes/SwiftObjectboxPlugin.swift
new file mode 100644
index 000000000..45141988e
--- /dev/null
+++ b/sync_flutter_libs/ios/Classes/SwiftObjectboxPlugin.swift
@@ -0,0 +1,14 @@
+import Flutter
+import UIKit
+
+public class SwiftObjectBoxFlutterPlugin: NSObject, FlutterPlugin {
+ public static func register(with registrar: FlutterPluginRegistrar) {
+ let channel = FlutterMethodChannel(name: "objectbox_sync_flutter_libs", binaryMessenger: registrar.messenger())
+ let instance = SwiftObjectBoxFlutterPlugin()
+ registrar.addMethodCallDelegate(instance, channel: channel)
+ }
+
+ public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ result("iOS " + UIDevice.current.systemVersion)
+ }
+}
diff --git a/sync_flutter_libs/ios/README.md b/sync_flutter_libs/ios/README.md
new file mode 100644
index 000000000..ab4ee1b86
--- /dev/null
+++ b/sync_flutter_libs/ios/README.md
@@ -0,0 +1,16 @@
+Contents of this folder is based on `flutter create --template=plugin`.
+It was reduced to the minimum that works for library inclusion by client apps.
+
+Notably, the package depends on `ObjectBox.framework` from ObjectBox Swift distribution, downloading
+a released `ObjectBox-framework-X.Y.Z.zip` archive.
+
+## Current limitations/TODOs
+There's currently an [issue](https://github.com/flutter/flutter/issues/45778) with Flutter tooling and/or its integration
+with Cocoapods. In short, an "http" source in the podspec doesn't work - the file has to be available locally.
+
+To circumvent this, we're currently including the extracted `ObjectBox.framework` for iOS in the package when publishing
+to pub.dev. Therefore, you need to run ./ios/download-framework.sh before publishing the package.
+This has the benefit of a "no-setup" iOS support for the ObjectBox users - it works out of the box.
+Also notably, we're only including the bare minimum from the ObjectBox Swift release which means smaller final app size.
+
+Note for contributors: you need to run the above-mentioned script as well to be able to test ObjectBox on iOS.
diff --git a/sync_flutter_libs/ios/download-framework.sh b/sync_flutter_libs/ios/download-framework.sh
new file mode 100755
index 000000000..302fcfcca
--- /dev/null
+++ b/sync_flutter_libs/ios/download-framework.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+# NOTE: run this script before publishing
+
+# https://github.com/objectbox/objectbox-swift/releases/
+obxSwiftVersion="1.4.0"
+
+dir=$(dirname "$0")
+
+url="https://github.com/objectbox/objectbox-swift/releases/download/v${obxSwiftVersion}/ObjectBox-framework-${obxSwiftVersion}.zip"
+zip="${dir}/fw.zip"
+
+curl --location --fail --output "${zip}" "${url}"
+
+rm -rf "${dir}/Carthage"
+unzip "${zip}" -d "${dir}" \
+ "Carthage/Build/iOS/ObjectBox.framework/Headers/*" \
+ "Carthage/Build/iOS/ObjectBox.framework/ObjectBox" \
+ "Carthage/Build/iOS/ObjectBox.framework/Info.plist"
+
+rm "${zip}"
\ No newline at end of file
diff --git a/sync_flutter_libs/ios/objectbox_sync_flutter_libs.podspec b/sync_flutter_libs/ios/objectbox_sync_flutter_libs.podspec
new file mode 100644
index 000000000..67d1dfe2f
--- /dev/null
+++ b/sync_flutter_libs/ios/objectbox_sync_flutter_libs.podspec
@@ -0,0 +1,29 @@
+# Provides the compiled framework as released in objectbox-swift. No dart-related sources.
+# Run `pod lib lint objectbox_sync_flutter_libs.podspec' to validate before publishing.
+# This package is not distributed as a CocoaPod, rather it's automatically used by Flutter when creating
+# ios/{app}.podspec in client applications using objectbox-dart as a dependency.
+# Some of the lines from the original podspec are commented out but left for future reference, in case it stops working.
+Pod::Spec.new do |s|
+ s.name = 'objectbox_sync_flutter_libs'
+ s.version = '0.0.1' # not used anywhere - official flutter plugins use the same
+ s.summary = 'ObjectBox is a super-fast NoSQL ACID compliant object database.'
+ s.homepage = 'https://objectbox.io'
+ s.license = 'Apache 2.0, ObjectBox Binary License'
+ s.author = 'ObjectBox'
+ s.platform = :ios, '8.0'
+
+ s.source = { :path => '.' }
+ s.source_files = 'Classes/**/*'
+
+ s.dependency 'Flutter'
+
+ # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
+ s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
+ s.swift_version = '5.0'
+
+ # Get the ObjectBoxC.framework from the objectbox-swift release (see README.md)
+ s.ios.vendored_frameworks = 'Carthage/Build/iOS/ObjectBox.framework'
+
+ # Fail early during build instead of not finding the library during runtime
+ s.xcconfig = { 'OTHER_LDFLAGS' => '-framework ObjectBox' }
+end
diff --git a/sync_flutter_libs/lib/objectbox_sync_flutter_libs.dart b/sync_flutter_libs/lib/objectbox_sync_flutter_libs.dart
new file mode 100644
index 000000000..84890f314
--- /dev/null
+++ b/sync_flutter_libs/lib/objectbox_sync_flutter_libs.dart
@@ -0,0 +1,2 @@
+/// This package only contains platform-specific native libraries for flutter.
+/// All the dart code is in package "objectbox".
diff --git a/sync_flutter_libs/pubspec.yaml b/sync_flutter_libs/pubspec.yaml
new file mode 100644
index 000000000..e62a832d3
--- /dev/null
+++ b/sync_flutter_libs/pubspec.yaml
@@ -0,0 +1,23 @@
+name: objectbox_sync_flutter_libs
+version: 0.8.1
+repository: https://github.com/objectbox/objectbox-dart
+homepage: https://objectbox.io
+description: ObjectBox is a super-fast NoSQL ACID compliant object database. This package contains flutter runtime libraries for ObjectBox, including ObjectBox Sync.
+
+environment:
+ sdk: ">=2.6.0 <3.0.0"
+ flutter: ">=1.12.0 <2.0.0"
+
+dependencies:
+ # This is here just to ensure compatibility between objectbox-dart code and the libraries used
+ # You should still depend on objectbox directly in your Flutter application.
+ objectbox: 0.8.1
+
+flutter:
+ plugin:
+ platforms:
+ android:
+ package: io.objectbox.flutter
+ pluginClass: ObjectBoxFlutterPlugin
+ ios:
+ pluginClass: ObjectBoxFlutterPlugin
diff --git a/test/entity.dart b/test/entity.dart
index aaaeef2d4..e8e20defc 100644
--- a/test/entity.dart
+++ b/test/entity.dart
@@ -7,6 +7,7 @@ class TestingUnknownAnnotation {
@Entity()
@TestingUnknownAnnotation()
+@Sync()
class TestEntity {
@Id()
@TestingUnknownAnnotation()
diff --git a/test/objectbox-model.json b/test/objectbox-model.json
index 658cfbaa9..c081e636a 100644
--- a/test/objectbox-model.json
+++ b/test/objectbox-model.json
@@ -7,6 +7,7 @@
"id": "1:4630700155272683157",
"lastPropertyId": "14:2723176855509462268",
"name": "TestEntity",
+ "flags": 2,
"properties": [
{
"id": "1:1502777149103994787",
diff --git a/test/sync_test.dart b/test/sync_test.dart
new file mode 100644
index 000000000..466969c43
--- /dev/null
+++ b/test/sync_test.dart
@@ -0,0 +1,207 @@
+import 'dart:math';
+import 'dart:typed_data';
+
+import 'package:test/test.dart';
+import 'package:objectbox/objectbox.dart';
+import 'package:objectbox/observable.dart';
+import 'package:objectbox/src/bindings/constants.dart';
+
+import 'entity.dart';
+import 'objectbox.g.dart';
+import 'test_env.dart';
+
+// We want to have types explicit - verifying the return types of functions.
+// ignore_for_file: omit_local_variable_types
+
+void main() {
+ TestEnv env;
+ Store store;
+
+ setUp(() {
+ env = TestEnv('sync');
+ store = env.store;
+ });
+
+ tearDown(() {
+ if (env != null) env.close();
+ });
+
+ // lambda to easily create clients in the test below
+ SyncClient createClient(Store s) =>
+ Sync.client(s, 'ws://127.0.0.1:9999', SyncCredentials.none());
+
+ // lambda to easily create clients in the test below
+ SyncClient loggedInClient(Store s) {
+ final client = createClient(s);
+ client.start();
+ expect(waitUntil(() => client.state() == SyncState.loggedIn), isTrue);
+ return client;
+ }
+
+ test('Model Entity has sync enabled', () {
+ final model = getObjectBoxModel().model;
+ final entity =
+ model.entities.firstWhere((ModelEntity e) => e.name == "TestEntity");
+ expect(entity.hasFlag(OBXEntityFlag.SYNC_ENABLED), isTrue);
+ });
+
+ test('SyncCredentials string encoding', () {
+ // Let's check some special characters and verify the data is how it would
+ // look like if the same shared secret was provided to the sync-server via
+ // an utf-8 encoded json file (i.e. the usual way).
+ final str = 'uũú';
+ expect(SyncCredentials.sharedSecretString(str).data,
+ equals(Uint8List.fromList([117, 197, 169, 195, 186])));
+ });
+
+ if (Sync.isAvailable()) {
+ // TESTS to run when SYNC is available
+
+ group('Circumvent issue #142 - async callbacks error', () {
+ final error = throwsA(predicate((Exception e) => e.toString().contains(
+ 'Using observers/query streams in combination with SyncClient is currently not supported')));
+
+ test('Must not start an Observer when SyncClient is active', () {
+ createClient(store);
+ expect(() => env.box.query().build().findStream(), error);
+ });
+
+ test('Must not start SyncClient when an Observer is active', () {
+ final error = throwsA(predicate((Exception e) => e.toString().contains(
+ 'Using observers/query streams in combination with SyncClient is currently not supported')));
+
+ SyncClient c = createClient(store);
+ expect(() => env.box.query().build().findStream(), error);
+ });
+ });
+
+ test('SyncClient lifecycle', () {
+ expect(store.syncClient(), isNull);
+
+ SyncClient c1 = createClient(store);
+
+ // Store now has the client available in cache.
+ expect(store.syncClient(), equals(c1));
+
+ // Can't have two clients on the same store.
+ expect(
+ () => createClient(store),
+ throwsA(predicate(
+ (Exception e) => e.toString().contains('one sync client'))));
+
+ // But we can have another one after the previous is closed or destroyed.
+ expect(c1.isClosed(), isFalse);
+ c1.close();
+ expect(c1.isClosed(), isTrue);
+ expect(store.syncClient(), isNull);
+ });
+
+ test('SyncClient instance caching', () {
+ {
+ // Just losing the variable scope doesn't close the client automatically.
+ // Store holds onto the same instance.
+ final client = createClient(store);
+ expect(client.isClosed(), isFalse);
+ }
+
+ // But we can still get a handle of the client in the store - we're never
+ // completely without an option to close it.
+ final client = store.syncClient();
+ expect(client, isNotNull);
+ expect(client.isClosed(), isFalse);
+ client.close();
+ expect(store.syncClient(), isNull);
+ });
+
+ test('SyncClient is closed when a store is closed', () {
+ final env2 = TestEnv('sync2');
+ final client = createClient(env2.store);
+ env2.close();
+ expect(client.isClosed(), isTrue);
+ });
+
+ test('different Store => different SyncClient', () {
+ SyncClient c1 = createClient(store);
+
+ final env2 = TestEnv('sync2');
+ SyncClient c2 = createClient(env2.store);
+ expect(c1, isNot(equals(c2)));
+ env2.close();
+ });
+
+ test('SyncClient states (no server available)', () {
+ SyncClient client = createClient(store);
+ expect(client.state(), equals(SyncState.created));
+ client.start();
+ expect(client.state(), equals(SyncState.started));
+ client.stop();
+ expect(client.state(), equals(SyncState.stopped));
+ });
+
+ test('SyncClient access after closing must throw', () {
+ SyncClient c = createClient(store);
+ c.close();
+ expect(c.isClosed(), isTrue);
+
+ final error = throwsA(predicate(
+ (Exception e) => e.toString().contains('SyncClient already closed')));
+ expect(() => c.start(), error);
+ expect(() => c.stop(), error);
+ expect(() => c.state(), error);
+ expect(() => c.cancelUpdates(), error);
+ expect(() => c.requestUpdates(true), error);
+ expect(() => c.outgoingMessageCount(), error);
+ expect(() => c.setCredentials(SyncCredentials.none()), error);
+ expect(() => c.setRequestUpdatesMode(SyncRequestUpdatesMode.auto), error);
+ });
+
+ test('SyncClient simple coverage (no server available)', () {
+ SyncClient c = createClient(store);
+ expect(c.isClosed(), isFalse);
+ c.setCredentials(SyncCredentials.none());
+ c.setCredentials(SyncCredentials.googleAuthString('secret'));
+ c.setCredentials(SyncCredentials.sharedSecretString('secret'));
+ c.setCredentials(
+ SyncCredentials.googleAuthUint8List(Uint8List.fromList([13, 0, 25])));
+ c.setCredentials(SyncCredentials.sharedSecretUint8List(
+ Uint8List.fromList([13, 0, 25])));
+ c.setCredentials(SyncCredentials.none());
+ c.setRequestUpdatesMode(SyncRequestUpdatesMode.manual);
+ c.start();
+ expect(c.requestUpdates(true), isFalse); // false because not connected
+ expect(c.requestUpdates(false), isFalse); // false because not connected
+ expect(c.outgoingMessageCount(), isZero);
+ c.stop();
+ expect(c.state(), equals(SyncState.stopped));
+ });
+
+ test('SyncClient - data test (requires manual server setup)', () {
+ final env2 = TestEnv('sync2');
+
+ loggedInClient(env.store);
+ loggedInClient(env2.store);
+
+ int id = env.box.put(TestEntity(tLong: Random().nextInt(1 << 32)));
+ expect(waitUntil(() => env2.box.get(id) != null), isTrue);
+
+ final read1 = env.box.get(id);
+ final read2 = env2.box.get(id);
+ expect(read1.id, equals(read2.id));
+ expect(read1.tLong, equals(read2.tLong));
+ },
+ // Note: only available when you start a sync server manually.
+ // Comment out the `skip: ` argument in tthe test-case definition.
+ // run sync-server --unsecured-no-authentication --model=/path/objectbox-dart/test/objectbox-model.json
+ skip: 'Data sync test is disabled, Enable after running sync-server.' //
+ );
+ } else {
+ // TESTS to run when SYNC isn't available
+
+ test('SyncClient cannot be created when running with non-sync library', () {
+ expect(
+ () => createClient(store),
+ throwsA(predicate((Exception e) => e.toString().contains(
+ 'Sync is not available in the loaded ObjectBox runtime library'))));
+ });
+ }
+}
diff --git a/test/test_env.dart b/test/test_env.dart
index dfa2e9929..16321bedf 100644
--- a/test/test_env.dart
+++ b/test/test_env.dart
@@ -19,3 +19,15 @@ class TestEnv {
if (dir.existsSync()) dir.deleteSync(recursive: true);
}
}
+
+/// "Busy-waits" until the predicate returns true.
+bool waitUntil(bool Function() predicate, {int timeoutMs = 1000}) {
+ var success = false;
+ final until = DateTime.now().millisecondsSinceEpoch + timeoutMs;
+
+ while (!(success = predicate()) &&
+ until > DateTime.now().millisecondsSinceEpoch) {
+ sleep(Duration(milliseconds: 1));
+ }
+ return success;
+}