diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 187ea46746..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,43 +0,0 @@ -> We LOVE to help with any issues or bug you have! - -> **Questions**: If you have questions about how to use Realm, please ask on [SO](http://stackoverflow.com/questions/ask?tags=realm) - we monitor the Realm tag. - -> **Feature Request**: Just fill in the first two sections below. - -> **Bugs**: To help you as fast as possible with an issue or bug please describe your issue and the steps you have taken to reproduce it in as many details as possible. -> -> Thanks for helping us help you :-) -> -> Remove this and above before submitting. - -#### Goal - -> What do you want to achieve? - -#### Expected Results - -> ? - -#### Actual Results - -> E.g. full stack trace with exception - -#### Steps & Code to Reproduce - -> Describe your current debugging efforts. - -#### Code Sample - -```java - -> Your code here. Bigger samples should ideally be as separate Android Studio project, -> in gists/repositories or privately at help@realm.io) - -``` - -#### Version of Realm and tooling -Realm version(s): ? - -Android Studio version: ? - -Which Android version and device: ? diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..1664fce38f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: General Questions and Inquiries + url: https://www.mongodb.com/community/forums/tags/c/realm-sdks/58/java + about: Please ask general design/architecture questions in the community forums. + - name: MongoDB Atlas Device Sync Production Issues + url: https://support.mongodb.com/ + about: Please report urgent production issues to the support portal directly. diff --git a/.github/advanced-issue-labeler.yml b/.github/advanced-issue-labeler.yml new file mode 100644 index 0000000000..87a2ae04a6 --- /dev/null +++ b/.github/advanced-issue-labeler.yml @@ -0,0 +1,76 @@ +# NOTE: This is a common file that is overwritten by realm/ci-actions sync service +# and should only be modified in that repository. + +# syntax - https://github.com/redhat-plumbers-in-action/advanced-issue-labeler#policy +# Below keys map from the option used in issue form dropdowns to issue labels +# Limitation: +# Currently it's not possible to use strings containing ,␣ in the dropdown menus in the issue forms. + +--- + +policy: + - template: [bug.yml, feature.yml] + section: + - id: [frequency] + label: + - name: 'Frequency:Once' + keys: ['Once'] + - name: 'Frequency:Sometimes' + keys: ['Sometimes'] + - name: 'Frequency:Always' + keys: ['Always'] + + - id: [repro] + label: + - name: 'Repro:Always' + keys: ['Always'] + - name: 'Repro:Sometimes' + keys: ['Sometimes'] + - name: 'Repro:No' + keys: ['No'] + + - id: [sync, flavour, services] + block-list: [] + label: + - name: 'SDK-Use:Local' + keys: ['Local Database only'] + - name: 'SDK-Use:Sync' + keys: ['Atlas Device Sync'] + - name: 'SDK-Use:Services' + keys: ['Atlas App Services: Function or GraphQL or DataAPI etc'] + - name: ['SDK-Use:All'] + keys: ['Both Atlas Device Sync and Atlas App Services'] + + - id: [encryption] + block-list: [] + label: + - name: 'Encryption:On' + keys: ['Yes'] + - name: 'Encryption:Off' + keys: ['No'] + + - id: [app-type] + block-list: [] + label: + - name: 'App-type:Unity' + keys: ['Unity'] + - name: 'App-type:Xamarin' + keys: ['Xamarin'] + - name: 'App-type:WPF' + keys: ['WPF'] + - name: 'App-type:Console' + keys: ['Console or Server'] + - name: 'App-type:Other' + keys: ['Other'] + + - id: [importance] + block-list: [] + label: + - name: 'Importance:Dealbraker' + keys: ['Dealbreaker'] + - name: 'Importance:Major' + keys: ['Would be a major improvement'] + - name: 'Importance:Workaround' + keys: ['I would like to have it but have a workaround'] + - name: 'Importance:Nice' + keys: ['Fairly niche but nice to have anyway'] diff --git a/.github/auto_assign.yml b/.github/auto_assign.yml new file mode 100644 index 0000000000..cb3b1df80f --- /dev/null +++ b/.github/auto_assign.yml @@ -0,0 +1,3 @@ +addAssignees: author +addReviewers: false +runOnDraft: true diff --git a/.github/no-response.yml b/.github/no-response.yml new file mode 100644 index 0000000000..e71a764961 --- /dev/null +++ b/.github/no-response.yml @@ -0,0 +1,13 @@ +# Configuration for probot-no-response - https://github.com/probot/no-response + +# Number of days of inactivity before an Issue is closed for lack of response +daysUntilClose: 14 +# Label requiring a response +responseRequiredLabel: More-information-needed +# Comment to post when closing an Issue for lack of response. Set to `false` to disable +closeComment: > + This issue has been automatically closed because there has been no response + to our request for more information from the original author. With only the + information that is currently in the issue, we don't have enough information + to take action. Please reach out if you have or find the answers we need so + that we can investigate further. diff --git a/.github/workflows/Issue-Needs-Attention.yml b/.github/workflows/Issue-Needs-Attention.yml new file mode 100644 index 0000000000..842194ac42 --- /dev/null +++ b/.github/workflows/Issue-Needs-Attention.yml @@ -0,0 +1,12 @@ +# NOTE: This is a common file that is overwritten by realm/ci-actions sync service +# and should only be modified in that repository. + +name: Issue Needs Attention +# This workflow is triggered on issue comments. +on: + issue_comment: + types: created + +jobs: + applyNeedsAttentionLabel: + uses: realm/ci-actions/.github/workflows/issue-needs-attention.yml@main diff --git a/.github/workflows/auto-assign.yml b/.github/workflows/auto-assign.yml new file mode 100644 index 0000000000..e2861e4490 --- /dev/null +++ b/.github/workflows/auto-assign.yml @@ -0,0 +1,13 @@ +# NOTE: This is a common file that is overwritten by realm/ci-actions sync service +# and should only be modified in that repository. + +name: 'Auto Assign' +on: + pull_request: + types: [opened] + +jobs: + add-assignee: + runs-on: ubuntu-latest + steps: + - uses: kentaro-m/auto-assign-action@248761c4feb3917c1b0444e33fad1a50093b9847 diff --git a/.github/workflows/check-changelog.yml b/.github/workflows/check-changelog.yml new file mode 100644 index 0000000000..e23ae17512 --- /dev/null +++ b/.github/workflows/check-changelog.yml @@ -0,0 +1,21 @@ +# NOTE: This is a common file that is overwritten by realm/ci-actions sync service +# and should only be modified in that repository. + +name: "Check Changelog" +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] + +jobs: + changelog: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@8230315d06ad95c617244d2f265d237a1682d445 + with: + submodules: false + - name: Enforce Changelog + uses: dangoslen/changelog-enforcer@c0b9fd225180a405c5f21f7a74b99e2eccc3e951 + with: + skipLabels: no-changelog + missingUpdateErrorMessage: Please add an entry in CHANGELOG.md or apply the 'no-changelog' label to skip this check. diff --git a/.github/workflows/check-pr-title.yml b/.github/workflows/check-pr-title.yml new file mode 100644 index 0000000000..1842f5bf39 --- /dev/null +++ b/.github/workflows/check-pr-title.yml @@ -0,0 +1,22 @@ +# NOTE: This is a common file that is overwritten by realm/ci-actions sync service +# and should only be modified in that repository. + +name: "Check PR Title" +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled, converted_to_draft, edited] + +jobs: + check-pr-title: + name: Check PR Title + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: false + - name: Enforce PR title + uses: realm/ci-actions/title-checker@main + with: + regex: R[A-Z]{2,6}-[0-9]{1,6} + error-hint: Invalid PR title. Make sure it's prefixed with the JIRA ticket the PR addresses or add the no-jira-ticket label. + ignore-labels: 'no-jira-ticket' \ No newline at end of file diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml new file mode 100644 index 0000000000..aa1326c7ff --- /dev/null +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -0,0 +1,13 @@ +name: "Validate Gradle Wrapper" +on: [push, pull_request] + +permissions: + contents: read # to fetch code (actions/checkout) + +jobs: + validation: + name: "Validation" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: gradle/wrapper-validation-action@v1 diff --git a/.github/workflows/issue-labeler.yml b/.github/workflows/issue-labeler.yml new file mode 100644 index 0000000000..a93e0c8c02 --- /dev/null +++ b/.github/workflows/issue-labeler.yml @@ -0,0 +1,35 @@ +# NOTE: This is a common file that is overwritten by realm/ci-actions sync service +# and should only be modified in that repository. + +# See configuration in .github/advanced-issue-labeler.yml + +name: Issue labeler (policy) +on: + issues: + types: [ opened ] + +jobs: + label-issues-policy: + runs-on: ubuntu-latest + permissions: + issues: write + + strategy: + matrix: + template: [ bug.yml, feature.yml ] + + steps: + - uses: actions/checkout@v4 + + - name: Parse issue form + uses: stefanbuck/github-issue-parser@c1a559d78bfb8dd05216dab9ffd2b91082ff5324 # v3.0.1 + id: issue-parser + with: + template-path: .github/ISSUE_TEMPLATE/${{ matrix.template }} + + - name: Set labels based on policy + uses: redhat-plumbers-in-action/advanced-issue-labeler@6ee6fddfd744ee26b977e6a0436916f256896971 # v2.0.3 + with: + issue-form: ${{ steps.issue-parser.outputs.jsonString }} + template: ${{ matrix.template }} + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/lock-threads.yml b/.github/workflows/lock-threads.yml new file mode 100644 index 0000000000..dc1d483a8e --- /dev/null +++ b/.github/workflows/lock-threads.yml @@ -0,0 +1,24 @@ +name: 'Lock Threads' + +on: + schedule: + - cron: '0 * * * *' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + discussions: write + +concurrency: + group: lock-threads + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v5 + with: + issue-inactive-days: 30 + pr-inactive-days: 30 + log-output: true diff --git a/.github/workflows/no-response.yml b/.github/workflows/no-response.yml new file mode 100644 index 0000000000..2505323873 --- /dev/null +++ b/.github/workflows/no-response.yml @@ -0,0 +1,22 @@ +# NOTE: This is a common file that is overwritten by realm/ci-actions sync service +# and should only be modified in that repository. + +name: No Response + +# Both `issue_comment` and `scheduled` event types are required for this Action +# to work properly. +on: + issue_comment: + types: [created] + schedule: + # Schedule at 00:00 every day + - cron: '0 0 * * *' + +jobs: + noResponse: + runs-on: ubuntu-latest + steps: + - uses: lee-dohm/no-response@v0.5.0 + with: + token: ${{ github.token }} + responseRequiredLabel: More-information-needed diff --git a/.gitignore b/.gitignore index 22df2e0956..8754b69638 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Gradle build artifacts build realm/build +!realm-transformer/src/main/kotlin/io/realm/transformer/build # Gradle cache .gradle @@ -11,6 +12,8 @@ local.properties # Core core core-* +realm-sync-android-* +!realm/realm-library/src/main/java/io/realm/internal/core/ # Android Studio .idea @@ -51,3 +54,4 @@ realm/realm-library/src/main/cpp/jni_include realm/realm-library/distribution # Cmake output realm/realm-library/.externalNativeBuild +realm/realm-library/.cxx diff --git a/.gitmodules b/.gitmodules index 35b419c520..b0bd09c398 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ -[submodule "realm/realm-library/src/main/cpp/object-store"] - path = realm/realm-library/src/main/cpp/object-store - url = https://github.com/realm/realm-object-store.git +[submodule "realm/realm-library/src/main/cpp/realm-core"] + path = realm/realm-library/src/main/cpp/realm-core + url = https://github.com/realm/realm-core.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e25fb2053..d409ac73b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,3021 @@ -## 2.0.0 +## 11.0.0 (2024-09-DD) ### Breaking Changes +* None. +### Enhancements +* None. + +### Fixed +* None. + +### Compatibility +* File format: Generates Realms with format v23. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* Gradle 7.5 and above. +* Android Gradle Plugin 7.4.0 and above. + +### Internal +* Updated links to new documentation URL. + + +## 10.19.0 (2024-09-13) + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixed +* Use 16 KB ELF packaging for native artifacts produced by `realm-library`, allowing them to be loaded on devices with 16 KB memory page sizes. (Issue [#7894](https://github.com/realm/realm-java/issues/7894)) + +### Compatibility +* File format: Generates Realms with format v23. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* Gradle 7.5 and above. +* Android Gradle Plugin 7.4.0 and above. + +### Internal +* Updated links to new documentation URL. + + +## 10.18.0 (2024-02-06) + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixed +* None. + +### Compatibility +* File format: Generates Realms with format v23. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* Gradle 7.5 and above. +* Android Gradle Plugin 7.4.0 and above. + +### Internal +* Updated to Realm Core 13.26.0, commit 5533505d18fda93a7a971d58a191db5005583c92. +* Updated to CMake 3.27.7. + + +## 10.17.0 (2023-10-13) + +This release upgrades the Sync metadata in a way that is not compatible with older versions. To downgrade a Sync app from this version, you'll need to manually delete the metadata folder located at $[SYNC-ROOT-DIRECTORY]/mongodb-realm/[APP-ID]/server-utility/metadata/. This will log out all users. + +### Breaking Changes +* None. + +### Enhancements +* [RealmApp] Simplified the number of error codes in `ErrorCode`. A number of enum entries have been deprecated. (Issue [#7837](https://github.com/realm/realm-java/pull/7837)). + +### Fixed +* Rare corruption causing 'Invalid streaming format cookie'-exception. Typically following compact, convert or copying to a new file. (Issue [#7775](https://github.com/realm/realm-java/issues/7775), since v10.13.0 (Core v12.12.0)) +* [RealmApp] Crash when opening a Realm with a proxy enabled. (Issue [#7828](https://github.com/realm/realm-java/issues/7828)) + +### Compatibility +* File format: Generates Realms with format v23. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* Gradle 7.5 and above. +* Android Gradle Plugin 7.4.0 and above. + +### Internal +* Updated to Realm Core 13.23.0, commit 5abbf7f10fb3ef6bd622877cc840ada804bccb89. + + +## 10.16.2 (2023-10-12) + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixed +* Realm objects accessors behave as an unmanaged object after an incremental build. (Issue [#7844](https://github.com/realm/realm-java/pull/7844)) +* [RealmApp] Crash when opening a Realm with a proxy enabled. (Issue [#7828](https://github.com/realm/realm-java/issues/7828)) +* [RealmApp] It was possible to create a `User` object with invalid state that would throw a `NullPointerException` when accessed. (Issue [#7847](https://github.com/realm/realm-java/issues/7847)) + +### Compatibility +* File format: Generates Realms with format v23. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* Gradle 7.5 and above. +* Android Gradle Plugin 7.4.0 and above. + +### Internal +* None. + + +## 10.16.1 (2023-06-26) + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixed +* [RealmApp] Sync errors could return the error code UNKNOWN instead of the actual error code. (Issue [#7823](https://github.com/realm/realm-java/issues/7823)) + +### Compatibility +* File format: Generates Realms with format v23. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* Gradle 7.5 and above. +* Android Gradle Plugin 7.4.0 and above. + +### Internal +* None. + + +## 10.16.0 (2023-06-02) + +### Breaking Changes +* None. + +### Enhancements +* Add support for sorting query results on dictionary values through `RealmQuery.rawPredicate(" SORT([''] )")` (Issue [#7817](https://github.com/realm/realm-java/issues/7817)). +* Improve performance of equality queries on a non-indexed mixed property by about 30%. (Core Issue [#6506](https://github.com/realm/realm-core/issues/6506)) +* [RealmApp] Support for migrating from Partition-based to Flexible Sync automatically on the device if the server has migrated to Flexible Sync. (Core Issue [#6554](https://github.com/realm/realm-core/issues/6554)) + +### Fixed +* Add support for incremental builds on the bytecode transformation with the new AGP transform API. Incremental builds can be disabled by setting the gradle property `io.realm.disableIncrementalBuilds` to `true`. (Issue [#7802](https://github.com/realm/realm-java/issues/7802)) + +### Compatibility +* File format: Generates Realms with format v23. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* Gradle 7.5 and above. +* Android Gradle Plugin 7.4.0 and above. + +### Internal +* Updated to Realm Core 13.13.0, commit 79183be6417821431fff44a6d416a68664957c66. + + +## 10.15.1 (2023-05-20) + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixed +* Building with Realm on Java 17 would fail with `java.lang.IllegalAccessError: class io.realm.processor.Utils (in unnamed module @0x5316ec7f) cannot access class com.sun.tools.javac.code.Symbol$ClassSymbol`. (Issue [#7799](https://github.com/realm/realm-java/issues/7799)) + +### Compatibility +* File format: Generates Realms with format v23. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* Gradle 7.5 and above. +* Android Gradle Plugin 7.4.0 and above. + +### Internal +* Updated to Google Compile Testing 0.21.0. +* Updated Annotation Processor to use Java 11 reflection API's. + + +## 10.15.0 (2023-04-19) + +### Breaking Changes +* Minimum version of Android Gradle Plugin has been raised to 7.4. +* Minimum version of Java has been raised to 11. +* Minimum supported version of Gradle has been raised to 7.5. + +### Enhancements +* None. + +### Fixed +* Building with Realm on Java 17 would fail with `java.lang.IllegalAccessError: class io.realm.processor.Utils (in unnamed module @0x5316ec7f) cannot access class com.sun.tools.javac.code.Symbol$ClassSymbol`. (Issue [#7799](https://github.com/realm/realm-java/issues/7799)) + +### Compatibility +* File format: Generates Realms with format v23. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 13.0.0 or above is required to open Realms created by this version. +* Gradle 7.5 and above. +* Android Gradle Plugin 7.4.0 and above. + +### Internal +* None. + + +## 10.14.0-transformer-api (2023-04-14) + +This release will bump the Realm file format from version 22 to 23. Opening a file with an older format will automatically upgrade it. Downgrading to a previous file format is not possible. + +### Breaking Changes +* [RealmApp] As a result of a refactor on the some error codes and categories have been deleted and new ones have been added, see PR for more details. ([#7760](https://github.com/realm/realm-java/pull/7760)) + +### Enhancements +* Updated OpenSSL from 1.1.1n to 3.0.8. +* Realm will now use a lot less memory and disk space when different versions of realm objects are used. (Core Issue [#5440](https://github.com/realm/realm-core/issues/5440)) +* Realm will now use a lot less memory and disk space when different versions of realm objects are used. (Core Issue [#5440](https://github.com/realm/realm-core/issues/5440)) +* Realm will now continuously track and reduce the size of the Realm file when it is in use rather that only when opening the file with Configuration.compactOnLaunch enabled. (Core Issue [#5754](https://github.com/realm/realm-core/issues/5754)) +* Multiple processes can now access the same encrypted Realm instead of throwing `Encrypted interprocess sharing is currently unsupported`. (Core Issue [#1845](https://github.com/realm/realm-core/issues/1845)) + +### Fixed +* Set consider string and binary data equivalent. This could cause the client to be inconsistent with the server if a string and some binary data with equivalent content was inserted from Atlas. ([#4860](https://github.com/realm/realm-core/issues/4860), since v11.0.0) +* Fixed wrong assertion on query error that could result in a crash. ([#6038](https://github.com/realm/realm-core/issues/6038), since v11.7.0) +* Fixed a bug that would result in `RealmDictionary` not being able to find `double` values due not taking the precision of the input parameter into consideration when using `RealmDictionary.contains`. +* Not possible to open an encrypted file on a device with a page size bigger than the one on which the file was produced. ([#8030](https://github.com/realm/realm-swift/issues/8030), since v12.11.0) +* Fix no notification for write transaction that contains only change to backlink property. ([#4994](https://github.com/realm/realm-core/issues/4994), since v11.4.1) + +### Compatibility +* File format: Generates Realms with format v23. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 13.9.0, commit 063927de66f79a0afffbbe36c0bb14d27deba8f2. + + +## 10.14.0 (2023-04-13) + +This release will bump the Realm file format from version 22 to 23. Opening a file with an older format will automatically upgrade it. Downgrading to a previous file format is not possible. + +### Breaking Changes +* [RealmApp] As a result of a refactor on the some error codes and categories have been deleted and new ones have been added, see PR for more details. ([#7760](https://github.com/realm/realm-java/pull/7760)) + +### Enhancements +* Updated OpenSSL from 1.1.1n to 3.0.8. +* Realm will now use a lot less memory and disk space when different versions of realm objects are used. (Core Issue [#5440](https://github.com/realm/realm-core/issues/5440)) +* Realm will now continuously track and reduce the size of the Realm file when it is in use rather that only when opening the file with Configuration.compactOnLaunch enabled. (Core Issue [#5754](https://github.com/realm/realm-core/issues/5754)) +* Multiple processes can now access the same encrypted Realm instead of throwing `Encrypted interprocess sharing is currently unsupported`. (Core Issue [#1845](https://github.com/realm/realm-core/issues/1845)) + +### Fixed +* Set consider string and binary data equivalent. This could cause the client to be inconsistent with the server if a string and some binary data with equivalent content was inserted from Atlas. ([#4860](https://github.com/realm/realm-core/issues/4860), since v11.0.0) +* Fixed wrong assertion on query error that could result in a crash. ([#6038](https://github.com/realm/realm-core/issues/6038), since v11.7.0) +* Fixed a bug that would result in `RealmDictionary` not being able to find `double` values due not taking the precision of the input parameter into consideration when using `RealmDictionary.contains`. +* Not possible to open an encrypted file on a device with a page size bigger than the one on which the file was produced. ([#8030](https://github.com/realm/realm-swift/issues/8030), since v12.11.0) +* Fix no notification for write transaction that contains only change to backlink property. ([#4994](https://github.com/realm/realm-core/issues/4994), since v11.4.1) + +### Compatibility +* File format: Generates Realms with format v23. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 13.9.0, commit 063927de66f79a0afffbbe36c0bb14d27deba8f2. + + +## 10.13.3-transformer-api (2023-03-16) + +### Enhancements +* None. + +### Fixed +* Added support for automatic handling of orphan embedded objects after migrating regular object properties to become embedded objects. (Issue [#7769](https://github.com/realm/realm-java/issues/7769)). +* Unit tests not being executed. (Issue [#7771](https://github.com/realm/realm-java/issues/7771)) +* Instrumented unit tests failed to execute because of the Realm dependencies being missing. (Issue [#7736](https://github.com/realm/realm-java/issues/7736)) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* None. + + +## 10.13.2-transformer-api (2023-01-27) + +### Enhancements +* None. + +### Fixed +* Fix zip path separator for transformer on Windows (Issue [#7757](https://github.com/realm/realm-java/issues/7757)) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* None. + + +## 10.13.1-transformer-api (2023-01-16) + +### Enhancements +* None. + +### Fixed +* Add support for Gradle configuration cache. (Issue [#7299](https://github.com/realm/realm-java/issues/7299)) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* None. + +### Credits +* Thanks to @pstavytskyi-turo for adding support for Gradle configuration cache. (Issue [#7299](https://github.com/realm/realm-java/issues/7299)) + + +## 10.13.1 (2023-03-16) + +### Enhancements +* None. + +### Fixed +* Added support for automatic handling of orphan embedded objects after migrating regular object properties to become embedded objects. (Issue [#7769](https://github.com/realm/realm-java/issues/7769)). + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* None. + + +## 10.13.0-transformer-api (2012-12-12) + +### Enhancements +* [RealmApp] Added option for working with Device Sync from an internal network. `SyncConfiguration.trustedRootCA(assetPath)` can embed a custom certificate in the app that will be used by Sync. (Issue [#7739](https://github.com/realm/realm-java/pull/7739)). +* [RealmApp] Added option for working with Device Sync from an internal network. `SyncConfiguration.disableSSLVerification()` makes it possible to turn off local SSL validation. (Issue [#7739](https://github.com/realm/realm-java/pull/7739)). + +### Fixed +* Fixed database corruption and encryption issues on apple platforms. (Issue [#5076](https://github.com/realm/realm-js/issues/5076)) +* [Sync] Bootstraps will not be applied in a single write transaction - they will be applied 1MB of changesets at a time. (Issue [#5999](https://github.com/realm/realm-core/pull/5999)). +* [Sync] Fixed a race condition which could result in operation cancelled errors being delivered to `Realm.open` rather than the actual sync error which caused things to fail. (Issue [#5968](https://github.com/realm/realm-core/pull/5968)). + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 12.13.0, commit b77443ca7fa25407869ca537bf3ae912b1427bff. + + +## 10.13.0 (2022-12-05) + +### Enhancements +* [RealmApp] Added option for working with Device Sync from an internal network. `SyncConfiguration.trustedRootCA(assetPath)` can embed a custom certificate in the app that will be used by Sync. (Issue [#7739](https://github.com/realm/realm-java/pull/7739)). +* [RealmApp] Added option for working with Device Sync from an internal network. `SyncConfiguration.disableSSLVerification()` makes it possible to turn off local SSL validation. (Issue [#7739](https://github.com/realm/realm-java/pull/7739)). + +### Fixed +* Fixed database corruption and encryption issues on apple platforms. (Issue [#5076](https://github.com/realm/realm-js/issues/5076)) +* [Sync] Bootstraps will not be applied in a single write transaction - they will be applied 1MB of changesets at a time. (Issue [#5999](https://github.com/realm/realm-core/pull/5999)). +* [Sync] Fixed a race condition which could result in operation cancelled errors being delivered to `Realm.open` rather than the actual sync error which caused things to fail. (Issue [#5968](https://github.com/realm/realm-core/pull/5968)). + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 12.13.0, commit b77443ca7fa25407869ca537bf3ae912b1427bff. + + +## 10.12.0-transformer-api (2022-09-28) + +### Breaking Changes +* Only works with Android Gradle Plugin 7.4 or newer. (Issue [#7714](https://github.com/realm/realm-java/issues/7714)) + +### Enhancements +* [RealmApp] Introduced `SyncSession.RecoverOrDiscardUnsyncedChangesStrategy`, an alternative automatic client reset strategy that tries to automatically recover any unsynced data from the client, and discards any unsynced data if not possible. This is now the default client reset policy if not explicitly set in the `SyncConfiguration`. +* [RealmApp] Introduced `SyncSession.RecoverUnsyncedChangesStrategy`, an alternative automatic client reset strategy that tries to automatically recover any unsynced data from the client and will revert to manual client reset if not possible. +* [RealmApp] Flexible sync client reset is no longer limited to `ManuallyRecoverChangesStrategy`, it now supports all available strategies: `RecoverOrDiscardUnsyncedChangesStrategy`, `RecoverUnsyncedChangesStrategy`, `DiscardUnsyncedChangesStrategy` and `ManuallyRecoverChangesStrategy`. + +### Fixed +* Now queries can point to fields with query language-reserved words like 'desc', 'sort', 'distinct', etc. Issue [#7705](https://github.com/realm/realm-java/issues/7705) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Update to Realm Core 12.6.0, commit: 5da7744b4056ad185c025bccf0924f17f73f7a91. + + +## 10.12.0 (2022-09-22) + +### Enhancements +* [RealmApp] Introduced `SyncSession.RecoverOrDiscardUnsyncedChangesStrategy`, an alternative automatic client reset strategy that tries to automatically recover any unsynced data from the client, and discards any unsynced data if not possible. This is now the default client reset policy if not explicitly set in the `SyncConfiguration`. +* [RealmApp] Introduced `SyncSession.RecoverUnsyncedChangesStrategy`, an alternative automatic client reset strategy that tries to automatically recover any unsynced data from the client and will revert to manual client reset if not possible. +* [RealmApp] Flexible sync client reset is no longer limited to `ManuallyRecoverChangesStrategy`, it now supports all available strategies: `RecoverOrDiscardUnsyncedChangesStrategy`, `RecoverUnsyncedChangesStrategy`, `DiscardUnsyncedChangesStrategy` and `ManuallyRecoverChangesStrategy`. + +### Fixed +* Now queries can point to fields with query language-reserved words like 'desc', 'sort', 'distinct', etc. Issue [#7705](https://github.com/realm/realm-java/issues/7705) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Update to Realm Core 12.6.0, commit: 5da7744b4056ad185c025bccf0924f17f73f7a91. + + +## 10.11.1 (2022-07-14) + +### Enhancements +* None + +### Fixed +* Fixed deadlock while trying to close all Realm instances during a manual client reset. Issue [#7696](https://github.com/realm/realm-java/pull/7696)) +* [RealmApp] Throw RuntimeException if subscription set is requested and flexible sync is not enabled. (Realm Core issue [#5079](https://github.com/realm/realm-core/issues/5079)) +* Adding an object to a Set, deleting the parent object, and then deleting the previously mentioned object causes crash. (Realm Core issue [#5387](https://github.com/realm/realm-core/issues/5387), since 11.0.0) +* [RealmApp] The sync client may have sent a corrupted upload cursor leading to a fatal error from the server due to an uninitialized variable. ([#5460](https://github.com/realm/realm-core/pull/5460, since v11.14.0) +* [RealmApp] Flexible sync would not correctly resume syncing if a bootstrap was interrupted. ([#5466](https://github.com/realm/realm-core/pull/5466, since v11.8.0) +* [RealmApp] Flexible sync subscription state changes will now correctly be reported after sync progress is reported. ([#5553](https://github.com/realm/realm-core/pull/5553, since v12.0.0) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Update to Realm Core 12.3.0, commit: 55a48c287b5e3a8ca129c257ec7e3b92bcb2a05f. + + +## 10.11.0 (2022-05-20) + +### Enhancements +* Throw a more comprehensive error when initializing Realm on an Instant App. + +### Fixed +* Fixed various corruption bugs when encryption is used. (Realm Core issue [#5360](https://github.com/realm/realm-core/issues/5360), since 10.10.0) +* Fixed imprecise conversion from double/float to Decimal128. (Realm Core issue [#5191](https://github.com/realm/realm-core/pull/5191)) +* Fixed `RealmQuery.distinct` when it receives three or more arguments. (Issue [#7639](https://github.com/realm/realm-java/pull/7639)) +* Fix issues resolving class information for `copyToRealmOrUpdate` for already managed objects in multi module projects. (Issue [#7650](https://github.com/realm/realm-java/issues/7650)) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Update to Realm Core 11.14.0, commit: db7ca86cf7ff8c9c3da6c7e742ecd46315ddc280. + +### Credits +* Thanks to @Mr4Mike4 for fixing `RealmQuery.distinct` when it receives three or more arguments ([#7639](https://github.com/realm/realm-java/pull/7639)). +* Thanks to @Waboodoo for fixing some typos ([#7646](https://github.com/realm/realm-java/pull/7646)). +* Thanks to @ZherebtsovAlexandr for updating the use of the deprecated method `offer` to `trySend` ([#7648](https://github.com/realm/realm-java/pull/7648)). + + +## 10.10.1 (2022-01-26) + +### Enhancements +* [RealmApp] Add support for setting the filename on Flexible and Partition Sync configurations. + +### Fixed +* [RealmApp] Creating multiple anonymous subscriptions for a Realm would throw an exception. + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Android Gradle Plugin 7.1.0 +* Updated to Gradle 7.3.3. + + +## 10.10.0 (2022-01-18) + +### Enhancements +* [RealmApp] Add support for a new mode for synchronized realms: Flexible Sync that only synchronizes selective parts of the backend data. The following classes have been added to support this: `Subscription`, `SubscriptionSet` and `MutableSubscriptionSet`. This mode and all APIs are marked as Beta. + +### Fixed +* [RealmApp] The sync client will now drain the receive queue when send fails with ECONNRESET - ensuring that any error message from the server gets received and processed. (Realm Core issue [#5078](https://github.com/realm/realm-core/pull/5078)) +* [RealmApp] UserIdentity metadata table grows indefinitely. (Realm Core issue [#5152](https://github.com/realm/realm-core/issues/5152)) +* Schema validation was missing for embedded objects in sets, resulting in an unhelpful error being thrown if the user attempted to define one. +* Output from the annotation processor was not deterministic, which could result in cache misses. (Issue [#7615](https://github.com/realm/realm-java/issues/7615)) +* Crashes when using `RealmAny` inside `RealmList` on ARM 32 devices. (Issue [#7626](https://github.com/realm/realm-java/issues/7626)) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Update to Realm Core 11.9.0, commit: 733f12702d16ab0d0c7fea0831a2aee5ca5c26db. + +### Credits +* Thanks to @jprinet for making the annotation processor output deterministic. + + +## 10.9.0 (2021-12-06) + +### Enhancements +* [RealmApp] Add support for UUID's as partition values. (Issue [#7598](https://github.com/realm/realm-java/issues/7598)) +* [RealmApp] Reduced native memory usage when working with synchronized Realms. +* [RealmApp] Make it possible to bundle synchronized Realms in apps using `Realm.writeCopyTo()` and `SyncConfiguration.Builder.assetFile()`. +* The Realm Transformer and Realm Gradle Plugin now supports the Gradle Configuration Cache. (Issue [#7299](https://github.com/realm/realm-java/issues/7299)) +* [RealmApp] Introduced `SyncSession.DiscardUnsyncedChangesStrategy`, an alternative automatic client reset strategy that doesn't require the Realm to be closed, but discards any unsynced data from the client. This is now the default policy if not overridden. + +### Deprecated +* [RealmApp] `SyncSession.ClientResetHandler()`. Use `SyncSession.ManuallyRecoverUnsyncedChangesStrategy()` instead. +* [RealmApp] `AppConfiguration.Builder.defaultClientResetHandler()`. Use `AppConfiguration.Builder.setDefaultSyncClientResetStrategy()` instead. +* [RealmApp] `AppConfiguration.getDefaultClientResetHandler()`. Use `AppConfiguration.getDefaultSyncClientResetStrategy()` instead. +* [RealmApp] `SyncConfiguration.Builder.clientResetHandler()`. Use `SyncConfiguration.Builder.setSyncClientResetStrategy()` instead. +* [RealmApp] `SyncConfiguration.getClientResetHandler()`. Use `SyncConfiguration.getSyncClientResetStrategy()` instead. + +### Fixed +* [RealmApp] Setting `AppConfiguration.syncRootDirectory()` didn't have any effect beside creating the new folder. Realms were still placed in the default location. +* [RealmApp] Bug where progress notifiers continue to be called after the download of a synced realm is complete. (Issue [Realm Core #4919](https://github.com/realm/realm-core/issues/4919)) +* [RealmApp] User being left in the logged in state when the user's refresh token expires. (Issue [Realm Core #4882](https://github.com/realm/realm-core/issues/4882), since v10) +* Using "sort", "distinct", or "limit" as field name in query expression would cause an "Invalid predicate" error. (Issue [#7545](), since v10.X.X) +* Crash when quering with 'Not()' followed by empty group. (Issue [Realm Core #4168]() since v1.0.0) +* Streaming download notifiers reported incorrect values for transferrable bytes. (Issue [Realm Core #5008]() since v11.5.2) +* `@sum` and `@avg` queries on Dictionaries of floats or doubles used too much precision for intermediates, resulting in incorrect rounding. + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 11.7.0, commit: 81eafa44879eb5f5829b345005abf99adb306133. +* Building the SDK now requires JDK 11. +* Updated to Gradle 7.2. +* Updated to Android Gradle Plugin 7.1.0-beta03. +* Updated to Kotlin 1.5.31. +* Updated to Kotlin Coroutines 1.5.2. +* Updated to CMake 3.21.4. +* Updated to NDK 23.1.7779620. +* Disable analytics for any value of the `REALM_DISABLE_ANALYTICS` environment variable, not just `true`. +* Disable analytics whenever the `CI` environment variable is set. + + +## 10.8.1 (2021-10-28) + +### Enhancements +* None. + +### Fixed +* [RealmApp] Failing to refresh the access token due to a 401/403 error will now correctly emit an error with `ErrorCode.BAD_AUTHENTICATION` rather than `ErrorCode.PERMISSION_DENIED`. (Realm Core [#4881](https://github.com/realm/realm-core/issues/4881), since 10.6.1) +* [RealmApp] If an object with a null primary key was deleted by another sync client, the exception `KeyNotFound: No such object` could be triggered. ([Realm Core #4885](https://github.com/realm/realm-core/issues/4885), since 10.0.0) +* Exceptions inside change listeners running on background looper threads would crash the Looper with a native `JNI DETECTED ERROR IN APPLICATION: JNI NewLocalRef called with pending exception` instead of the original Java exception. This could also happen when canceling a corutine using a background looper as a Dispatcher. +* [RealmApp] Reduced native memory use when synchronizing changes with the server in the background. + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 11.4.1, commit: 23f60515a00f076a9e3f2dc672fe1ae07601ee90. + + +## 10.8.0 (2021-08-27) + +### Enhancements +* [RealmApp] `ErrorCode.INVALID_EMAIL_PASSWORD` has been added, and is now thrown instead of `ErrorCode.SERVICE_UNKNOWN` when loggin in with the wrong credentials. +* `RealmQuery.rawPredicate()` now accepts a "BETWEEN" operator. Can be used like "age BETWEEN {20, 60}" which means "'Age' must be in the open interval ]20;60[". +* [RealmApp] Added `User.remove()` and `User.removeAsync()` that makes it possible to delete a user's Realm(s) from the device. + +### Fixed +* [RealmApp] Crash when integrating a schema from the server with a `RealmAny` property to a Realm File that already had that property defined locally. ([Realm Core #4873](https://github.com/realm/realm-core/issues/4873), since 10.0.0) +* [RealmApp] Refreshing the access token after 30 minutes would fail silently, causing infinite retries every 10 seconds. This would also block opening Realms when opening an app with an already logged in user. (Issue [#7501](https://github.com/realm/realm-java/issues/7501), since 10.0.0) +* [RealmApp] Clarified Javadoc for `User.logOut()` and `User.logOutAsync()` as these methods do not delete a user's Realm(s). +* Build error when having cross module model references (Issue [#7474](https://github.com/realm/realm-java/issues/7474), since v10.4.0) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 11.3.0, commit: 321c79a67119db8177af13eefd5378586648ba73. + + +## 10.7.1 (2021-08-03) + +### Enhancements +* None. + +### Fixed +* [RealmApp] Crash when an object which is linked to by a `RealmAny` is invalidated (Sync only). ([Realm Core #4828](https://github.com/realm/realm-core/issues/4828), since v10.6.0) +* Object change listeners did not handle the object being deleted properly, which could result in assertion failures mentioning "m_table" in ObjectNotifier ([Realm Core #4824](https://github.com/realm/realm-core/issues/4824), since v10.6.0). +* Crash when delivering notifications over a nested hierarchy of lists of `RealmAny` that contain object references. ([Realm Core #4803](https://github.com/realm/realm-core/issues/4803), since v10.6.0) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 11.2.0, commit: 583fc73040709383470797813096bee17802398e. + + +## 10.7.0 (2021-07-27) + +### Breaking Changes +* Removed automatic injection of repositories from Gradle plugin. From now on `mavenCentral()` repository needs to be added manually. (Issue [#7365](https://github.com/realm/realm-java/issues/7365)) + +### Enhancements +* None. + +### Fixed +* [RealmApp] Realm.getInstanceAsync does not wait for the initial remote data. (Issue [#7517](https://github.com/realm/realm-java/issues/7517)) +* Build errors when doing incremental builds with Android Studio's _Apply Changes..._-actions. (Issue [#7473](https://github.com/realm/realm-java/issues/7473)) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 11.1.1, commit: 71db56caba8f8ef0398eedfffb82a908cb94ccec. + + +## 10.6.1 (2021-07-01) + +### Enhancements +* None. + +### Fixed +* [RealmApp] Configuring HTTP timeout through `AppConfiguration.Builder.requestTimeout()` did not work correctly. (Issue [#7455](https://github.com/realm/realm-java/issues/7455)) +* [RealmApp] A recursive loop that would eventually crash trying to refresh a user app token when it had been revoked by an admin. Now this situation logs the user out and reports an error. (Issue [#7501](https://github.com/realm/realm-java/issues/7501)) +* An endless recursive loop that could cause a stack overflow when computing changes on a set of objects which contained cycles. (Realm Core Issue [#4767](https://github.com/realm/realm-core/issues/4767)) +* Opening cached Realms no longer trigger `android.os.strictmode.DiskReadViolation`. (Issue [#7500](https://github.com/realm/realm-java/issues/7500])) +* `NullPointerException` was thrown instead of `IllegalStateException` when calling `Realm.executeTransaction()` on a closed Realm. (Issue [#7511](https://github.com/realm/realm-java/issues/7511), since 10.0.0) +* `RealmDictionary` did not handle hash collisions correctly. (Realm Core issue [#4776](https://github.com/realm/realm-core/issues/4767)) +* Crash after clearing a List or Set of `RealmAny` containing references to objects (Realm Core issue [#4774](https://github.com/realm/realm-core/issues/4774)) + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 11.0.4, commit: 44304ce6104c4a9fc7e2359990c75be3b867b8fe. + + +## 10.6.0 (2021-06-15) + +This release combines all changes from 10.6.0-BETA.1 and 10.6.0-BETA.2. + +### Breaking Changes +* [RealmApp] Sync protocol version increased to 3. This version adds support for the new data types introduced in file format version 21. +* Primary keys now have automatic indexes again. Indexes was removed in v10.0.0 because they were not needed, but it caused issues when upgrading from a pre v10 version of Realm, and in some cases resulted in large delays when upgrading the fileformat. (Issue [#7426](https://github.com/realm/realm-java/issues/7426), since 10.0.0). +* Queries no longer do nullability checks on non-nullable fields, so using `null` as an argument will not throw an `IllegalArgumentException`. +* String query filters `contains`, `beginsWith`, `endsWith`, and `like`, now throw a null pointer exception on null values. +* The query builder no longer throw `IllegalStateException` but `IllegalArgumentException`. +* The `distinct` query filter on unsupported fields no longer throws an exception when applied through when querying across relationships. +* The `distinct` query filter no longer throws an exception when applied on non-existent fields. +* `RealmFieldType` has been updated to account for the new types being added. + +### Enhancements +* Added support for `java.util.UUID` as supported field in model classes. +* Added support for `java.util.UUID` as a primary key. +* Added support for `RealmAny` as supported field in model classes. A `RealmAny` is used to represent a polymorphic Realm value or Realm Object, is indexable but cannot be used as a primary key. See [Javadoc for RealmAny](https://docs.mongodb.com/realm-sdks/java/latest/io/realm/RealmAny.html). +* Added support for `RealmDictionary` as supported field in model classes. A `RealmDictionary` is a `Map` of strings to values - all types under the `RealmAny` umbrella can be used as values. See [Javadoc for RealmDictionary](https://docs.mongodb.com/realm-sdks/java/latest/io/realm/RealmDictionary.html) and [Javadoc for RealmMap](https://docs.mongodb.com/realm-sdks/java/latest/io/realm/RealmMap.html). `RealmDictionary` is not yet supported by any of the `Realm.insert` and `Realm.createFromJson` methods - This support will be added in a future release. +* Added support for `RealmSet` as supported field in model classes. A `RealmSet` is a collection that implements the Java `Set` interface and contains no duplicate values - all types under the `RealmAny` umbrella can be used as values. See [Javadoc for RealmSet](https://docs.mongodb.com/realm-sdks/java/latest/io/realm/RealmSet.html). `RealmSet` is not yet supported by any of the `Realm.insert` and `Realm.createFromJson` methods - This support will be added in a future release. +* Allow UTF8 encoded characters in property names in string-based queries ([#4467](https://github.com/realm/realm-core/issues/4467)) +* The error message when the initial steps of opening a Realm file fails is now more descriptive. +* Make conversion of Decimal128 to/from string work for numbers with more than 19 significant digits. ([#4548](https://github.com/realm/realm-core/issues/4548)) +* Remove type coercion on bool and ObjectId when doing queries. +* Allow passing arguments into string-based query predicates. +* Queries across relationships now support the `between` operator. +* Queries on numerical fields (byte, short, int, long, float, double, decimal128) now accept any numerical value as an argument. +* `isEmpty` query filter can now be applied on `RealmList` and `RealmObject` fields. + +### Fixed +* Fix assertion failures such as "!m_notifier_skip_version.version" or "m_notifier_sg->get_version() + 1 == new_version.version" when performing writes inside change notification callbacks. Previously refreshing the Realm by beginning a write transaction would skip delivering notifications, leaving things in an inconsistent state. Notifications are now delivered recursively when needed instead. ([Cocoa #7165](https://github.com/realm/realm-cocoa/issues/7165)). +* Fixed name aliasing not working in sort/distinct clauses when doing string-based queries. ([#4550](https://github.com/realm/realm-core/issues/4550), never before working). +* Potential/unconfirmed fix for crashes associated with failure to memory map (low on memory, low on virtual address space). For example ([#4514](https://github.com/realm/realm-core/issues/4514)). +* Syncing large Decimal128 values will cause "Assertion failed: cx.w[1] == 0" ([#4519](https://github.com/realm/realm-core/issues/4519), since v10.0.0) +* Classes names "class_class_..." were not handled correctly when doing queries ([#4480](https://github.com/realm/realm-core/issues/4480)) +* Fix collection notification reporting for modifications. This could be observed by receiving the wrong indices of modifications on sorted or distinct results, or notification blocks sometimes not being called when only modifications have occurred. ([#4573](https://github.com/realm/realm-core/pull/4573) since v6). + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 11.0.3, commit de25ad9db783f931e7652d5c1431d5610b2ad67b. + + +## 10.6.0-BETA.2 (2021-06-14) + +### Breaking Changes +* `MapChangeSet.getDeletionsCount()` has been replaced with `MapChangeSet.getDeletions()` that return the keys for entries that has been deleted instead of just the number of deleted entries. +* Primary keys now have automatic indexes again. Indexes was removed in v10.0.0 because they were not needed, but it caused issues when upgrading from a pre v10 version of Realm, and in some cases resulted in large delays when upgrading the fileformat. (Issue [#7426](https://github.com/realm/realm-java/issues/7426), since 10.0.0). + +### Enhancements +* Allow `insert` and `insertOrUpdate` operations on `RealmObject` or `RealmObject` collections containing `RealmDictionary` or `RealmSet` fields. +* Added support for `RealmDictionary` in `DynamicRealmObject` with `setDictionary(String fieldName, RealmDictionary dictionary)`, `getDictionary(String fieldName, Class primitiveType)`, and `getDictionary(String fieldName)`. +* Added support for `RealmSet` in `DynamicRealmObject` with `setRealmSet(String fieldName, RealmSet realmSet)`, `getRealmSet(String fieldName, Class primitiveType)`, and `getRealmSet(String fieldName)`. + +### Fixed +* Removed wrong `@Nullable` annotation on `RealmQuery.maxRealmAny()`. +* Fixed `RealmAny.getValueClass()` returning the `RealmObject` proxy class instead of the model class on a `RealmAny` referencing a managed `RealmObject`. + +### Compatibility +* File format: Generates Realms with format v22. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 11.0.2, commit a30382469eb72c0cf1824b44e7062071c2f3f3a9. +* Updated to Gradle 6.8.3. + + +## 10.6.0-BETA.1 (2021-05-17) + +### Breaking Changes +* [RealmApp] Sync protocol version increased to 3. This version adds support for the new data types introduced in file format version 21. +* File format version bumped to 21. In this version we support new basic datatypes `UUID` and `RealmAny`, as well as `RealmSet` and `RealmMap` collections with string-based keys (i.e. `RealmDictionary`). +* Queries no longer do nullability checks on non-nullable fields, so using `null` as an argument will not throw an `IllegalArgumentException`. +* String query filters `contains`, `beginsWith`, `endsWith`, and `like`, now throw a null pointer exception on null values. +* The query builder no longer throw `IllegalStateException` but `IllegalArgumentException`. +* The `distinct` query filter on unsupported fields no longer throws an exception when applied through when querying across relationships. +* The `distinct` query filter no longer throws an exception when applied on non-existent fields. + +### Enhancements +* Added support for `java.util.UUID` as supported field in model classes. +* Added support for `java.util.UUID` as a primary key. +* Added support for `RealmAny` as supported field in model classes. A `RealmAny` is used to represent a polymorphic Realm value or Realm Object, is indexable but cannot be used as a primary key. See [Javadoc for RealmAny](https://docs.mongodb.com/realm-sdks/java/latest/io/realm/RealmAny.html). +* Added support for `RealmDictionary` as supported field in model classes. A `RealmDictionary` is a `Map` of strings to values - all types under the `RealmAny` umbrella can be used as values. See [Javadoc for RealmDictionary](https://docs.mongodb.com/realm-sdks/java/latest/io/realm/RealmDictionary.html) and [Javadoc for RealmMap](https://docs.mongodb.com/realm-sdks/java/latest/io/realm/RealmMap.html). `RealmDictionary` is not yet supported by any of the `Realm.insert` and `Realm.createFromJson` methods - This support will be added in a future release. +* Added support for `RealmSet` as supported field in model classes. A `RealmSet` is a collection that implements the Java `Set` interface and contains no duplicate values - all types under the `RealmAny` umbrella can be used as values. See [Javadoc for RealmSet](https://docs.mongodb.com/realm-sdks/java/latest/io/realm/RealmSet.html). `RealmSet` is not yet supported by any of the `Realm.insert` and `Realm.createFromJson` methods - This support will be added in a future release. +* Allow UTF8 encoded characters in property names in string-based queries ([#4467](https://github.com/realm/realm-core/issues/4467)) +* The error message when the initial steps of opening a Realm file fails is now more descriptive. +* Make conversion of Decimal128 to/from string work for numbers with more than 19 significant digits. ([#4548](https://github.com/realm/realm-core/issues/4548)) +* Remove type coercion on bool and ObjectId when doing queries. +* Allow passing arguments into string-based query predicates. +* Queries across relationships now support the `between` operator. +* Queries on numerical fields (byte, short, int, long, float, double, decimal128) now accept any numerical value as an argument. +* `isEmpty` query filter can now be applied on `RealmList` and `RealmObject` fields. + +### Fixed +* Fix assertion failures such as "!m_notifier_skip_version.version" or "m_notifier_sg->get_version() + 1 == new_version.version" when performing writes inside change notification callbacks. Previously refreshing the Realm by beginning a write transaction would skip delivering notifications, leaving things in an inconsistent state. Notifications are now delivered recursively when needed instead. ([Cocoa #7165](https://github.com/realm/realm-cocoa/issues/7165)). +* Fixed name aliasing not working in sort/distinct clauses when doing string-based queries. ([#4550](https://github.com/realm/realm-core/issues/4550), never before working). +* Potential/unconfirmed fix for crashes associated with failure to memory map (low on memory, low on virtual address space). For example ([#4514](https://github.com/realm/realm-core/issues/4514)). +* Syncing large Decimal128 values will cause "Assertion failed: cx.w[1] == 0" ([#4519](https://github.com/realm/realm-core/issues/4519), since v10.0.0) +* Classes names "class_class_..." were not handled correctly when doing queries ([#4480](https://github.com/realm/realm-core/issues/4480)) +* Fix collection notification reporting for modifications. This could be observed by receiving the wrong indices of modifications on sorted or distinct results, or notification blocks sometimes not being called when only modifications have occurred. ([#4573](https://github.com/realm/realm-core/pull/4573) since v6). + +### Compatibility +* File format: Generates Realms with format v21. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.6.y series. +* Realm Studio 11.0.0-alpha.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 11.0.0-beta.4, commit: d50aef63a8aaf435e3afed82b589b47d8e1ab1ab. + + +## 10.5.1 (2021-06-14) + +### Enhancements +* None. + +### Fixes +* [RealmApp] Errors related to "uncaught exception in notifier thread: N5realm11KeyNotFoundE: No such object". This could happen in a sync'd app when a linked object was deleted by another client. +* [RealmApp] Replacing a referenced embedded object could result in a "ERROR: ArrayInsert: Invalid" error. (Issue [#7480](https://github.com/realm/realm-java/issues/7480)) +* Notifications now trigger correctly on Linux kernel 5.5 and above. So far this only impacted the preview emulator image for Android 12. (Issue[#7321](https://github.com/realm/realm-java/issues/7321)) +* Raw query predicates not supporting integer constants above 32 bits on a 32 bit platform. + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 10.8.1, commit 2a67b996faf9e0b5d47ac402c4d3926713f99418. + + +## 10.5.0 (2021-05-07) + +### Breaking Changes +* [RealmApp] `SyncSession.State.WaitingForAccessToken` has been added. It represents the local access token not longer being valid, but is automatically being refreshed. + +### Enhancements +* We now make a backup of a Realm file prior to any file format upgrade. The backup is retained for 3 months. Backups from before a file format upgrade allows for better analysis of any upgrade failure. We also restore a backup, if a) an attempt is made to open a realm file whith a "future" file format and b) a backup file exist that fits the current file format. The backup file is placed next to the real Realm file and is named `.v.backup.realm`. +* The error message when the intial steps of opening a Realm file fails is now more descriptive. + +### Fixes +* [RealmApp] Client Reset errors now correctly forward the server error message. (Issue [#7363](https://github.com/realm/realm-java/issues/7363), since 10.0.0) +* [RealmApp] All `AppException`s now correctly report the error message through `RuntimeException.getMessage()` instead of only through `AppException.getErrorMessage()`. +* [RealmApp] Proactively check the expiry time on the access token and refresh it before attempting to initiate a sync session. This prevents some error logs from appearing on the client such as: "ERROR: Connection[1]: Websocket: Expected HTTP response 101 Switching Protocols, but received: HTTP/1.1 401 Unauthorized" (RCORE-473, since v10.0.0). +* Fix name aliasing not working in sort/distinct clauses of raw string predicates. +* Fix collection notification reporting for modifications. This could be observed by receiving the wrong indices of modifications on sorted or distinct results, or notification sometimes not being called when only modifications have occured. (since v7.0.0). +* Make conversion of Decimal128 to/from string work for numbers with more than 19 significant digits. (#4548) + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 10.7.1, commit 5043c25e1d8f5971002e0fec85dea5ea3d7eb3d7. + + +## 10.4.0 (2021-03-26) + +All releases from 10.4.0 and forward are now found on `mavenCentral()` instead of `jcenter()`. + +A minimal supported setup will therefore now look like this: + +``` +allprojects { + buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "io.realm:realm-gradle-plugin:10.4.0" + } + } + + repositories { + mavenCentral() + } +} +``` + +`SNAPSHOT` releases have also been moved from `http://oss.jfrog.org/artifactory/oss-snapshot-local` +to `https://oss.sonatype.org/content/repositories/snapshots/`. See [here](https://github.com/realm/realm-java/blob/master/README.md#using-snapshots) +for more information. + +### Enhancements +* Added support for the string-based Realm Query Language through `RealmQuery.rawPredicate(...)`. This allows many new type of queries not previously supported by the typed query API. See the Javadoc on this method for further details. (Issue [#6116](https://github.com/realm/realm-java/pull/6116)) +* Performance of sorting on more than one property has been improved. Especially important if many elements match on the first property. + +### Fixes +* Calling max/min/sum/avg on a List may give wrong results (Realm Core [#4252](https://github.com/realm/realm-core/issues/4252), since v10.0.0) +* Fix an issue when using `RealmResults.freeze()` across threads with different transaction versions. Previously, copying the `RealmsResults`' native resource could result in a stale state or objects from a future version. (Realm Core [#4254](https://github.com/realm/realm-core/pull/4254)). +* On 32-bit devices you may get exception with "No such object" when upgrading to v10.* ([#7314](https://github.com/realm/realm-java/issues/7314), since v10.0.0) + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core 10.5.6, commit 92129110dece2cee59839e20be3a7067084a1196. +* Updated to NDK 22.0.7026061. +* Updated to ReLinker 1.4.3. + +## 10.3.1 (2021-01-28) + +### Enhancements +* None. + +### Fixes +* RxJava Flowables/Observables and Coroutine Flows would crash if they were created from a `RealmList` and the parent object holding the list was deleted. Now, the stream is disposed/closed instead. (Issue [#7242](https://github.com/realm/realm-java/issues/7242)) +* Fixes Realm models default values containing objects with a PK might crash with a `RealmPrimaryKeyConstraintException`. (Issue [#7269] (https://github.com/realm/realm-java/issues/7269)) + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 or above is required to open Realms created by this version. + +### Internal +* None. + + +## 10.3.0 (2021-01-08) + +### Enhancements +* [RealmApp] Upgraded to OpenSSL 1.1.1g. + +### Fixes +* [RealmApp] Integrating a remote Sync changeset into the local Realm could result in an `Index out of range error`. +* Change notifications not firing when removing and adding an object with the same primary key within a transaction (Issue [#7098](https://github.com/realm/realm-java/issues/7098)). +* Race condition which would lead to "uncaught exception in notifier thread: N5realm15InvalidTableRefE: transaction_ended" and a crash when the source Realm was closed or invalidated at a very specific time during the first run of a collection notifier (Core issue [#3761](https://github.com/realm/realm-core/issues/3761), since v7.0.0). +* Deleting and recreating objects with embedded objects could fail (Core issue [#4240](https://github.com/realm/realm-core/pull/4240), since v10.0.0) +* Added `@Nullable` annotation to input parameter in `RealmObject.isValid(item)` to avoid mismatch warnings from Kotlin code (Issue [#7216](https://github.com/realm/realm-java/issues/7216)). + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Core: 10.3.3 (Monorepo). +* Updated to Realm Core commit: 8af0f8d609491986b49f2c986e771d9dc445664d. + + +## 10.2.0 (2020-12-02) + +### Deprecated +* [RealmApp] `Credentials.google(authenticationCode: String)`. Use `Credentials.google(token: String, authType: GoogleAuthType)` instead. + +### Breaking Changes +* None. + +### Enhancements +* [RealmApp] Added `Credentials.google(token: String, authType: GoogleAuthType)`, as MongoDB Realm now supports multiple ways of logging into Google Accounts. + +### Fixes +* [RealmApp] Bug that would prevent eventual consistency during conflict resolution. Affected clients would experience data divergence and potentially consistency errors as a result if they experienced conflict resolution between cycles of Create-Erase-Create for objects with primary keys. +* Clean up JNI references to prevent crash from JNI reference table overflow (Issue [#7217](https://github.com/realm/realm-java/issues/7217)) + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Sync: 10.1.4. +* Updated to Object Store commit: f838a27402c5b5243280102014defd844420abba66eb93c10334507d9c0fd513. + + +## 10.1.2 (2020-12-02) + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixes +* Complementary fix for missed edge case in https://github.com/realm/realm-java/pull/7220 where KAPT crash if we process a RealmObject referencing a type in RealmList defined in another module. (Issue [#7213](https://github.com/realm/realm-java/issues/7213), since v10.0.0). + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 or above is required to open Realms created by this version. + + +## 10.1.1 (2020-11-27) + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixes +* KAPT crash when processing a RealmObject referenced from another module (changed revealed after we started checking for embedded types). (Issue [#7213](https://github.com/realm/realm-java/issues/7213), since v10.0.0). + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Sync: 10.1.3. +* Updated to Realm Core: 10.1.3. +* Updated to Object Store commit: fc6daca61133aa9601e4cb34fbeb9ec7569e162e. + + +## 10.1.0 (2020-11-23) + +### Breaking Changes +* None. + +### Enhancements +* Added `FlowFactory` interface that allows customization of `Flow` emissions, just as we do with `RxObservableFactory`. A default implementation, `RealmFlowFactory`, is provided when building `RealmConfiguration`s. +* Added `toChangeSetFlow` methods (similar to the Rx `asChangesetFlowable` methods) for `RealmObject`, `RealmResults` and `RealmList`. + +### Fixes +* Fixed crash when adding classes containing an `ObjectId` as primary key to the schema. (Issue [#7189](https://github.com/realm/realm-java/issues/7189), since v10.0.0) +* Fixed crash when creating proxy classes containing an `ObjectId` as primary key. (Issue [#7197](https://github.com/realm/realm-java/issues/7197), since v10.0.0) +* Fixed crash where calls to `toFlow` could crash if the Flow job is canceled and object updates are emitted after that happens. (Issue [7211](https://github.com/realm/realm-java/issues/7211), since v10.0.1) + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Sync: 10.1.3. +* Updated to Realm Core: 10.1.3. +* Updated to Object Store commit: fc6daca61133aa9601e4cb34fbeb9ec7569e162e. + + +## 10.0.1 (2020-11-06) + +### Breaking Changes +* None. + +### Enhancements +* Improved the error message for `NoSuchTable` errors. In some cases an outdated native reference was used,but the table was still there. In those cases an `InvalidTableRef` error is now used. + +### Fixes +* [RealmApp] The `SyncConfiguration.Builder.allowQueriesOnUiThread` flag was wrongly initialized to `false` keeping users from running queries from the UI thread when using synced Realms. It now defaults to `true`, allowing queries to be run from the UI. (Issue [#7177](https://github.com/realm/realm-java/issues/7177), since 10.0.0) +* Crash with `Assertion failed: m_method_id != nullptr with (method_name, signature) = ["", "(Ljava/lang/String;)V"]` when `Minify` is enabled. (Issue [#7159](https://github.com/realm/realm-java/pull/7159), since 10.0.0) +* Fix crash in case insensitive query on indexed string columns when nothing matches (Cocoa issue [#6836](https://github.com/realm/realm-cocoa/issues/6836), since v10.0.0) +* Fix list of primitives with nullable values where `Lst::is_null(ndx)` always false even on null values, (Core issue [#3987](https://github.com/realm/realm-core/pull/3987), since v10.0.0). +* Fix queries for the size of a list of primitive nullable ints returning size + 1. (Core issue [#4016](https://github.com/realm/realm-core/pull/4016), since v10.0.0). + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Sync: 10.1.0. +* Updated to Realm Core: 10.1.0. +* Updated to Object Store commit: fd246c54de7d1fee6bcbeb3609de75a4eccd5b70. + + +## 10.0.0 (2020-10-15) + +NOTE: This is a unified release note covering all v10.0.0-BETA.X v10.0.0-RC.X releases. + +NOTE: Support for syncing with realm.cloud.io and/or Realm Object Server has been replaced with support for syncing with MongoDB Realm Cloud. + +NOTE: This version upgrades the Realm file format to version 20. It is not possible to downgrade to earlier versions than v10.0.0-BETA.7. Non-sync Realms will be upgraded automatically. Synced Realms can only be automatically upgraded if created with Realm Java v10.0.0-BETA.1 and above. + +### Breaking Changes +* [RealmApp] Most APIs for interacting with Realm Cloud have changed significantly. All new APIs can be found in the `io.realm.mongodb` package. The entry point is through the `App` class from which you can create and login users and otherwise interact with MongoDB Realm. See [the docs](https://docs.mongodb.com/realm/android/) for further details. Synced Realms still use a `SyncConfiguration` that are largely created the same way. +* [RealmApp] Client Resets are now handled through a custom `SyncConfiguration.Builder.clientResetHandler()` instead of through the default session error handler `SyncConfiguration.Builder.errorHandler()` +* [RealmApp] Realm files have changed location on disk. They are now located in `getFiles()/mongodb-realm`. +* [RealmApp] All synced model classes not marked as embedded are required to have a primary key named `_id`. It is possible to use `@RealmField(name = "_id")` to map from any Java or Kotlin property. +* From now on it is by default not allowed to run transactions with either `Realm.executeTransaction()` or `DynamicRealm.executeTransaction()` from the UI thread. Doing so will yield a `RealmException`. Users can override this behavior by using `RealmConfiguration.Builder.allowWritesOnUiThread(true)` when building a `RealmConfiguration` to obtain a Realm or DynamicRealm instance, however, we do not recommend doing so. Instead, we recommend using `executeTransactionAsync()` or, alternatively, using non-UI threads when calling `executeTransaction()` for both `Realm`s and `DynamicRealm`s. + +### Enhancements +* Users can now opt out from allowing queries to be launched from the UI thread by using `RealmConfiguration.Builder.allowQueriesOnUiThread(false)`. A `RealmException` will be thrown when calling `RealmQuery.findAll()`, `RealmQuery.findFirst()`, `RealmQuery.minimumDate()`, `RealmQuery.maximumDate()`, `RealmQuery.count()`, `RealmQuery.sum()`, `RealmQuery.max()`, `RealmQuery.min()`, `RealmQuery.average()` and `RealmQuery.averageDecimal128()` from the UI thread after having used `allowQueriesOnUiThread(false)`. Queries will be allowed from the thread from which the Realm instance was obtained as it always has been by default, although we recommend using `RealmQuery.findAllAsync()` or `RealmQuery.findFirstAsync()`, or, alternatively, using a non-UI thread to launch them. +* `BaseRealm.refresh()` will throw a `RealmException` if it is being called from the UI thread if `allowQueriesOnUiThread` is set to `false`, though it will be allowed by default. +* Added `DynamicRealm.executeTransactionAsync()`. +* Added Kotlin extension suspend function `Realm.executeTransactionAwait()` which runs transactions inside coroutines. +* Added Kotlin extension function `RealmResults.toFlow()` which returns a Kotlin flow, similar to our RxJava convenience method `asFlowable()`. +* Added Kotlin extension function `RealmList.toFlow()` which returns a Kotlin flow, similar to our RxJava convenience method `asFlowable()`. +* Added Kotlin extension function `RealmModel.toFlow()` which returns a Kotlin flow, similar to our RxJava convenience method `asFlowable()`. +* RealmLists can now be marked final. (Issue [#6892](https://github.com/realm/realm-java/issues/6892)) +* Added support for `distinct` queries on non-index and linked fields. (Issue [#1906](https://github.com/realm/realm-java/issues/1906)) +* Added support for `org.bson.types.Decimal128` and `org.bson.types.ObjectId` as supported fields in model classes. +* Added support for `org.bson.types.ObjectId` as a primary key. +* Added support for "Embedded Objects". They are enabled using `@RealmClass(embedded = true)`. An embedded object must have exactly one parent object linking to it and it will be deleted when the parent is. Embedded objects can also be the parent of other embedded classes. Read more [here](https://docs.mongodb.com/realm/android/embedded-objects/). (Issue [#6713](https://github.com/realm/realm-java/issues/6713)) + + +### Fixes +* None. + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java v10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 or above is required to open Realms created by this version. + +### Internal +* Updated to Realm Sync: 10.0.0. +* Updated to Realm Core: 10.0.0. + + +## 10.0.0-RC.2 (2020-10-12) + +### Enhancements +* [RealmApp] Illegal schemas where embedded object classes referenced each other is now correctly detected and throws and exception when opening a Realm with such a schema. + +### Fixed +* [RealmApp] It is now possible to use types different than `ObjectId` for the `_id` field in documents inserted with `MongoCollection.insertOne` and `MongoCollection.insertMany`. +* [RealmApp] Lossy round trip of Double and Timestamps through functions when using Bson. (ObjectStore issue (#1106)[https://github.com/realm/realm-object-store/issues/1106]) + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java 10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 and above is required to open Realms created by this version. + +### Internal +* Updated to Object Store commit: 6b44209e6fcac0137e193c96444f93c50d184d06. + + +## 10.0.0-RC.1 (2020-10-02) + +We no longer support Realm Cloud (legacy), but instead the new MongoDB Realm Cloud. MongoDB Realm is a serverless platform that enables developers to quickly build applications without having to set up server infrastructure. MongoDB Realm is built on top of MongoDB Atlas, automatically integrating the connection to your database. + +The old Realm Cloud legacy APIs have undergone significant refactoring. The new APIs are all located in the `io.realm.mongodb` package with `io.realm.mongodb.App` as the entry point. + +### Breaking Changes +* From now on it is not allowed by default to run transactions with either `Realm.executeTransaction()` or `DynamicRealm.executeTransaction()` from the UI thread. Doing so will yield a `RealmException`. Users can override this behavior by using `RealmConfiguration.Builder.allowWritesOnUiThread(true)` when building a `RealmConfiguration` to obtain a Realm or DynamicRealm instance, though we do not recommend doing so. Instead, we recommend using `executeTransactionAsync()` or, alternatively, using non-UI threads when calling `executeTransaction()` for both `Realm`s and `DynamicRealm`s. + +### Enhancements +* Users can now opt out from allowing queries to be launched from the UI thread by using `RealmConfiguration.Builder.allowQueriesOnUiThread(false)`. A `RealmException` will be thrown when calling `RealmQuery.findAll()`, `RealmQuery.findFirst()`, `RealmQuery.minimumDate()`, `RealmQuery.maximumDate()`, `RealmQuery.count()`, `RealmQuery.sum()`, `RealmQuery.max()`, `RealmQuery.min()`, `RealmQuery.average()` and `RealmQuery.averageDecimal128()` from the UI thread after having used `allowQueriesOnUiThread(false)`. Queries will be allowed from the thread from which the Realm instance was obtained as it always has been by default, although we recommend using `RealmQuery.findAllAsync()` or `RealmQuery.findFirstAsync()`, or, alternatively, using a non-UI thread to launch them. +* `BaseRealm.refresh()` will throw a `RealmException` if it is being called from the UI thread if `allowQueriesOnUiThread` is set to `false`, though it will be allowed by default. +* Added `DynamicRealm.executeTransactionAsync()`. +* Added Kotlin extension suspend function `Realm.executeTransactionAwait()` which runs transactions inside coroutines. +* Added Kotlin extension function `RealmResults.toFlow()` which returns a Kotlin flow, similar to our RxJava convenience method `asFlowable()`. +* Added Kotlin extension function `RealmList.toFlow()` which returns a Kotlin flow, similar to our RxJava convenience method `asFlowable()`. +* Added Kotlin extension function `RealmModel.toFlow()` which returns a Kotlin flow, similar to our RxJava convenience method `asFlowable()`. + +### Fixed +* Using `Realm.copyToRealmOrUpdate()` and `Realm.insertOrUpdate()` did not correctly update objects if they contained lists of embedded objets. Instead of replacing the original list, list items was appended to the original list. Note, some corner cases are still not supported. See [#7138](https://github.com/realm/realm-java/issues/7138) for more information. (Issue [#7131](https://github.com/realm/realm-java/issues/7131), since 10.0.0-BETA.1). + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java 10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 and above is required to open Realms created by this version. + +### Internal +* Updated to Object Store commit: ef6736cc07a8b94d1242c522969114bb8047deef +* Updated to Realm Sync 10.0.0-beta.14. +* Updated to Realm Core 10.0.0-beta.9. + + +## 10.0.0-BETA.8 (2020-09-23) + +We no longer support Realm Cloud (legacy), but instead the new MongoDB Realm Cloud. MongoDB Realm is a serverless platform that enables developers to quickly build applications without having to set up server infrastructure. MongoDB Realm is built on top of MongoDB Atlas, automatically integrating the connection to your database. + +The old Realm Cloud legacy APIs have undergone significant refactoring. The new APIs are all located in the `io.realm.mongodb` package with `io.realm.mongodb.App` as the entry point. + +### Fixed +* [RealmApp] Logging in caused an `token contains an invalid number of segments` error. (Issue [#7117](https://github.com/realm/realm-java/issues/7117), since 10.0.0-BETA.7) +* [RealmApp] The order of arguments to `EmailPassword.resetPassword()` was not handled correctly, resulting in resetting the password failing. (Issue [#7116](https://github.com/realm/realm-java/issues/7116), since 10.0.0-BETA.1) + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java 10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 and above is required to open Realms created by this version. + +### Internal +* Updated to Object Store commit: 035eb07f3ef313bfb78c046be9cf6b4f065d6772. + + +## 10.0.0-BETA.7 (2020-09-16) + +We no longer support Realm Cloud (legacy), but instead the new MongoDB Realm Cloud. MongoDB Realm is a serverless platform that enables developers to quickly build applications without having to set up server infrastructure. MongoDB Realm is built on top of MongoDB Atlas, automatically integrating the connection to your database. + +The old Realm Cloud legacy APIs have undergone significant refactoring. The new APIs are all located in the `io.realm.mongodb` package with `io.realm.mongodb.App` as the entry point. + +WARNING: This release upgrades the fileformat to 20. Non-sync Realms will be upgraded automatically. Synced Realms can only be automatically upgraded if created with Realm Java 10.0.0-BETA.1 and above. + + +### Breaking Changes +* [RealmApp] Moved `User.remove()` to `App.removeUser()`. +* [RealmApp] Renamed `ApiKeyAuth.createApiKey()` to `ApiKeyAuth.create()` and `ApiKeyAuth.createApiKeyAsync()` to `ApiKeyAuth.createAsync()`. +* [RealmApp] Renamed `ApiKeyAuth.fetchApiKey()` to `ApiKeyAuth.fetch()` and `ApiKeyAuth.fetchApiKeyAsync()` to `ApiKeyAuth.fetchAsync()`. +* [RealmApp] Renamed `ApiKeyAuth.fetchAllApiKeys()` to `ApiKeyAuth.fetchAll()` and `ApiKeyAuth.fetchAllApiKeysAsync()` to `ApiKeyAuth.fetchAllAsync()`. +* [RealmApp] Renamed `ApiKeyAuth.deleteApiKey()` to `ApiKeyAuth.delete()` and `ApiKeyAuth.deleteApiKeyAsync()` to `ApiKeyAuth.deleteAsync()`. +* [RealmApp] Renamed `ApiKeyAuth.enableApiKey()` to `ApiKeyAuth.enable()` and `ApiKeyAuth.enableApiKeyAsync()` to `ApiKeyAuth.enableAsync()`. +* [RealmApp] Renamed `ApiKeyAuth.disableApiKey()` to `ApiKeyAuth.disable()` and `ApiKeyAuth.disableApiKeyAsync()` to `ApiKeyAuth.disableAsync()`. +* [RealmApp] Renamed `User.getApiKeysAuth()` to `User.getApiKeys()`. +* [RealmApp] Renamed `UserApiKey` class to `ApiKey`. +* [RealmApp] Removed support for `Credentials.serverApiKey()`. +* [RealmApp] Renamed `App.getEmailPasswordAuth()` to `App.getEmailPassword()`. +* [RealmApp] User profile methods `getName()`, `getEmail()`, `getPictureUrl()`, `getFirstName()`, `getLastName()`, `getGender()`, `getBirthday()`, `getMinAge()` and `getMaxAge()` are now available under a new class `UserProfile`. It can be accessed using `User.getProfile()`. +* [RealmApp] Renamed `Sync.refreshConnections()` to `Sync.reconnect()`. +* [RealmApp] Renamed `Credentials.IdentityProvider` to `Credentials.Provider`. +* [RealmApp] Removed support for `User.getLocalId()`. +* [RealmApp] Client Resets are now handled through a custom `SyncConfiguration.Builder.clientResetHandler()` instead of through the default session error handler `SyncConfiguration.Builder.errorHandler()` + +### Enhancements +* [RealmApp] It is now possible to create App instances with different app id's. +* [RealmApp] Support for using `null` as a partition value. +* [RealmApp] Improve errors exception messages from `SyncSession.downloadAllServerChanges()` and `SyncSession.uploadAllLocalChanges()`. +* [RealmApp] Support for watching MongoCollection change streams (Issue [#6912](https://github.com/realm/realm-java/issues/6912)) +* [RealmApp] Support for retrying a custom confirmation function on an User for a given email (Issue [#7079](https://github.com/realm/realm-java/pull/7079)) +* [RealmApp] Support for getting all app sessions via `Sync.getAllSessions()`. +* [RealmApp] Support to retrieve the MongoClient service name using `MongoClient.getServiceName()` +* [RealmApp] Support to retrieve the MongoDatabase name using `MongoDatabase.getName()` +* [RealmApp] Support to retrieve the MongoCollection name using `MongoCollection.getName()` + +### Fixed +* If you have a realm file growing towards 2Gb and have a table with more than 16 columns, then you may get a "Key not found" exception when updating an object. If asserts are enabled at the sdk level, you may get an "assert(m_has_refs)" instead. ([#3194](https://github.com/realm/realm-js/issues/3194), since v7.0.0) +* In cases where you have more than 32 columns in a table, you may get a currrupted file resulting in various crashes ([#7057](https://github.com/realm/realm-java/issues/7057), since v7.0.0) + +### Compatibility +* File format: Generates Realms with format v20. Unsynced Realms will be upgraded from Realm Java 2.0 and later. Synced Realms can only be read and upgraded if created with Realm Java 10.0.0-BETA.1. +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 and above is required to open Realms created by this version. + +### Internal +* Updated to Object Store commit: 6ab48d3b4b1e0865f68b84d5993bb2aad910320b. +* Updated to Realm Sync 10.0.0-beta.11. +* Updated to Realm Core 10.0.0-beta.7. + + +## 10.0.0-BETA.6 (2020-08-17) + +We no longer support Realm Cloud (legacy), but instead the new MongoDB Realm Cloud. MongoDB Realm is a serverless platform that enables developers to quickly build applications without having to set up server infrastructure. MongoDB Realm is built on top of MongoDB Atlas, automatically integrating the connection to your database. + +The old Realm Cloud legacy APIs have undergone significant refactoring. The new APIs are all located in the `io.realm.mongodb` package with `io.realm.mongodb.App` as the entry point. + +### Breaking Changes +* [RealmApp] Realm files have changed location on disk, so Realms should upload all their data to the server before upgrading. +* [RealmApp] Removed GMS Task framework and added RealmResultTask to provide with a mechanism to operate with asynchronous operations. MongoCollection has been updated to reflect this change. + +### Enhancements +* [RealmApp] Credentials information (e.g. username, password) displayed in Logcat is now obfuscated by default, even if [LogLevel] is set to DEBUG, TRACE or ALL. +* RealmLists can now be marked final. (Issue [#6892](https://github.com/realm/realm-java/issues/6892)) +* It is now possible to create embedded objects using [DynamicRealm]s. (Issue [#6982](https://github.com/realm/realm-java/pull/6982)) +* Added extra validation and more meaningful error messages when creating embedded objects pointing to the wrong parent property. (See issue above) + +### Fixed +* [RealmApp] The same user opening different Realms with different partion key values would crash with an IllegalArgumentException. (Issue [#6882](https://github.com/realm/realm-java/issues/6882), since 10.0.0-BETA.1) +* [RealmApp] Sync would not refresh the access token if started with an expired one. (Since 10.0.0-BETA.1) +* [RealmApp] Leaking objects when registering session listeners. (Issue [#6916](https://github.com/realm/realm-java/issues/6916)) +* Added support for Json-import of objects containing embedded objects. (Issue [#6896](https://github.com/realm/realm-java/issues/6896)) +* Upgrading the file format result did in some cases not work correctly. This could result in a number of crashes, e.g. `FORMAT_UPGRADE_REQUIRED`. (Issue [#6889](https://github.com/realm/realm-java/issues/6889), since 7.0.0) +* Bug in memory mapping management. This bug could result in multiple different asserts as well as segfaults. In many cases stack backtraces would include members of the EncyptedFileMapping near the top - even if encryption was not used at all. In other cases asserts or crashes would be in methods reading an array header or array element. In all cases the application would terminate immediately. (Realm Core PR [#3838](https://github.com/realm/realm-core/pull/3838), since 7.0.0) +* It was possible to use `RealmObjectSchema` to mark a Class as embedded even if some of the objects broke the constraints for being embedded. + +### Compatibility +* File format: Generates Realms with format v11 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 and above is required to open Realms created by this version. + +### Internal +* Upgraded to Object Store commit: 5b5fb8a90192cb4ee6799e7465745cd2067f939b. +* Upgraded to Realm Sync 10.0.0-beta.6. +* Upgraded to Realm Core 10.0.0-beta.4. + + +## 10.0.0-BETA.5 (2020-06-19) + +We no longer support Realm Cloud (legacy), but instead the new MongoDB Realm Cloud. MongoDB Realm is a serverless platform that enables developers to quickly build applications without having to set up server infrastructure. MongoDB Realm is built on top of MongoDB Atlas, automatically integrating the connection to your database. + +The old Realm Cloud legacy API's have undergone significant refactoring. The new API's are all located in the `io.realm.mongodb` package with `io.realm.mongodb.App` as the entry point. + +### Enhancements +* [RealmApp] Added support for Api Keys, Server Api Keys and Custom Functions as Credential types when logging in. +* Added support for `distinct` queries on non-index and linked fields. (Issue [#1906](https://github.com/realm/realm-java/issues/1906)) + +### Fixed +* None. + +### Compatibility +* File format: Generates Realms with format v11 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 and above is required to open Realms created by this version. + +### Internal +* Upgraded to Object Store commit: e1570f8d3d7cf4d77f049933e6a241a501301383. + +## 10.0.0-BETA.4 (2020-06-11) + +We no longer support Realm Cloud (legacy), but instead the new MongoDB Realm Cloud. MongoDB Realm is a serverless platform that enables developers to quickly build applications without having to set up server infrastructure. MongoDB Realm is built on top of MongoDB Atlas, automatically integrating the connection to your database. + +The old Realm Cloud legacy API's have undergone significant refactoring. The new API's are all located in the `io.realm.mongodb` package with `io.realm.mongodb.App` as the entry point. + +### Breaking Changes +* None. + +### Enhancements +* [RealmApp] Added support for Custom Data using `User.customData()` and `User.refreshCustomData()`. +* [RealmApp] Added support for managing push notifications using `App.getPush()`. + +### Fixed +* [RealmApp] Opening a synced Realm for a cached user with expired access token would crash the app with `Assertion failed: cls with (class_name) = ["io/realm/internal/objectstore/OsJavaNetworkTransport$Response"]`. (Issue [#6937](https://github.com/realm/realm-java/issues/6937), since 10.0.0-BETA.1) + +### Compatibility +* File format: Generates Realms with format v11 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 and above is required to open Realms created by this version. + +### Internal +* Updated to Object Store commit: 017d58fbec8a18ab003976b4c346308df88349a6. + + +## 10.0.0-BETA.3 (2020-06-09) + +We no longer support Realm Cloud (legacy), but instead the new MongoDB Realm Cloud. MongoDB Realm is a serverless platform that enables developers to quickly build applications without having to set up server infrastructure. MongoDB Realm is built on top of MongoDB Atlas, automatically integrating the connection to your database. + +The old Realm Cloud legacy API's have undergone significant refactoring. The new API's are all located in the `io.realm.mongodb` package with `io.realm.mongodb.App` as the entry point. + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixed +* [RealmApp] When restarting an app, the base URL used would in some cases be incorrect. (Since 10.0.0-BETA.2) + +### Compatibility +* File format: Generates Realms with format v11 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 and above is required to open Realms created by this version. + +### Internal +* Updated to Object Store commit: c02707bc28e1886970c5da29ef481dc0cb6c3dd8. + + +## 10.0.0-BETA.2 (2020-06-08) + +We no longer support Realm Cloud (legacy), but instead the new MongoDB Realm Cloud. MongoDB Realm is a serverless platform that enables developers to quickly build applications without having to set up server infrastructure. MongoDB Realm is built on top of MongoDB Atlas, automatically integrating the connection to your database. + +The old Realm Cloud legacy API's have undergone significant refactoring. The new API's are all located in the `io.realm.mongodb` package with `io.realm.mongodb.App` as the entry point. + +### Breaking Changes +* None. + +### Enhancements +* None. + +### Fixed +* [RealmApp] `AppConfiguration` did not fallback to the correct default baseUrl if none was provided. (Since 10.0.0-BETA.1) +* [RealmApp] When restarting an app, re-using the already logged in user would result in Sync not resuming. (Since 10.0.0-BETA.1) + +### Compatibility +* File format: Generates Realms with format v11 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 and above is required to open Realms created by this version. + +### Internal +* Updated to Object Store commit: c50be4dd178ef7e11d453f61a5ac2afa8c1c10bf. +* Updated to Realm Sync 10.0.0-beta.2. + + +## 10.0.0-BETA.1 (2020-06-05) + +We no longer support Realm Cloud (legacy), but instead the new MongoDB Realm Cloud. MongoDB Realm is a serverless platform that enables developers to quickly build applications without having to set up server infrastructure. MongoDB Realm is built on top of MongoDB Atlas, automatically integrating the connection to your database. + +The old Realm Cloud legacy API's have undergone significant refactoring. The new API's are all located in the `io.realm.mongodb` package with `io.realm.mongodb.App` as the entry point. + +### Breaking Changes +* [RealmApp] Removed all references and API's releated to permissions. These are now managed through MongoDB Realm. +* [RealmApp] Query Based Sync API's and Subscriptions. These API's are not initially supported by MongoDB Realm. They will be re-introduced in a future release. `SyncConfiguration.partitionKey()` has been added as a replacement. +* [RealmApp] Removed support for Client Resync. These API's are not initially supported by MongoDB Realm. They will be re-introduced in a future release. +* [RealmApp] Removed suppport for custom SSL certificates. These API's are not initially supported by MongoDB Realm. They will be re-introduced in a future release. +* [RealmApp] Destructive updates of a schema of a synced Realm will now consistently throw an `UnsupportedOperationException` instead of some methods throwing `IllegalArgumentException`. The affected methods are `RealmSchema.remove(String)`, `RealmSchema.rename(String, String)`, `RealmObjectSchema.setClassName(String)`, `RealmObjectSchema.removeField(String)`, `RealmObjectSchema.renameField(String, String)`, `RealmObjectSchema.removeIndex(String)`, `RealmObjectSchema.removePrimaryKey()`, `RealmObjectSchema.addPrimaryKey(String)` and `RealmObjectSchema.addField(String, Class, FieldAttribute)` + +### Enhancements +* Added support for `org.bson.types.Decimal128` and `org.bson.types.ObjectId` as supported fields in model classes. +* Added support for `org.bson.types.ObjectId` as a primary key. +* Added support for "Embedded Objects". They are enabled using `@RealmClass(embedded = true)`. An embedded object must have exactly one parent object linking to it and it will be deleted when the the parent is. Embedded objects can also be the parent of other embedded classes. Read more [here](https://realm.io/docs/java/latest/#embedded-objects). (Issue [#6713](https://github.com/realm/realm-java/issues/6713)) + +### Fixed +* After upgrading a Realm file, you may at some point receive a 'NoSuchTable' exception. (Issue [Core#3701](https://github.com/realm/realm-core/issues/3701), since 7.0.0) +* If the Realm file upgrade process was interrupted/killed for various reasons, the following run would some assertions failing. (Issue [#6866](https://github.com/realm/realm-java/issues/6866), since 7.0.0). + +### Compatibility +* File format: Generates Realms with format v11 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 10.x.y series. +* Realm Studio 10.0.0 and above is required to open Realms created by this version. + +### Internal +* Updated to Object Store commit: 6d081a53377514f9b77736cb03051a03d829da922. +* Updated to Realm Sync 10.0.0-beta.1. +* Updated to Realm Core 10.0.0-beta.1. +* OKHttp was upgraded to 3.12.0 from 3.10.0. +* Updated Android Gradle Plugin to 3.6.1. +* Updated Gradle to 5.6.4 +* Updated Dokka to 0.10.1 +* Updated Android Build Tools to 29.0.2. +* Updated compileSdkVersion to 29. + + +## 7.0.8 (2020-10-01) + +### Enhancements +* Slightly improve performance of most operations which read data from the Realm file. + +### Fixes +* Making a query in an indexed property may give a "Key not found" exception. (.NET issue [#2025](https://github.com/realm/realm-dotnet/issues/2025), since 7.0.0) +* Queries for null on non-nullable indexed integer properties could return wrong results if 0 entries should be found. (Since 7.0.0) +* Rerunning an equals query on an indexed string column which previously had more than one match and now has one match would sometimes throw a "key not found" exception. (Cocoa issue [#6536](https://github.com/realm/realm-cocoa/issues/6536), Since 7.0.0) + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* File format: Generates Realms with format v11 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 7.x.y series. + +### Internal +* Upgraded to Object Store commit: 8a68df3e9fa7743c13d927eb7fc330ed9bb06693. +* Upgraded to Realm Sync: 5.0.28. +* Upgraded to Realm Core: 6.1.3. + + +## 7.0.7 (2020-09-25) + +### Enhancements +* None. + +### Fixes +* When querying a class where object references are part of the condition, the application may crash if objects have recently been added to the target table. (Issue [#7118](https://github.com/realm/realm-java/issues/7118), since v7.0.0) + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* File format: Generates Realms with format v11 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 7.x.y series. + +### Internal +* Upgraded to Object Store commit: 37e86c2905bfd424c16fc5d7860a1298bfc0ffa2. +* Upgraded to Realm Sync: 5.0.25. +* Upgraded to Realm Core: 6.1.1. + + +## 7.0.6 (2020-09-18) + +### Enhancements +* Better exception messaging for UTF encoding errors. ([Issue #7093](https://github.com/realm/realm-java/pull/7093)) + +### Fixes +* Fixes concurrent modification exceptions in the schema when refreshing a Realm (Issue [#6876](https://github.com/realm/realm-java/issues/6876)) +* If you use encryption your application cound crash with a message like "Opening Realm files of format version 0 is not supported by this version of Realm". ([#6889](https://github.com/realm/realm-java/issues/6889) among others, since v7.0.0) + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* Realm Studio: 5.0.0 or later. +* File format: Generates Realms with format v11 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 7.x.y series. + +### Internal +* Upgraded to Object Store commit: e29b5515df8b8adfe2454424b78878bb63879307. +* Upgraded to Realm Sync: 5.0.23. +* Upgraded to Realm Core: 6.0.26. + + +## 7.0.5 (2020-09-09) + +### Enhancements +* None. + +### Fixes +* If you have a Realm file growing towards 2Gb and have a model class with more than 16 properties, then you may get a "Key not found" exception when updating an object. (Realm JS issue [#3194](https://github.com/realm/realm-js/issues/3194), since v7.0.0) +* In cases where you have more than 32 properties in a model class, you may get a currrupted file resulting in various crashes (Issue [#7057](https://github.com/realm/realm-java/issues/7057), since v7.0.0) + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* Realm Studio: 5.0.0 or later. +* File format: Generates Realms with format v11 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 7.x.y series. + +### Internal +* Upgraded to Realm Sync: 5.0.22. +* Upgraded to Realm Core: 6.0.25. + + +## 7.0.4 (2020-09-08) + +Note: Fileformat has been bumped from 10 to 11. This means that downgrading to an earlier version of Realm is not possible and Realm Studio 5.0.0 must be used to view Realm files. + +### Enhancements +* None. + +### Fixes +* In some cases a frozen Realm of the wrong version could be returned. ([ObjectStore issue #1078](https://github.com/realm/realm-object-store/pull/1078)) +* Upgrading files with string primary keys would result in a file where it was not possible to find the objects by primary key. ([Core issue #3893](https://github.com/realm/realm-core/pull/3893), since 7.0.0) +* NullPointerException when calling `toString` on RealmObjects with a binary field containing `null`. (Issue [#7084](https://github.com/realm/realm-java/issues/7084), since 7.0.0) + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* Realm Studio: 5.0.0 or later. +* File format: Generates Realms with format v11 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 7.x.y series. + +### Internal +* Upgraded to Object Store commit: 286d7cb2f10c41f89a2efb43b22938610ccad4cf. +* Upgraded to Realm Sync: 5.0.21. +* Upgraded to Realm Core: 6.0.24. + +## 7.0.3 (2020-09-01) + +### Enhancements +* Added `Realm.getNumberOfActiveVersions()`, which returns the current number of active versions maintained by the Realm file. + +### Fixes +* Creating a query inside a change listener could in some cases result in the version being pinned, which would either drastically increase filesize or cause `RealmConfiguration.maxNumberOfActiveVersions()` to trigger. (Issue [#6977](https://github.com/realm/realm-java/issues/6977), since 7.0.0) +* If you upgrade a Realm file where you have "" elements in a list of non-nullable strings, the upgrade would crash. +* If an attempt to upgrade a Realm file has ended with a crash with "migrate_links" in the call stack, the Realm ended in a corrupt state where further upgrade was not possible. A remedy for this situation is now provided. + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* Realm Studio: 4.0.0 or later. +* File format: Generates Realms with format v10 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 7.x.y series. + +### Internal +* Upgraded to Object Store commit: eef80f42e6ede2294eb60f048228012d9b7bc627. +* Upgraded to Realm Sync: 5.0.19. +* Upgraded to Realm Core: 6.0.22. +* The upgrade logic for upgrading fileformats has changed so that progress is now recorded explicitly in a table. This makes the logic simpler and reduces the chance of errors. It will also make it easier to detect if a file has only been partially upgraded. + + +## 7.0.2 (2020-08-14) + +### Enhancements +* None. + +### Fixes +* [ObjectServer] Calling `SyncManager.refreshConnections()` did not correctly refresh connections in all cases, which could delay reconnects up to 5 minutes. (Issue [#7003](https://github.com/realm/realm-java/issues/7003)) +* Upgrading the file format result did in some cases not work correctly. This could result in a number of crashes, e.g. `FORMAT_UPGRADE_REQUIRED`. (Issue [#6889](https://github.com/realm/realm-java/issues/6889), since 7.0.0) +* Bug in memory mapping management. This bug could result in multiple different asserts as well as segfaults. In many cases stack backtraces would include members of the EncyptedFileMapping near the top - even if encryption was not used at all. In other cases asserts or crashes would be in methods reading an array header or array element. In all cases the application would terminate immediately. (Issue [#3838](https://github.com/realm/realm-core/pull/3838), since 7.0.0) +* Crash when retrieving `null` valued primitive fields from dynamic realm. (Issue [#7025](https://github.com/realm/realm-java/issues/7025)) + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* Realm Studio: 4.0.0 or later. +* File format: Generates Realms with format v10 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 7.x.y series. + +### Internal +* Upgraded to Realm Sync 5.0.15. +* Upgraded to Realm Core 6.0.17. + + +## 7.0.1 (2020-07-01) + +### Enhancements +* None. + +### Fixes +* Upgrading older Realm files with String indexes was very slow. (Issue [#6875](https://github.com/realm/realm-java/issues/6875), since 7.0.0) +* Aborting upgrading a Realm file could result in the file getting corrupted. (Isse [#6866](https://github.com/realm/realm-java/issues/6866), since 7.0.0) +* Automatic indexes on primary keys are now correctly stripped when upgrading the file as they are no longer needed. (Since 7.0.0) +* `NoSuchTable` was thrown after comitting a transaction. (Issue [#6947](https://github.com/realm/realm-java/issues/6947)) + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* Realm Studio: 4.0.0 or later. +* File format: Generates Realms with format v10 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 7.x.y series. + +### Internal +* Upgraded to Realm Sync 5.0.7. +* Upgraded to Realm Core 6.0.8. + + +## 7.0.0 (2020-05-16) + +NOTE: This version bumps the Realm file format to version 10. Files created with previous versions of Realm will be automatically upgraded. It is not possible to downgrade to version 9 or earlier. Only [Studio 3.11](https://github.com/realm/realm-studio/releases/tag/v3.11.0) or later will be able to open the new file format. +NOTE: This version bumps the Realm file format to version 10. Files created with previous versions of Realm will be automatically upgraded. It is not possible to downgrade to version 9 or earlier. Only [Realm Studio 4](https://github.com/realm/realm-studio/releases/tag/v4.0.0) or later will be able to open the new file format. + +### Breaking Changes +* [ObjectServer] Removed deprecated method `SyncConfiguration.Builder.partialRealm()`. Use `SyncConfiguration.Builder.fullSynchronization()` instead. +* [ObjectServer] Removed deprecated methods `SyncConfiguration.automatic()` and `SyncConfiguration.automatic(User, Uri)`. Use `SyncUser.getDefaultConfiguration()` and `SyncUser.createConfiguration(Url)`. +* [ObjectServer] Removed deprecated method `ErrorCode.fromInt(int)`. +* [ObjectServer] Removed deprecated method `SyncCredentials.nickname(name)` and `SyncCredentials.nickname(name, isAdmin)`. Use `SyncCredentials.usernamePassword(username, password)` instead. +* [ObjectServer] Deprecated state `SyncSession.State.ERROR` has been removed. Use `SyncConfiguration.Builder.errorHandler(ErrorHandler)` instead. +* [ObjectServer] `IncompatibleSyncedFileException` is removed as it is no longer used. +* [ObjectServer] New error codes thrown by the underlying sync layers now have proper enum mappings in `ErrorCode.java`. A few other errors have been renamed in order to have consistent naming. (Issue [#6387](https://github.com/realm/realm-java/issues/6387)) +* RxJava Flowables and Observables are now subscribed to and unsubscribed to asynchronously on the thread holding the live Realm, instead of previously where this was done synchronously. +* All RxJava Flowables and Observables now return frozen objects instead of live objects. This can be configured using `RealmConfiguration.Builder.rxFactory(new RealmObservableFactory(true|false))`. By using frozen objects, it is possible to send RealmObjects across threads, which means that all RxJava operators should now be supported without the need to copy Realm data into unmanaged objects. +* MIPS is not supported anymore. +* Realm now requires `minSdkVersion` 16. Up from 9. +* [ObjectServer] `IncompatibleSyncedFileException` is removed and no longer thrown. + +### Enhancements +* Added `Realm.freeze()`, `RealmObject.freeze()`, `RealmResults.freeze()` and `RealmList.freeze()`. These methods will return a frozen version of the current Realm data. This data can be read from any thread without throwing an `IllegalStateException`, but will never change. All frozen Realms and data can be closed by calling `Realm.close()` on the frozen Realm, but fully closing all live Realms will also close the frozen ones. Frozen data can be queried as normal, but trying to mutate it in any way will throw an `IllegalStateException`. This includes all methods that attempt to refresh or add change listeners. (Issue [#6590](https://github.com/realm/realm-java/pull/6590)) +* Added `Realm.isFrozen()`, `RealmObject.isFrozen()`, `RealmObject.isFrozen(RealmModel)`, `RealmResults.isFrozen()` and `RealmList.isFrozen()`, which returns whether or not the data is frozen. +* Added `RealmConfiguration.Builder.maxNumberOfActiveVersions(long number)`. Setting this will cause Realm to throw an `IllegalStateException` if too many versions of the Realm data are live at the same time. Having too many versions can dramatically increase the filesize of the Realm. +* Storing large binary blobs in Realm files no longer forces the file to be at least 8x the size of the largest blob. +* Reduce the size of transaction logs stored inside the Realm file, reducing file size growth from large transactions. +* `RealmResults.asJSON()` is no longer `@Beta` +* The default `toString()` for proxy objects now print the length of binary fields. (Issue [#6767](https://github.com/realm/realm-java/pull/6767)) + +### Fixes +* If a DynamicRealm and Realm was opened for the same file they would share transaction state by accident. The implication was that writes to a `Realm` would immediately show up in the `DynamicRealm`. This has been fixed, so now it is required to call `refresh()` on the other Realm or wait for normal change listeners to detect the change. + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* Realm Studio: 4.0.0 or later. +* File format: Generates Realms with format v10 (Reads and upgrades all previous formats from Realm Java 2.0 and later). +* APIs are backwards compatible with all previous release of realm-java in the 7.x.y series. + +### Internal +* `OsSharedRealm.VersionID.hashCode()` was not implemented correctly and included the memory location in the hashcode. +* OKHttp was upgraded to 3.10.0 from 3.9.0. +* The NDK has been upgraded from r10e to r21. +* The compiler used for C++ code has changed from GCC to Clang. +* OpenSSL used by Realms encryption layer has been upgraded from 1.0.2k to 1.1.1b. +* Updated to Object Store commit: 820b74e2378f111991877d43068a95d2b7a2e404. +* Updated to Realm Sync 5.0.3. +* Updated to Realm Core 6.0.4. + +### Credits +* Thanks to @joxon for better support for binary fields in proxy objects. + + +## 6.1.0(2020-01-17) + +### Fixed +* None. + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 6.x.y series. + +### Internal +* None. + + + +## 6.1.0(2020-01-17) + +### Enhancements +* The Realm Gradle plugin now applies `kapt` when used in Kotlin Multiplatform projects. Note, Realm Java still only works for the Android part of a Kotlin Multiplatform project. (Issue [#6653](https://github.com/realm/realm-java/issues/6653)) +* The error message shown when no native code could be found for the device is now much more descriptive. This is particular helpful if an app is using App Bundle or APK Split and the resulting APK was side-loaded outside the Google Play Store. (Issue [#6673](https://github.com/realm/realm-java/issues/6673)) +* `RealmResults.asJson()` now encode binary data as Base64 and null object links are reported as `null` instead of `[]`. + +### Fixed +* Fixed using `RealmList` with a primitive type sometimes crashing with `Destruction of mutex in use`. (Issue [#6689](https://github.com/realm/realm-java/issues/6689)) +* `RealmObjectSchema.transform()` would crash if one of the `DynamicRealmObject` provided are deleted from the Realm. (Issue [#6657](https://github.com/realm/realm-java/issues/6657), since 0.86.0) +* The Realm Transformer will no longer attempt to send anonymous metrics when Gradle is invoked with `--offline`. (Issue [#6691](https://github.com/realm/realm-java/issues/6691)) + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 6.x.y series. + +### Internal +* Updated to ReLinker 1.4.0. +* Updated to Object Store commit: 2a204063e1e1a366efbdd909fbea9effceb7d3c4. +* Updated to Realm Sync 4.9.4. +* Updated to Realm Core 5.23.8. + +### Credits +* Thanks to @sellmair (Sebastian Sellmair) for improving Kotlin Multiplatform support. + + +## 6.0.2(2019-11-21) + +### Enhancements +* None. + +### Fixed +* [ObjectServer] `SyncSession` progress listeners now work correctly in combination with `SyncConfiguration.waitForInitialRemoteData()`. +* The `@RealmModule` annotation would be stripped on an empty class when using R8 resulting in apps crashing on startup with `io.realm.DefaultRealmModule is not a RealmModule. Add @RealmModule to the class definition.`. ([#6449](https://github.com/realm/realm-java/issues/6449)) + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 6.x.y series. + +### Internal +* Updated to Object Store commit: ad96a4c334b475dd67d50c1ca419e257d7a21e18. +* Updated to Realm Sync v4.8.3. + +## 6.0.1(2019-11-11) + +NOTE: Anyone using encrypted Realms are strongly advised to upgrade to this version. + +### Enhancements +* None + +### Fixed +* When using encrypted Realms a race condition could lead to the Realm ending up corrupted when the file increased in size. This could manifest as a wide array of different error messages. Most commonly seen has been "Fatal signal 11 (SIGSEGV) from Java_io_realm_internal_UncheckedRow_nativeGetString", "RealmFileException: Top ref outside file" and "Unable to open a realm at path. ACCESS_ERROR: Invalid mnemonic". ([#6152](https://github.com/realm/realm-java/issues/6152), since 5.0.0) +* `RealmResults.asJSON()` now prints lists with primitive values directly instead of wrapping each value in an object with an `!ARRAY_VALUE` property. + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 6.x.y series. + +### Internal +* Updated to Realm Sync 4.7.12. +* Updated to Realm Core 5.23.6. + +### Credits +* Thanks to Vladimir Konkov (@vladimirfx) for help with isolating ([#6152](https://github.com/realm/realm-java/issues/6152)). + + +## 6.0.0(2019-10-01) + +### Breaking Changes +* [ObjectServer] The `PermissionManager` is no longer backed by Realms but instead a REST API. This means that the `PermissionManager` class has been removed and all methods have been moved to `SyncUser`. Some method names have been renamed slightly and return values for methods have changed from `RealmResults` to `List`. This should only have an impact if change listeners were used to listen for changes. In these cases, you must now manually retry the request. + +### Enhancements +None. + +### Fixed +None. + +### Compatibility +* Realm Object Server: 3.23.1 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 6.x.y series. + +### Internal +* [ObjectServer] The OKHttp client will now follow redirects from the Realm Object Server. + + +## 5.15.2(2019-09-30) + +### Enhancements +* None. + +### Fixed +* `null` values were not printed correctly when using `RealmResults.asJSON()` (Realm Core Issue [#3399](https://github.com/realm/realm-core/pull/3399)) +* [ObjectServer] Queries with nullable `Date`'s did not serialize correctly. Only relevant if using Query-based Synchronization. (Realm Core issue [#3388](https://github.com/realm/realm-core/pull/3388)) +* [ObjectServer] Fixed crash with `java.lang.IllegalStateException: The following changes cannot be made in additive-only schema mode` when opening an old Realm created between Realm Java 5.10.0 and Realm Java 5.13.0. (Issue [#6619](https://github.com/realm/realm-java/issues/6619), since 5.13.0). + +### Compatibility +* Realm Object Server: 3.21.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Updated to Object Store commit: 8416010e4be5e32ba552ff3fb29e500f3102d3db. +* Updated to Realm Sync 4.7.8. +* Updated to Realm Core 5.23.5. +* Updated Docker image used on CI to Node 10. + + +## 5.15.1(2019-09-09) + +### Enhancements +* None. + +### Fixed +* Projects with `flatDirs` repositories defined crashed the build with `MissingPropertyException`. (Issue [#6610](https://github.com/realm/realm-java/issues/6610), since 5.15.0). + +### Compatibility +* Realm Object Server: 3.21.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* None. + +## 5.15.0(2019-09-05) + +### Enhancements +* [ObjectServer] Added support for Client Resync for fully synchronized Realms which automatically will recover the local Realm in case the server is rolled back. This largely replaces the Client Reset mechanism. Can be configured using `SyncConfiguration.Builder.clientResyncMode()`. (Issue [#6487](https://github.com/realm/realm-java/issues/6487)) + +### Fixed +* Huawei devices reporting `Permission denied` when opening a Realm file after an app upgrade or factory reset. This does not automatically fix already existing Realm files. See [this FAQ entry](https://realm.io/docs/java/latest/#huawei-permission-denied) for more details. (Issue [#5715](https://github.com/realm/realm-java/issues/5715)) +* `Realm.copyToRealm()` and `Realm.insertOrUpdate()` crashed on model classes if `@LinkingObjects` was used to target a field with a re-defined internal name in the parent class (e.g. by using `@RealmField`). (Issue [#6581](https://github.com/realm/realm-java/issues/6581)) + +### Compatibility +* Realm Object Server: 3.21.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Implemented direct access to sync workers on Cloud, bypassing the Sync Proxy: the binding will override the sync session's url prefix if the token refresh response for a realm contains a sync worker path field. +* Updated to Object Store commit: 9f19d79fde248ba37cef0bd52fe64984f9d71be0. +* Updated to Realm Sync 4.7.4. +* Updated to Realm Core 5.23.2. + + +## 5.14.0(2019-08-12) + +### Deprecated +* [ObjectServer] `SyncCredentials.nickname()` has been deprecated in favour of `SyncCredentials.usernamePassword()`. +* [ObjectServer] `SyncCredentials.IdentityProvider.NICKNAME` has been deprecated in favour of `SyncCredentials.IdentityProvider.USERNAME_PASSWORD`. + +### Enhancements +* None. + +### Fixed +* None. + +### Compatibility +* Realm Object Server: 3.21.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* None. + + +## 5.13.1(2019-08-05) + +### Enhancements +* None. + +### Fixed +* [ObjectServer] The C++ networking layer now correctly uses any system defined proxy the same way the Java networking layer does. (Issue [#6574](https://github.com/realm/realm-java/pull/6574)). +* The Realm bytecode transformer now works correctly with Android Gradle Plugin 3.6.0-alpha01 and beyond. (Issue [#6531](https://github.com/realm/realm-java/issues/6531)). +* Queries on RealmLists with objects containing indexed integers could return the wrong result. (Issue [#6522](https://github.com/realm/realm-java/issues/6522), since 5.11.0) + +### Compatibility +* Realm Object Server: 3.21.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Updated JavaAssist in the Realm Transformer to 3.25.0-GA. +* Updated to Realm Core 5.23.1. +* Updated to Realm Sync 4.7.1. +* Updated to Object Store commit: bcc6a7524e52071bfcd35cf740f506e0cc6a595e + + +## 5.13.0(2019-07-23) + +### Enhancements +* [ObjectServer] Added support for faster initial synchronization for fully synchronized Realms. (Issue [#6469](https://github.com/realm/realm-java/issues/6469)) +* [ObjectServer] Improved session lifecycle debug output. (Issue [#6552](https://github.com/realm/realm-java/pull/6552)). + +### Fixed +* None. + +### Compatibility +* Realm Object Server: 3.21.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Updated to Realm Core 5.22.0. +* Updated to Realm Sync 4.6.1. +* Updated to Object Store commit f0d75261fc8d332c20dc82f643dd795c0f4c7aec + + +## 5.12.0(2019-06-20) + +### Enhancements +* [ObjectServer] Added `SyncManager.refreshConnections()` that can be used to manually trigger a reconnect for all sessions. This is useful if the device has been offline for a long time or fail to detect that it regained connectivity. (Issue [#259](https://github.com/realm/realm-java-private/issues/259)) +* Added `RealmResults.asJson()` in `@Beta` that returns the result of the query as a JSON payload (#6540). + +### Fixed +* [ObjectServer] `PermissionManager` stopped working if an intermittent network error was reported. (Issue [#6492](https://github.com/realm/realm-java/issues/6492), since 3.7.0) +* The Kotlin extensions library no longer defines a `app_name`, which in some cases conflicted with the `app_name` defined by applications. (Issue [#6536](https://github.com/realm/realm-java/issues/6536), since 4.3.0) + +### Compatibility +* Realm Object Server: 3.21.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Updated to Realm Core 5.22.0. +* Updated to Realm Sync 4.6.1. +* Updated to Object Store commit 7c3ff8235579550a3e3c6060c47140b2005174f5 + +## 5.11.0(2019-05-01) + +NOTE: This version is only compatible with Realm Object Server 3.21.0 or later. + +### Enhancements +* [ObjectServer] Added `RealmQuery.includeLinkingObjects()`. This is only relevant for Query-based Realms and tells subscriptions to include objects linked through `@LinkingObjects` fields as part of the subscription as well. Objects referenced through objects and lists are always included as a default. (Issue [#6426](https://github.com/realm/realm-java/issues/6426)) +* Encryption now uses hardware optimized functions, which significantly improves the performance of encrypted Realms. ([Realm Core PR #3241](https://github.com/realm/realm-core/pull/3241)) +* Improved query performance when using `RealmQuery.in()` queries. ([Realm Core PR #3250](https://github.com/realm/realm-core/pull/3250)). +* Improved query performance when querying Integer fields with indexes, e.g. primary key fields. ([Realm Core PR #3272](https://github.com/realm/realm-core/pull/3272)). +* Improved write performance when writing changes to disk ([Realm Core PR #2927](https://github.com/realm/realm-sync/issues/2927)) +* Added support for incremental annotation processing added in Gradle 4.7. (Issue [#5906](https://github.com/realm/realm-java/issues/5906)). + +### Fixed +* [ObjectServer] Fix an error in the calculation of the `downloadableBytes` value sent by `ProgressListeners`. +* [ObjectServer] HTTP requests made by the Sync client now always include a Host: header, as required by HTTP/1.1, although its value will be empty if no value is specified by the application. +* [ObjectServer] The server no longer rejects subscriptions based on queries with distinct and/or limit clauses. +* [ObjectServer] If a user had `canCreate` but not `canUpdate` privileges on a class, the user would be able to create the object, but not actually set any meaningful values on that object, despite the rule that objects created within the same transaction can always be modified. +* Native crash happening if bulk updating a field in a `RealmResult` would cause the object to no longer be part of the query result. (Issue [#6478](https://github.com/realm/realm-java/issues/6478), since 5.8.0). + +### Compatibility +* Realm Object Server: 3.21.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Updated to Realm Core 5.19.1. +* Updated to Relm Sync 4.4.2. +* Updated to Object Store commit e4b1314d21b521fd604af7f1aacf3ca94272c19a + + +## 5.10.0(2019-03-22) + +### Enhancements +* [ObjectServer] Added 4 new fields to query-based Subscriptions: `createdAt`, `updatedAt`, `expiresAt` and `timeToLive`. These make it possible to better reason about and control current subscriptions. (Issue [#6453](https://github.com/realm/realm-java/issues/6453)) +* [ObjectServer] Added the option of updating the query controlled by a Subscription using either `RealmQuery.findAllAsync(String name, boolean update)`, `RealmQuery.subscribe(String name, boolean update)` or `Subscription.setQuery(RealmQuery query)`. (Issue [#6453](https://github.com/realm/realm-java/issues/6453)) +* [ObjectServer] Added the option of setting a time-to-live for subscriptions. Setting this will automatically delete the subscription after the provided TTL has expired and the subscription hasn't been used. (Issue [#6453](https://github.com/realm/realm-java/issues/6453)) + +### Fixed +* Dates returned from the Realm file no longer overflow or underflow if they exceed `Long.MAX_VALUE` or `Long.MIN_VALUE` but instead clamp to their respective value. (Issue [#2722](https://github.com/realm/realm-java/issues/2722)) + +### Compatibility +* Realm Object Server: 3.11.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats). +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Updated to Object Store commit: e9819ed9c77ed87b5d7bed416a76cd5bcf255802 + + +## 5.9.1(2019-02-21) + +### Enhancements +* None + +### Fixed +* [ObjectServer] Reporting too many errors from the native layer resulted in a native crash with `local reference table overflow`. (Issue [#249](https://github.com/realm/realm-java-private/issues/249), since 5.9.0) + +### Compatibility +* Realm Object Server: 3.11.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* None + +## 5.9.0(2019-01-15) + +### Enhancements +* [ObjectServer] Added `ObjectServerError.getErrorType()` and `ObjectServerError.getErrorType()` which returns the underlying native error information. This is especially relevant if `ObjectServerError.getErrorCode()` returns `UNKNOWN`. [#6364](https://github.com/realm/realm-java/issues/6364) +* Added better checks for detecting corrupted files, both before and after the file is written to disk. + +### Fixed +* [ObjectServer] Native errors sometimes mapped to the wrong Java ErrorCode. (Issue [#6364](https://github.com/realm/realm-java/issues/6364), since 2.0.0) +* [ObjectServer] Query-based Sync queries involving LIMIT, limited the result before permissions were evaluated. This could sometimes result in the wrong number of elements being returned. +* Removed Java 8 bytecode. Resulted in errors like `D8: Invoke-customs are only supported starting with Android O (--min-api 26)` if not compiled with Java 8. (Issue [#6300](https://github.com/realm/realm-java/issues/6300), since 5.8.0). + +### Compatibility +* Realm Object Server: 3.11.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Updated to Object Store commit: f964c2640f635e76839559cb703732e9e906ba4c +* Updated Realm Sync to 3.14.13 +* Updated Realm Core to 5.12.7 + + +## 5.8.0 (2018-11-06) + +This release also contains all changes from 5.8.0-BETA1 and 5.8.0-BETA2. + +### Enhancements +* [ObjectServer] Added Subscription class available to Query-based Realms. This exposes a Subscription more directly. This class is in beta. [#6231](https://github.com/realm/realm-java/pull/6231). + * [ObjectServer] Added `Realm.getSubscriptions()`, `Realm.getSubscriptions(String pattern)` and `Realm.getSubscription` to make it easier to find existing subscriptions. These API's are in beta. [#6231](https://github.com/realm/realm-java/pull/6231) + * [ObjectServer] Added `RealmQuery.subscribe()` and `RealmQuery.subscribe(String name)` to subscribe immediately inside a transaction. These API's are in beta. [#6231](https://github.com/realm/realm-java/pull/6231) + * [ObjectServer] Added support for subscribing directly inside `SyncConfiguration.initialData()`. This can be coupled with `SyncConfiguration.waitForInitialRemoteData()` in order to block a Realm from opening until the initial subscriptions are ready and have downloaded data. This API are in beta. [#6231](https://github.com/realm/realm-java/pull/6231) +* [ObjectServer] Improved performance when merging changes from the server. +* [ObjectServer] Added support for timeouts when uploading or downloading data manually using `SyncSession.downloadAllServerChanges(long timeout, TimeUnit unit)` and `SyncSession.uploadAllLocalChanges(long timeout, TimeUnit unit)`. [#6073](https://github.com/realm/realm-java/pull/6073) +* [ObjectServer] Added support for timing out when downloading initial data for synchronized Realms using `SyncConfiguration.waitForInitialRemoteData(long timeout, TimeUnit unit)`. [#6247](https://github.com/realm/realm-java/issues/6247) +* [ObjectServer] Added `Realm.init(Context, String)` which defines a custom User-Agent String sent to the Realm Object Server when a session is created. Using this requires Realm Object Server 3.12.4 or later. [#6267](https://github.com/realm/realm-java/issues/6267) +* Added support for `ImportFlag`s to `Realm.copyToRealm()` and `Realm.copyToRealmOrUpdate()`. This makes it possible to choose a mode so only fields that actually changed are written to disk. This improves notifications and Object Server performance. [#6224](https://github.com/realm/realm-java/pull/6224) +* Added support for bulk updating the same property in all objects that are part of a query result using `RealmResults.setValue(String fieldName, Object value)` or one of the specialized overrides that have been added for all supported types, e.g. `RealmResults.setString(String fieldName, String value)`. [#762](https://github.com/realm/realm-java/issues/762) + +### Fixed +* All known bugs introduced in 5.8.0-BETA1 and 5.8.0-BETA2. See the release notes for these releases. + +### Compatibility +* Realm Object Server: 3.11.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Updated to Object Store commit: f0dfe6c03be49194bc40777901059eaf55e7bff6 +* Updated Realm Sync to 3.13.1 +* Updated Realm Core to 5.12.0 + + +## 5.8.0-BETA2 (2018-10-19) + +### Enhancements +* None + +### Fixed +* `RealmResults` listeners not triggering the initial callback for Query-based Realm when the device is offline [#6235](https://github.com/realm/realm-java/issues/6235). + +### Known Bugs +* `Realm.copyToRealm()` and `Realm.copyToRealmOrUpdate` has been rewritten to support import flags. It is currently ~30% slower than in 5.7.0. +* IllegalStateException thrown when trying to create an object with a primary key that already exists when using `Realm.copyToRealm`, will always report "null" instead of the correct primary key value. +* When using `ImportFlag.DO_NOT_SET_SAME_VALUES`, lists will still be written and reported as changed, even if they didn't change. + +### Compatibility +* Realm Object Server: 3.11.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* None + + +## 5.8.0-BETA1 (2018-10-11) + +### Enhancements +* Added new `ImportFlag` class that is used to specify additional behaviour when importing + data into Realm [#6224](https://github.com/realm/realm-java/pull/6224). +* Added support for `ImportFlag` to `Realm.copyToRealm()` and `Realm.copyToRealmOrUpdate()` [#6224](https://github.com/realm/realm-java/pull/6224). + +### Fixed +* None + +### Known Bugs +* `Realm.copyToRealm()` and `Realm.copyToRealmOrUpdate` has been rewritten to support import flags. It is currently ~30% slower than in 5.7.0. +* IllegalStateException thrown when trying to create an object with a primary key that already exists when using `Realm.copyToRealm`, will always report "null" instead of the correct primary key value. +* When using `ImportFlag.DO_NOT_SET_SAME_VALUES`, lists will still be written and reported as changed, even if they didn't change. + +### Compatibility +* Realm Object Server: 3.11.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + + +## 5.7.1 (2018-10-22) + +### Enhancements +* None + +### Fixed +* [ObjectServer] `RealmResults` listeners not triggering the initial callback for Query-based Realm when the device is offline. (Issue [#6235](https://github.com/realm/realm-java/issues/6235), since 5.0.0). + +### Compatibility +* Realm Object Server: 3.11.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Updated to Object Store commit: 362b886628b3aefc5b7a0bc32293d794dc1d4ad5 + + +## 5.7.0 (2018-09-24) + +### Enhancements +* [ObjectServer] Devices will now report download progress for read-only Realms which + will allow the server to compact files sooner, saving server space. This does not affect + the client. You will need to upgrade your Realm Object Server to at least version 3.11.0 + or use [Realm Cloud](https://cloud.realm.io). If you try to connect to a ROS v3.10.x or + previous, you will see an error like `Wrong protocol version in Sync HTTP request, + client protocol version = 25, server protocol version = 24`. + +### Fixed +* None + +### Compatibility +* Realm Object Server: 3.11.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Sync Protocol version increased to 25. +* Updated Realm Sync to 3.10.1 +* Updated Realm Core to 5.10.2 + + +## 5.6.0 (2018-09-24) + +### Enhancements +* [ObjectServer] Added `RealmPermissions.findOrCreate(String roleName)` and + `ClassPermissions.findOrCreate(String roleName)` ([#6168](https://github.com/realm/realm-java/issues/6168)). +* `@RealmClass("name")` and `@RealmField("name")` can now be used as a shorthand for defining custom + name mappings ([#6145](https://github.com/realm/realm-java/issues/6145)). +* Added support for `RealmQuery.limit(long limit)` ([#544](https://github.com/realm/realm-java/issues/544)). + When building a `RealmQuery`, `sort()`, `distinct()` and `limit()` will now be applied in the order + they are called. Before this release, `sort()` and `distinct()` could be called any order, but + `sort()` would always be applied before `distinct()`. +* Building with Android App Bundle is now supported ([#5977](https://github.com/realm/realm-java/issues/5977)). + +### Fixed +* None + +### Compatibility +* Realm Object Server: 3.11.0 or later. +* File format: Generates Realms with format v9 (Reads and upgrades all previous formats) +* APIs are backwards compatible with all previous release of realm-java in the 5.x.y series. + +### Internal +* Updated ReLinker to 1.3.0. +* Updated to Object Store commit: 7e19c51af72c3343b453b8a13c82dfda148e4bbc + + +## 5.5.0 (2018-08-31) + +### Enhancements +* [ObjectServer] Added `ConnectionState` enum describing the states a connection can be in. +* [ObjectServer] Added `SyncSession.isConnected()` and `SyncSession.getConnectionState()`. +* [ObjectServer] Added support for observing connection changes for a session using `SyncSession.addConnectionChangeListener()` and `SyncSession.removeConnectionChangeListener()`. +* [ObjectServer] Added Kotlin extension property `Realm.syncSession` for synchronized Realms. +* [ObjectServer] Added Kotlin extension method `Realm.classPermissions()`. +* [ObjectServer] Added support for starting and stopping synchronization using `SyncSession.start()` and `SyncSession.stop()` (#6135). +* [ObjectServer] Added API's for making it easier to work with network proxies (#6163): + * `SyncManager.setAuthorizationHeaderName(String headerName)` + * `SyncManager.setAuthorizationHeaderName(String headerName, String host)` + * `SyncManager.addCustomRequestHeader(String headerName, String headerValue)` + * `SyncManager.addCustomRequestHeader(String headerName, String headerValue, String host)` + * `SyncManager.addCustomRequestHeaders(Map headers)` + * `SyncManager.addCustomRequestHeaders(Map headers, String host)` + * `SyncConfiguration.Builder.urlPrefix(String prefix)` + +### Fixed +* Methods and classes requiring synchronized Realms have been removed from the standard AAR package. They are now only visible when enabling synchronized Realms in Gradle. The methods and classes will still be visible in the source files and docs, but annotated with `@ObjectServer` (#5799). + +### Internal +* Updated to Realm Sync 3.9.4 +* Updated to Realm Core 5.8.0 +* Updated to Object Store commit: b0fc2814d9e6061ce5ba1da887aab6cfba4755ca + +### Credits +* Thanks to @lucasdornelasv for improving the performance of `Realm.copyToRealm()`, `Realm.copyToRealmOrUpdate()` and `Realm.copyFromRealm()` #(6124). + + +## 5.4.3 (YYYY-MM-DD) + +### Bug Fixes + +* [ObjectServer] ProGuard was not configured correctly when working with Subscriptions for Query-based Realms. + + +## 5.4.2 (2018-08-09) + +### Bug Fixes + +* [ObjectServer] Fixed bugs in the Sync Client that could lead to memory corruption and crashes. + +### Internal + +* Upgraded to Realm Sync 3.8.8 + + +## 5.4.1 (2018-08-03) + +### Bug Fixes + +* Compile time crash if no `targetSdk` was defined in Gradle. This was introduced in 5.4.0 (#6082). +* Fix Realm Gradle Plugin adding dependencies in a way incompatible with Kotlin Android Extensions. This was introduced in Realm Java 5.4.0 (#6080). + + +## 5.4.0 (2018-07-22) + +### Enhancements + +* Removing a ChangeListener on invalid objects or `RealmResults` should warn instead of throwing (fixes #5855). + +### Bug Fixes + +* [ObjectServer] Using Android Network Security Configuration is necessary to install the custom root CA for tests (API >= 24) (#5970). +* Fixes issue with the incremental build causing direct access to model without accessor to fail (#6056). +* `RealmQuery.distinct()` is now correctly applied when calling `RealmQuery.count()` (#5958). + +### Internal + +* Upgraded to Realm Core 5.7.2 +* Upgraded to Realm Sync 3.8.1 +* [ObjectServer] Improved performance when integrating changes from the server. +* Added extra information about the state of the Realm file if an exception is thrown due to Realm not being able to open it. +* Removed internal dependency on Groovy in the Realm Transformer (#3971). + +### Credits + +* Thanks to @kageiit for removing Groovy from the Realm Transformer (#3971). + + +## 5.3.1 (2018-06-19) + +### Bug Fixes + +* [ObjectServer] Fixed a bug which could potentially flood Realm Object Server with PING messages. +* Calling `Realm.deleteAll()` on a Realm file that contains more classes than in the schema throws exception (#5745). +* `Realm.isEmpty()` returning false in some cases, even if all tables part of the schema are empty (#5745). +* Fixed rare native crash materializing as `Assertion failed: ref + size <= after_ref with (ref, size, after_ref, ndx, m_free_positions.size())` (#5300). + +### Internal + +* Upgraded to Realm Core 5.6.2 +* Upgraded to Realm Sync 3.5.6 +* Upgraded to Object Store commit `0bcb9643b8fb14323df697999b79c4a5341a8a21` + + +## 5.3.0 (2018-06-12) + +### Enhancements + +* [ObjectServer] `Realm.compactRealm(config)` now works on synchronized Realms (#5937). +* [ObjectServer] `SyncConfiguration.compactOnLaunch()` and `SyncConfiguration.compactOnLaunch(callback)` has been added (#5937). +* Added `RealmQuery.getRealm()`, `RealmResults.getRealm()`, `RealmList.getRealm()` and `OrderedRealmCollectionSnapshot.getRealm()` (#5997). +* Removing a ChangeListener on invalid objects or `RealmResults` should warn instead of throwing (fixes #5855). + + +### Internal + +* Upgraded to Realm Core 5.6.0 +* Upgraded to Realm Sync 3.5.2 + + +## 5.2.0 (2018-06-06) + +The feature previously named Partial Sync is now called Query-Based Sync and is now the default mode when synchronizing Realms. +This has impacted a number of API's. See below for the details. + +### Deprecated + +* [ObjectServer] `SyncConfiguration.automatic()` has been deprecated in favour of `SyncUser.getDefaultConfiguration()`. +* [ObjectServer] `new SyncConfiguration.Builder(user, url)` has been deprecated in favour of `SyncUser.createConfiguration(url)`. NOTE: Creating configurations using `SyncUser` will default to using query-based Realms, while creating them using `new SyncConfiguration.Builder(user, url)` will default to fully synchronized Realms. +* [ObjectServer] With query-based sync being the default `SyncConfiguration.Builder.partialRealm()` has been deprecated. Use `SyncConfiguration.Builder.fullSynchronization()` if you want full synchronisation instead. + +### Enhancements + +* [ObjectServer] Added `SyncUser.createConfiguration(url)`. Realms created this way are query-based Realms by default. +* [ObjectServer] Added `SyncUser.getDefaultConfiguration()`. +* The Realm bytecode transformer now supports incremental builds (#3034). +* Improved speed and allocations when parsing field descriptions in queries (#5547). + +### Bug Fixes + +* Having files that ends with `RealmProxy` will no longer break the Realm Transformer (#3709). + +### Internal + +* Module mediator classes being generated now produces a stable output enabling better support for incremental builds (#3034). + + +## 5.1.0 (2018-04-25) + +### Enhancements + +* [ObjectServer] Added support for `SyncUser.requestPasswordReset()`, `SyncUser.completePasswordReset()` + and their async variants. This makes it possible to reset the password for users created using + `Credentials.usernamePassword()` where they used their email as username (#5821). +* [ObjectServer] Added support for `SyncUser.requestEmailConfirmation()`, `SyncUser.confirmEmail()` + and their async variants. This makes it possible to ask users to confirm their email. This is only + supported for users created using `Credentials.usernamePassword()` who have used an email as their + username (#5821). +* `RealmQuery.in()` now support `null` which will always return no matches (#4011). +* Added support for `RealmQuery.alwaysTrue()` and `RealmQuery.alwaysFalse()`. + +### Bug Fixes + +* Changing a primary key from being nullable to being required could result in objects being deleted (##5899). + + +## 5.0.1 (2018-04-09) + +### Enhancements + +* [ObjectServer] `SyncConfiguration.automatic()` will make use of the host port to work out the default Realm URL. +* [ObjectServer] A role is now automatically created for each user with that user as its only member. This simplifies the common use case of restricting access to specific objects to a single user. This role can be accessed at `PermissionUser.getRole()`. +* [ObjectServer] Expose `Role.getMembers()` to access the list of associated `UserPermission`. + +### Bug Fixes + +* `RealmList.move()` did not move items correctly for unmanaged lists (#5860). +* `RealmObject.isValid()` not correctly returns `false` if `null` is provided as an argument (#5865). +* `RealmQuery.findFirst()` and `RealmQuery.findFirstAsync()` not working correctly with sorting (#5714). +* Permission `noPrivileges` and `allPrivileges` were returning opposite privileges. +* Fixes an issue caused by JNI local table reference overflow (#5880). + +### Internal + +* Upgraded to Realm Sync 3.0.1 +* Upgraded to Realm Core 5.4.2 + +## 5.0.0 (2018-03-15) + +This release is compatible with the Realm Object Server 3.0.0-beta.3 or later. + +### Known Bugs + +* API's marked @ObjectServer are shipped as part of the base binary, they should only be available when enabling synchronized Realms. + +### Breaking Changes + +* [ObjectServer] Renamed `SyncUser.currentUser()` to `SyncUser.current()`. +* [ObjectServer] Renamed `SyncUser.login(...)` and `SyncUser.loginAsync(...)` to `SyncUser.logIn(...)` and `SyncUser.logInAsync(...)`. +* [ObjectServer] Renamed `SyncUser.logout()` to `SyncUser.logOut()`. +* The `OrderedCollectionChangeSet` parameter in `OrderedRealmCollectionChangeListener.onChange()` is no longer nullable. Use `changeSet.getState()` instead (#5619). +* `realm.subscribeForObjects()` have been removed. Use `RealmQuery.findAllAsync(String subscriptionName)` and `RealmQuery.findAllAsync()` instead. +* Removed previously deprecated `RealmQuery.findAllSorted()`, `RealmQuery.findAllSortedAsync()` `RealmQuery.distinct()` and `RealmQuery.distinctAsync()`. +* Renamed `RealmQuery.distinctValues()` to `RealmQuery.distinct()` + +### Enhancements + +* [ObjectServer] Added support for partial Realms. Read [here](https://realm.io/docs/java/latest/#partial-realms) for more information. +* [ObjectServer] Added support for Object Level Permissions (requires partial synchronized Realms). Read [here](https://realm.io/docs/java/latest/#partial-realms) for more information. +* [ObjectServer] Added `SyncConfiguration.automatic()` and `SyncConfiguration.automatic(SyncUser user)` (#5806). +* Added two new methods to `OrderedCollectionChangeSet`: `getState()` and `getError()` (#5619). + +## Bug Fixes + +* Better exception message if a non model class is provided to methods only accepting those (#5779). + +### Internal + +* Upgraded to Realm Sync 3.0.0 +* Upgraded to Realm Core 5.3.0 + + +## 4.4.0 (2018-03-13) + +### Enhancements + +* Added support for mapping between a Java name and the underlying name in the Realm file using `@RealmModule`, `@RealmClass` and `@RealmField` annotations (#5280). + +## Bug Fixes + +* [ObjectServer] Fixed an issue where login after a logout will not resume Syncing (https://github.com/realm/my-first-realm-app/issues/22). + + +## 4.3.4 (2018-02-06) + +## Bug Fixes + +* Added missing `RealmQuery.oneOf()` for Kotlin that accepts non-nullable types (#5717). +* [ObjectServer] Fixed an issue preventing sync to resume when the network is back (#5677). + +## 4.3.3 (2018-01-19) + +### Internal + +* Downgrade JavaAssist to 3.21.0-GA to fix an issue with a `ClassNotFoundException` at runtime (#5641). + + +## 4.3.2 (2018-01-17) + +### Bug Fixes + +* Throws a better exception message when calling `RealmObjectSchema.addField()` with a `RealmModel` class (#3388). +* Use https for Realm version checker (#4043). +* Prevent Realms Gradle plugin from transitively forcing specific versions of Google Build Tools onto downstream projects (#5640). +* [ObjectServer] logging a warning message instead of throwing an exception, when sync report an unknown error code (#5403). + +### Enhancements + +* [ObjectServer] added support for both Anonymous and Nickname authentication. + + +### Internal + +* Upgraded to Realm Sync 2.2.9 +* Upgraded to Realm Core 5.1.2 + +## 4.3.1 (2017-12-06) + +### Bug Fixes + +* Fixed kotlin standard library being added to both Java and Kotlin projects (#5587). + + +## 4.3.0 (2017-12-05) + +### Deprecated + +* Support for mips devices are deprecated. +* `RealmQuery.findAllSorted()` and `RealmQuery.findAllSortedAsync()` variants in favor of predicate `RealmQuery.sort().findAll()`. +* `RealmQuery.distinct()` and `RealmQuery.distinctAsync()` variants in favor of predicate `RealmQuery.distinctValues().findAll()` + +### Enhancements + +* [ObjectServer] Added explicit support for JSON Web Tokens (JWT) using `SyncCredentials.jwt(String token)`. It requires Object Server 2.0.23+ (#5580). +* Projects using Kotlin now include additional extension functions that make working with Kotlin easier. See [docs](https://realm.io/docs/java/latest/#kotlin) for more info (#4684). +* New query predicate: `sort()`. +* New query predicate: `distinctValues()`. Will be renamed to `distinct` in next major version. +* The Realm annotation processor now has a stable output when there are no changes to model classes, improving support for incremental compilers (#5567). + +### Bug Fixes + +* Added missing `toString()` for the implementation of `OrderedCollectionChangeSet`. +* Sync queries are evaluated immediately to solve the performance issue when the query results are huge, `RealmResults.size()` takes too long time (#5387). +* Correctly close the Realm instance if an exception was thrown while opening it. This avoids `IllegalStateException` when deleting the Realm in the catch block (#5570). +* Fixed the listener on `RealmList` not being called when removing the listener then adding it again (#5507). Please notice that a similar issue still exists for `RealmResults`. + +### Internal + +* Use `OsList` instead of `OsResults` to add notification token on for `RealmList`. +* Updated Gradle and plugins to support Android Studio `3.0.0` (#5472). +* Upgraded to Realm Sync 2.1.8. +* Upgraded to Realm Core 4.0.4. + +### Credits + +* Thanks to @tbsandee for fixing a typo (#5548). +* Thanks to @vivekkiran for updating Gradle and plugins to support Android Studio `3.0.0` (#5472). +* Thanks to @madisp for adding better support for incremental compilers (#5567). + + +## 4.2.0 (2017-11-17) + +### Enhancements + +* Added support for using non-encrypted Realms in multiple processes. Some caveats apply. Read [doc](https://realm.io/docs/java/latest/#multiprocess) for more info (#1091). +* Added support for importing primitive lists from JSON (#5362). +* [ObjectServer] Support SSL validation using Android TrustManager (no need to specify `trustedRootCA` in `SynConfiguration` if the certificate is installed on the device), fixes (#4759). +* Added the and() function to `RealmQuery` in order to improve readability. + +### Bug Fixes + +* Leaked file handler in the Realm Transformer (#5521). +* Potential fix for "RealmError: Incompatible lock file" crash (#2459). + +### Internal + +* Updated JavaAssist to 3.22.0-GA. +* Upgraded to Realm Sync 2.1.4. +* Upgraded to Realm Core 4.0.3. + +### Credits + +* Thanks to @rakshithravi1997 for adding `RealmQuery.and()` (#5520). + + +## 4.1.1 (2017-10-27) + +### Bug Fixes + +* Fixed the compile warnings of using deprecated method `RealmProxyMediator.getTableName()` in generated mediator classes (#5455). +* [ObjectServer] now retrying network query when encountering any `IOException` (#5453). +* Fixed a `NoClassDefFoundError` due to using `@SafeVarargs` below API 19 (#5463). + +### Internal + +* Updated Realm Sync to 2.1.0. + + +## 4.1.0 (2017-10-20) + +### Enhancements + +* `Realm.deleteRealm()` and `RealmConfiguration.assetFile()` are multi-processes safe now. + +### Bug Fixes + +* Fix some potential database corruption caused by deleting the Realm file while a Realm instance are still opened in another process or the sync client thread. +* Added `realm.ignoreKotlinNullability` as a kapt argument to disable treating kotlin non-null types as `@Required` (#5412) (introduced in `v3.6.0`). +* Increased http connect/write timeout for low bandwidth network. + + +## 4.0.0 (2017-10-16) + +### Breaking Changes + +The internal file format has been upgraded. Opening an older Realm will upgrade the file automatically, but older versions of Realm will no longer be able to read the file. + +* [ObjectServer] Updated protocol version to 22 which is only compatible with Realm Object Server >= 2.0.0. +* [ObjectServer] Removed deprecated APIs `SyncUser.retrieveUser()` and `SyncUser.retrieveUserAsync()`. Use `SyncUser.retrieveInfoForUser()` and `retrieveInfoForUserAsync()` instead. +* [ObjectServer] `SyncUser.Callback` now accepts a generic parameter indicating type of object returned when `onSuccess` is called. +* [ObjectServer] Renamed `SyncUser.getAccessToken` to `SyncUser.getRefreshToken`. +* [ObjectServer] Removed deprecated API `SyncUser.getManagementRealm()`. +* Calling `distinct()` on a sorted `RealmResults` no longer clears any sorting defined (#3503). +* Relaxed upper bound of type parameter of `RealmList`, `RealmQuery`, `RealmResults`, `RealmCollection`, `OrderedRealmCollection` and `OrderedRealmCollectionSnapshot`. +* Realm has upgraded its RxJava1 support to RxJava2 (#3497) + * `Realm.asObservable()` has been renamed to `Realm.asFlowable()`. + * `RealmList.asObservable()` has been renamed to `RealmList.asFlowable()`. + * `RealmResults.asObservable()` has been renamed to `RealmResults.asFlowable()`. + * `RealmObject.asObservable()` has been renamed to `RealmObject.asFlowable()`. + * `RxObservableFactory` now return RxJava2 types instead of RxJava1 types. +* Removed deprecated APIs `RealmSchema.close()` and `RealmObjectSchema.close()`. Those don't have to be called anymore. +* Removed deprecated API `RealmResults.removeChangeListeners()`. Use `RealmResults.removeAllChangeListeners()` instead. +* Removed deprecated API `RealmObject.removeChangeListeners()`. Use `RealmObject.removeAllChangeListeners()` instead. +* Removed `UNSUPPORTED_TABLE`, `UNSUPPORTED_MIXED` and `UNSUPPORTED_DATE` from `RealmFieldType`. +* Removed deprecated API `RealmResults.distinct()`/`RealmResults.distinctAsync()`. Use `RealmQuery.distinct()`/`RealmQuery.distinctAsync()` instead. +* `RealmQuery.createQuery(Realm, Class)`, `RealmQuery.createDynamicQuery(DynamicRealm, String)`, `RealmQuery.createQueryFromResult(RealmResults)` and `RealmQuery.createQueryFromList(RealmList)` have been removed. Use `Realm.where(Class)`, `DynamicRealm.where(String)`, `RealmResults.where()` and `RealmList.where()` instead. + +### Enhancements + +* [ObjectServer] `SyncUserInfo` now also exposes a users metadata using `SyncUserInfo.getMetadata()` +* `RealmList` can now contain `String`, `byte[]`, `Boolean`, `Long`, `Integer`, `Short`, `Byte`, `Double`, `Float` and `Date` values. [Queries](https://github.com/realm/realm-java/issues/5361) and [Importing primitive lists from JSON](https://github.com/realm/realm-java/issues/5362) are not supported yet. +* Added support for lists of primitives in `RealmObjectSchema` with `addRealmListField(String fieldName, Class primitiveType)` +* Added support for lists of primitives in `DynamicRealmObject` with `setList(String fieldName, RealmList list)` and `getList(String fieldName, Class primitiveType)`. +* Minor performance improvement when copy/insert objects into Realm. +* Added `static RealmObject.getRealm(RealmModel)`, `RealmObject.getRealm()` and `DynamicRealmObject.getDynamicRealm()` (#4720). +* Added `RealmResults.asChangesetObservable()` that emits the pair `(results, changeset)` (#4277). +* Added `RealmList.asChangesetObservable()` that emits the pair `(list, changeset)` (#4277). +* Added `RealmObject.asChangesetObservable()` that emits the pair `(object, changeset)` (#4277). +* All Realm annotations are now kept at runtime, allowing runtime tools access to them (#5344). +* Speedup schema initialization when a Realm file is first accessed (#5391). + +### Bug Fixes + +* [ObjectServer] Exposing a `RealmConfiguration` that allows a user to open the backup Realm after the client reset (#4759/#5223). +* [ObjectServer] Realm no longer throws a native “unsupported instruction” exception in some cases when opening a synced Realm asynchronously (https://github.com/realm/realm-object-store/issues/502). +* [ObjectServer] Fixed "Cannot open the read only Realm" issue when get`PermissionManager` (#5414). +* Throw `IllegalArgumentException` instead of `IllegalStateException` when calling string/binary data setters if the data length exceeds the limit. +* Added support for ISO8601 2-digit time zone designators (#5309). +* "Bad File Header" caused by the device running out of space while compacting the Realm (#5011). +* `RealmQuery.equalTo()` failed to find null values on an indexed field if using Case.INSENSITIVE (#5299). +* Assigning a managed object's own list to itself would accidentally clear it (#5395). +* Don't try to acquire `ApplicationContext` if not available in `Realm.init(Context)` (#5389). +* Removing and re-adding a changelistener from inside a changelistener sometimes caused notifications to be missed (#5411). + +### Internal + +* Upgraded to Realm Sync 2.0.2. +* Upgraded to Realm Core 4.0.2. +* Upgraded to OkHttp 3.9.0. +* Upgraded to RxJava 2.1.4. +* Use Object Store to create the primary key table. + +### Credits + +* Thanks to @JussiPekonen for adding support for 2-digit time zone designators when importing JSON (#5309). + + +## 3.7.2 (2017-09-12) + +### Bug Fixes + +* Fixed a JNI memory issue when doing queries which might potentially cause various native crashes. +* Fixed a bug that `RealmList.deleteFromRealm(int)`, `RealmList.deleteFirstFromRealm()` and `RealmList.deleteLastFromRealm()` did not remove target objects from Realm. This bug was introduced in `3.7.1` (#5233). +* Crash with "'xxx' doesn't exist in current schema." when ProGuard is enabled (#5211). + + +## 3.7.1 (2017-09-07) + +### Bug Fixes + +* Fixed potential memory leaks of `LinkView` when calling bulk insertions APIs. +* Fixed possible assertion when using `PermissionManager` at the beginning (#5195). +* Crash caused by JNI couldn't find `SharedRealm`'s inner classes when ProGuard is enabled (#5211). + +### Internal + +* Replaced LinkView with Object Store's List. +* Renaming `io.realm.internal.CollectionChangeSet` to `io.realm.internal.OsCollectionChangeSet`. + + +## 3.7.0 (2017-09-01) + +### Deprecated + +* [ObjectServer] `SyncUser.getManagementRealm()`. Use `SyncUser.getPermissionManager()` instead. + +### Enhancements + +* [ObjectServer] `SyncUser.getPermissionManager` added as a helper API for working with permissions and permission offers. + +### Internal + +* [ObjectServer] Upgraded OkHttp to 3.7.0. + + +## 3.6.0 (2017-09-01) + +### Breaking Changes + +* [ObjectServer] `SyncUser.logout()` no longer throws an exception when associated Realms instances are not closed (#4962). + +### Deprecated + +* [ObjectServer] `SyncUser#retrieveUser` and `SyncUser#retrieveUserAsync` replaced by `SyncUser#retrieveInfoForUser` +and `SyncUser#retrieveInfoForUserAsync` which returns a `SyncUserInfo` with mode information (#5008). +* [ObjectServer] `SyncUser#Callback` replaced by the generic version `SyncUser#RequestCallback`. + +### Enhancements + +* [ObjectServer] Added `SyncSession.uploadAllLocalChanges()`. +* [ObjectServer] APIs of `UserStore` have been changed to support same user identity but different authentication server scenario. +* [ObjectServer] Added `SyncUser.allSessions` to retrieve the all valid sessions belonging to the user (#4783). +* Added `Nullable` annotation to methods that may return `null` in order to improve Kotlin usability. This also introduced a dependency to `com.google.code.findbugs:jsr305`. +* `org.jetbrains.annotations.NotNull` is now an alias for `@Required`. This means that the Realm Schema now fully understand Kotlin non-null types. +* Added support for new data type `MutableRealmIntegers`. The new type behaves almost exactly as a reference to a Long (mutable nullable, etc) but supports `increment` and `decrement` methods, which implement a Conflict Free Replicated Data Type, whose value will converge even when changed across distributed devices with poor connections (#4266). +* Added more detailed exception message for `RealmMigrationNeeded`. +* Bumping schema version only without any actual schema changes will just succeed even when the migration block is not supplied. It threw an `RealmMigrationNeededException` before in the same case. +* Throw `IllegalStateException` when schema validation fails because of wrong declaration of `@LinkingObjects`. + +### Bug Fixes + +* Potential crash after using `Realm.getSchema()` to change the schema of a typed Realm. `Realm.getSchema()` now returns an immutable `RealmSchema` instance. +* `Realm.copyToRealmOrUpdate()` could cause a `RealmList` field to contain duplicated elements (#4957). +* `RealmSchema.create(String)` and `RealmObjectSchema.setClassName(String)` did not accept class name whose length was 51 to 57. +* Workaround for an Android JVM crash when using `compactOnLaunch()` (#4964). +* Class name in exception message from link query is wrong (#5096). +* The `compactOnLaunch` callback is no longer invoked if the Realm at that path is already open on other threads. + +### Internal + +* [ObjectServer] removed `ObjectServerUser` and its inner classes, in a step to reduce `SyncUser` complexity (#3741). +* [ObjectServer] changed the `SyncSessionStopPolicy` to `AfterChangesUploaded` to align with other binding and to prevent use cases where the Realm might be deleted before the last changes get synchronized (#5028). +* Upgraded Realm Sync to 1.10.8 +* Let Object Store handle migration. + + +## 3.5.0 (2017-07-11) + +### Enhancements + +* Added `RealmConfiguration.Builder.compactOnLaunch()` to compact the file on launch (#3739). +* [ObjectServer] Adding user lookup API for administrators (#4828). +* An `IllegalStateException` will be thrown if the given `RealmModule` doesn't include all required model classes (#3398). + +### Bug Fixes + +* Bug in `isNull()`, `isNotNull()`, `isEmpty()`, and `isNotEmpty()` when queries involve nullable fields in link queries (#4856). +* Bug in how to resolve field names when querying `@LinkingObjects` as the last field (#4864). +* Rare crash in `RealmLog` when log level was set to `LogLevel.DEBUG`. +* Broken case insensitive query with indexed field (#4788). +* [ObjectServer] Bug related to the behaviour of `SyncUser#logout` and the use of invalid `SyncUser` with `SyncConfiguration` (#4822). +* [ObjectServer] Not all error codes from the server were recognized correctly, resulting in UNKNOWN being reported instead. +* [ObjectServer] Prevent the use of a `SyncUser` that explicitly logged out, to open a Realm (#4975). + +### Internal + +* Use Object Store to do table initialization. +* Removed `Table#Table()`, `Table#addEmptyRow()`, `Table#addEmptyRows()`, `Table#add(Object...)`, `Table#pivot(long,long,PivotType)` and `Table#createnative()`. +* Upgraded Realm Core to 2.8.6 +* Upgraded Realm Sync to 1.10.5 +* Removed `io.realm.internal.OutOfMemoryError`. `java.lang.OutOfMemoryError` will be thrown instead. + + +## 3.4.0 (2017-06-22) + +### Breaking Changes + +* [ObjectServer] Updated protocol version to 18 which is only compatible with ROS > 1.6.0. + +### Deprecated + +* `RealmSchema.close()` and `RealmObjectSchema.close()`. They don't need to be closed manually. They were added to the public API by mistake. + +### Enhancements + +* [ObjectServer] Added support for Sync Progress Notifications through `SyncSession.addDownloadProgressListener(ProgressMode, ProgressListener)` and `SyncSession.addUploadProgressListener(ProgressMode, ProgressListener)` (#4104). +* [ObjectServer] Added `SyncSession.getState()` (#4784). +* Added support for querying inverse relationships (#2904). +* Moved inverse relationships out of beta stage. +* Added `Realm.getDefaultConfiguration()` (#4725). + +### Bug Fixes + +* [ObjectServer] Bug which may crash when the JNI local reference limitation was reached on sync client thread. +* [ObjectServer] Retrying connections with exponential backoff, when encountering `ConnectException` (#4310). +* When converting nullable BLOB field to required, `null` values should be converted to `byte[0]` instead of `byte[1]`. +* Bug which may cause duplicated primary key values when migrating a nullable primary key field to not nullable. `RealmObjectSchema.setRequired()` and `RealmObjectSchema.setNullable()` will throw when converting a nullable primary key field with null values stored to a required primary key field. + +### Internal + +* Upgraded to Realm Sync 1.10.1 +* Upgraded to Realm Core 2.8.4 + +### Credits + +* Thanks to Anis Ben Nsir (@abennsir) for upgrading Roboelectric in the unitTestExample (#4698). + + +## 3.3.2 (2017-06-09) + +### Bug Fixes + +* [ObjectServer] Crash when an authentication error happens (#4726). +* [ObjectServer] Enabled encryption with Sync (#4561). +* [ObjectServer] Admin users did not connect correctly to the server (#4750). + +### Internal + +* Factor out internal interface ManagedObject. + +## 3.3.1 (2017-05-26) + +### Bug Fixes + +* [ObjectServer] Accepted extra columns against synced Realm (#4706). + + +## 3.3.0 (2017-05-24) + +### Enhancements + +* [ObjectServer] Added two options to `SyncConfiguration` to provide a trusted root CA `trustedRootCA` and to disable SSL validation `disableSSLVerification` (#4371). +* [ObjectServer] Added support for changing passwords through `SyncUser.changePassword()` using an admin user (#4588). + +### Bug Fixes + +* Queries on proguarded Realm model classes, failed with "Table not found" (#4673). + + +## 3.2.1 (2017-05-19) + +### Enhancements + +* Not in transaction illegal state exception message changed to "Cannot modify managed objects outside of a write transaction.". + +### Bug Fixes + +* [ObjectServer] `schemaVersion` was mistakenly required in order to trigger migrations (#4658). +* [ObjectServer] Fields removed from model classes will now correctly be hidden instead of throwing an exception when opening the Realm (#4658). +* Random crashes which were caused by a race condition in encrypted Realm (#4343). + +### Internal + +* Upgraded to Realm Sync 1.8.5. +* Upgraded to Realm Core 2.8.0. + +## 3.2.0 (2017-05-16) + +### Enhancements + +* [ObjectServer] Added support for `SyncUser.isAdmin()` (#4353). +* [ObjectServer] New set of Permission API's have been added to `SyncUser` through `SyncUser.getPermissionManager()` (#4296). +* [ObjectServer] Added support for changing passwords through `SyncUser.changePassword()` (#4423). +* [ObjectServer] Added support for `SyncConfiguration.Builder.waitForInitialRemoteData()` (#4270). +* Transient fields are now allowed in model classes, but are implicitly treated as having the `@Ignore` annotation (#4279). +* Added `Realm.refresh()` and `DynamicRealm.refresh()` (#3476). +* Added `Realm.getInstanceAsync()` and `DynamicRealm.getInstanceAsync()` (#2299). +* Added `DynamicRealmObject#linkingObjects(String,String)` to support linking objects on `DynamicRealm` (#4492). +* Added support for read only Realms using `RealmConfiguration.Builder.readOnly()` and `SyncConfiguration.Builder.readOnly()`(#1147). +* Change listeners will now auto-expand variable names to be more descriptive when using Android Studio. +* The `toString()` methods for the standard and dynamic proxies now print "proxy", or "dynamic" before the left bracket enclosing the data. + +### Bug Fixes + +* `@LinkingObjects` annotation now also works with Kotlin (#4611). + +### Internal + +* Use separated locks for different `RealmCache`s (#4551). + +## 3.1.4 (2017-05-04) + +## Bug fixes + +* Added missing row validation check in certain cases on invalidated/deleted objects (#4540). +* Initializing Realm is now more resilient if `Context.getFilesDir()` isn't working correctly (#4493). +* `OrderedRealmCollectionSnapshot.get()` returned a wrong object (#4554). +* `onSuccess` callback got triggered infinitely if a synced transaction was committed in the async transaction's `onSuccess` callback (#4594). + +## 3.1.3 (2017-04-20) + +### Enhancements + +* [ObjectServer] Resume synchronization as soon as the connectivity is back (#4141). + +### Bug Fixes + +* `equals()` and `hashCode()` of managed `RealmObject`s that come from linking objects don't work correctly (#4487). +* Field name was missing in exception message when `null` was set to required field (#4484). +* Now throws `IllegalStateException` when a getter of linking objects is called against deleted or not yet loaded `RealmObject`s (#4499). +* `NullPointerException` caused by local transaction inside the listener of `findFirstAsync()`'s results (#4495). +* Native crash when adding listeners to `RealmObject` after removing listeners from the same `RealmObject` before (#4502). +* Native crash with "Invalid argument" error happened on some Android 7.1.1 devices when opening Realm on external storage (#4461). +* `OrderedRealmCollectionChangeListener` didn't report change ranges correctly when circular link's field changed (#4474). + +### Internal + +* Upgraded to Realm Sync 1.6.0. +* Upgraded to Realm Core 2.6.1. + +## 3.1.2 (2017-04-12) + +### Bug Fixes + +* Crash caused by JNI couldn't find `OsObject.notifyChangeListeners` when ProGuard is enabled (#4461). +* Incompatible return type of `RealmSchema.getAll()` and `BaseRealm.getSchema()` (#4443). +* Memory leaked when synced Realm was initialized (#4465). +* An `IllegalStateException` will be thrown when starting iterating `OrderedRealmCollection` if the Realm is closed (#4471). + +## 3.1.1 (2017-04-07) + +### Bug Fixes + +* Crash caused by Listeners on `RealmObject` getting triggered the 2nd time with different changed field (#4437). +* Unintentionally exposing `StandardRealmSchema` (#4443). +* Workaround for crashes on specific Samsung devices which are caused by a buggy `memmove` call (#3651). + +## 3.1.0 (2017-04-05) + +### Breaking Changes + +* Updated file format of Realm files. Existing Realm files will automatically be migrated to the new format when they are opened, but older versions of Realm cannot open these files. +* [ObjectServer] Due to file format changes, Realm Object Server 1.3.0 or later is required. + +### Enhancements + +* Added support for reverse relationships through the `@LinkingObjects` annotation. See `io.realm.annotations.LinkingObjects` for documentation. + * This feature is in `@Beta`. + * Queries on linking objects do not work. Queries like `where(...).equalTo("field.linkingObjects.id", 7).findAll()` are not yet supported. + * Backlink verification is incomplete. Evil code can cause native crashes. +* The listener on `RealmObject` will only be triggered if the object changes (#3894). +* Added `RealmObjectChangeListener` interface that provide detailed information about `RealmObject` field changes. +* Listeners on `RealmList` and `RealmResults` will be triggered immediately when the transaction is committed on the same thread (#4245). +* The real `RealmMigrationNeededException` is now thrown instead of `IllegalArgumentException` if no migration is provided for a Realm that requires it. +* `RealmQuery.distinct()` can be performed on unindexed fields (#2285). +* `targetSdkVersion` is now 25. +* [ObjectServer] In case of a Client Reset, information about the location of the backed up Realm file is now reported through the `ErrorHandler` interface (#4080). +* [ObjectServer] Authentication URLs now automatically append `/auth` if no other path segment is set (#4370). + +### Bug Fixes + +* Crash with `LogicError` with `Bad version number` on notifier thread (#4369). +* `Realm.migrateRealm(RealmConfiguration)` now fails correctly with an `IllegalArgumentException` if a `SyncConfiguration` is provided (#4075). +* Potential cause for Realm file corruptions (never reported). +* Add `@Override` annotation to proxy class accessors and stop using raw type in proxy classes in order to remove warnings from javac (#4329). +* `findFirstAsync()` now returns an invalid object if there is no object matches the query condition instead of running the query repeatedly until it can find one (#4352). +* [ObjectServer] Changing the log level after starting a session now works correctly (#4337). + +### Internal + +* Using the Object Store's Session and SyncManager. +* Upgraded to Realm Sync 1.5.0. +* Upgraded to Realm Core 2.5.1. +* Upgraded Gradle to 3.4.1 + +## 3.0.0 (2017-02-28) + +### Breaking Changes + +* `RealmResults.distinct()` returns a new `RealmResults` object instead of filtering on the original object (#2947). +* `RealmResults` is auto-updated continuously. Any transaction on the current thread which may have an impact on the order or elements of the `RealmResults` will change the `RealmResults` immediately instead of change it in the next event loop. The standard `RealmResults.iterator()` will continue to work as normal, which means that you can still delete or modify elements without impacting the iterator. The same is not true for simple for-loops. In some cases a simple for-loop will not work (https://realm.io/docs/java/3.0.0/api/io/realm/OrderedRealmCollection.html#loops), and you must use the new createSnapshot() method. +* `RealmChangeListener` on `RealmObject` will now also be triggered when the object is deleted. Use `RealmObject.isValid()` to check this state(#3138). +* `RealmObject.asObservable()` will now emit the object when it is deleted. Use `RealmObject.isValid()` to check this state (#3138). +* Removed deprecated classes `Logger` and `AndroidLogger` (#4050). + +### Deprecated + +* `RealmResults.removeChangeListeners()`. Use `RealmResults.removeAllChangeListeners()` instead. +* `RealmObject.removeChangeListeners()`. Use `RealmObject.removeAllChangeListeners()` instead. +* `RealmResults.distinct()` and `RealmResults.distinctAsync()`. Use `RealmQuery.distinct()` and `RealmQuery.distinctAsync()` instead. + +### Enhancements + +* Added support for sorting by link's field (#672). +* Added `OrderedRealmCollectionSnapshot` class and `OrderedRealmCollection.createSnapshot()` method. `OrderedRealmCollectionSnapshot` is useful when changing `RealmResults` or `RealmList` in simple loops. +* Added `OrderedRealmCollectionChangeListener` interface for supporting fine-grained collection notifications. +* Added support for ChangeListeners on `RealmList`. +* Added `RealmList.asObservable()`. + +### Bug Fixes + +* Element type checking in `DynamicRealmObject#setList()` (#4252). +* Now throws `IllegalStateException` instead of process crash when any of thread confined methods in `RealmQuery` is called from wrong thread (#4228). +* Now throws `IllegalStateException` when any of thread confined methods in `DynamicRealmObject` is called from wrong thread (#4258). + +### Internal + +* Use Object Store's `Results` as the backend for `RealmResults` (#3372). + - Use Object Store's notification mechanism to trigger listeners. + - Local commits triggers Realm global listener and `RealmObject` listener on current thread immediately instead of in the next event loop. + + +## 2.3.2 (2017-02-27) + +### Bug fixes + +* Log levels in JNI layer were all reported as "Error" (#4204). +* Encrypted realms can end up corrupted if many threads are reading and writing at the same time (#4128). +* "Read-only file system" exception when compacting Realm file on external storage (#4140). + +### Internal + +* Updated to Realm Sync v1.2.1. +* Updated to Realm Core v2.3.2. + +### Enhancements + +* Improved performance of getters and setters in proxy classes. + + +## 2.3.1 (2017-02-07) + +### Enhancements + +* [ObjectServer] The `serverUrl` given to `SyncConfiguration.Builder()` is now more lenient and will also accept only paths as argument (#4144). +* [ObjectServer] Add a timer to refresh periodically the access_token. + +### Bug fixes + +* NPE problem in SharedRealm.finalize() (#3730). +* `RealmList.contains()` and `RealmResults.contains()` now correctly use custom `equals()` method on Realm model classes. +* Build error when the project is using Kotlin (#4087). +* Bug causing classes to be replaced by classes already in Gradle's classpath (#3568). +* NullPointerException when notifying a single object that it changed (#4086). + + +## 2.3.0 (2017-01-19) + +### Object Server API Changes + +* Realm Sync v1.0.0 has been released, and Realm Mobile Platform is no longer considered in beta. +* Breaking change: Location of Realm files are now placed in `getFilesDir()/` instead of `getFilesDir()/`. + This is done in order to support shared Realms among users, while each user retaining their own local copy. +* Breaking change: `SyncUser.all()` now returns Map instead of List. +* Breaking change: Added a default `UserStore` saving users in a Realm file (`RealmFileUserStore`). +* Breaking change: Added multi-user support to `UserStore`. Added `get(String)` and `remove(String)`, removed `remove()` and renamed `get()` to `getCurrent()`. +* Breaking change: Changed the order of arguments to `SyncCredentials.custom()` to match iOS: token, provider, userInfo. +* Added support for `PermissionOffer` and `PermissionOfferResponse` to `SyncUser.getManagementRealm()`. +* Exceptions thrown in error handlers are ignored but logged (#3559). +* Removed unused public constants in `SyncConfiguration` (#4047). +* Fixed bug, preventing Sync client to renew the access token (#4038) (#4039). +* Now `SyncUser.logout()` properly revokes tokens (#3639). + +### Bug fixes + +* Fixed native memory leak setting the value of a primary key (#3993). +* Activated Realm's annotation processor on connectedTest when the project is using kapt (#4008). +* Fixed "too many open files" issue (#4002). +* Added temporary work-around for bug crashing Samsung Tab 3 devices on startup (#3651). + +### Enhancements + +* Added `like` predicate for String fields (#3752). + +### Internal + +* Updated to Realm Sync v1.0.0. +* Added a Realm backup when receiving a Sync client reset message from the server. + +## 2.2.2 (2017-01-16) + +### Object Server API Changes (In Beta) + +* Disabled `Realm.compactRealm()` when sync is enabled as it might corrupt the Realm (https://github.com/realm/realm-core/issues/2345). + +### Bug fixes + +* "operation not permitted" issue when creating Realm file on some devices' external storage (#3629). +* Crash on API 10 devices (#3726). +* `UnsatisfiedLinkError` caused by `pipe2` (#3945). +* Unrecoverable error with message "Try again" when the notification fifo is full (#3964). +* Realm migration wasn't triggered when the primary key definition was altered (#3966). +* Use phantom reference to solve the finalize time out issue (#2496). + +### Enhancements + +* All major public classes are now non-final. This is mostly a compromise to support Mockito. All protected fields/methods are still not considered part of the public API and can change without notice (#3869). +* All Realm instances share a single notification daemon thread. +* Fixed Java lint warnings with generated proxy classes (#2929). + +### Internal + +* Upgraded Realm Core to 2.3.0. +* Upgraded Realm Sync to 1.0.0-BETA-6.5. + +## 2.2.1 (2016-11-12) + +### Object Server API Changes (In Beta) + +* Fixed `SyncConfiguration.toString()` so it now outputs a correct description instead of an empty string (#3787). + +### Bug fixes + +* Added version number to the native library, preventing ReLinker from accidentally loading old code (#3775). +* `Realm.getLocalInstanceCount(config)` throwing NullPointerException if called after all Realms have been closed (#3791). + +## 2.2.0 (2016-11-12) + +### Object Server API Changes (In Beta) + +* Added support for `SyncUser.getManagementRealm()` and permission changes. + +### Bug fixes + +* Kotlin projects no longer create the `RealmDefaultModule` if no Realm model classes are present (#3746). +* Remove `includedescriptorclasses` option from ProGuard rule file in order to support built-in shrinker of Android Gradle Plugin (#3714). +* Unexpected `RealmMigrationNeededException` was thrown when a field was added to synced Realm. + +### Enhancements + +* Added support for the `annotationProcessor` configuration provided by Android Gradle Plugin 2.2.0 or later. Realm plugin adds its annotation processor to the `annotationProcessor` configuration instead of `apt` configuration if it is available and the `com.neenbedankt.android-apt` plugin is not used. In Kotlin projects, `kapt` is used instead of the `annotationProcessor` configuration (#3026). + +## 2.1.1 (2016-10-27) + +### Bug fixes + +* Fixed a bug in `Realm.insert` and `Realm.insertOrUpdate` methods causing a `StackOverFlow` when you try to insert a cyclic graph of objects between Realms (#3732). + +### Object Server API Changes (In Beta) + +* Set default RxFactory to `SyncConfiguration`. + +### Bug fixes + +* ProGuard configuration introduced in 2.1.0 unexpectedly kept classes that did not have the @KeepMember annotation (#3689). + +## 2.1.0 (2016-10-25) + +### Breaking changes + +* * `SecureUserStore` has been moved to its own GitHub repository: https://github.com/realm/realm-android-user-store + See https://github.com/realm/realm-android-user-store/blob/master/README.md for further info on how to include it. + + +### Object Server API Changes (In Beta) + +* Renamed `User` to `SyncUser`, `Credentials` to `SyncCredentials` and `Session` to `SyncSession` to align names with Cocoa. +* Removed `SyncManager.setLogLevel()`. Use `RealmLog.setLevel()` instead. +* `SyncUser.logout()` now correctly clears `SyncUser.currentUser()` (#3638). +* Missing ProGuard configuration for libraries used by Sync extension (#3596). +* Error handler was not called when sync session failed (#3597). +* Added `User.all()` that returns all known Realm Object Server users. +* Upgraded Realm Sync to 1.0.0-BETA-3.2 + +### Deprecated + +* `Logger`. Use `RealmLogger` instead. +* `AndroidLogger`. The logger for Android is implemented in native code instead. + +### Bug fixes + +* The following were not kept by ProGuard: names of native methods not in the `io.realm.internal` package, names of classes used in method signature (#3596). +* Permission error when a database file was located on external storage (#3140). +* Memory leak when unsubscribing from a RealmResults/RealmObject RxJava Observable (#3552). + +### Enhancements + +* `Realm.compactRealm()` now works for encrypted Realms. +* Added `first(E defaultValue)` and `last(E defaultValue)` methods to `RealmList` and `RealmResult`. These methods will return the provided object instead of throwing an `IndexOutOfBoundsException` if the list is empty. +* Reduce transformer logger verbosity (#3608). +* `RealmLog.setLevel(int)` for setting the log level across all loggers. + +### Internal + +* Upgraded Realm Core to 2.1.3 + +### Credits + +* Thanks to Max Furman (@maxfurman) for adding support for `first()` and `last()` default values. + +## 2.0.2 (2016-10-06) + +This release is not protocol-compatible with previous versions of the Realm Mobile Platform. The base library is still fully compatible. + +### Bug fixes + +* Build error when using Java 7 (#3563). + +### Internal + +* Upgraded Realm Core to 2.1.0 +* Upgraded Realm Sync to 1.0.0-BETA-2.0. + +## 2.0.1 (2016-10-05) + +### Bug fixes + +* `android.net.conn.CONNECTIVITY_CHANGE` broadcast caused `RuntimeException` if sync extension was disabled (#3505). +* `android.net.conn.CONNECTIVITY_CHANGE` was not delivered on Android 7 devices. +* `distinctAsync` did not respect other query parameters (#3537). +* `ConcurrentModificationException` from Gradle when building an application (#3501). + +### Internal + +* Upgraded to Realm Core 2.0.1 / Realm Sync 1.3-BETA + +## 2.0.0 (2016-09-27) + +This release introduces support for the Realm Mobile Platform! +See for an overview of these great new features. + +### Breaking Changes + +* Files written by Realm 2.0 cannot be read by 1.x or earlier versions. Old files can still be opened. * It is now required to call `Realm.init(Context)` before calling any other Realm API. * Removed `RealmConfiguration.Builder(Context)`, `RealmConfiguration.Builder(Context, File)` and `RealmConfiguration.Builder(File)` constructors. * `isValid()` now always returns `true` instead of `false` for unmanaged `RealmObject` and `RealmList`. This puts it in line with the behaviour of the Cocoa and .NET API's (#3101). @@ -23,20 +3037,24 @@ * Added `RealmConfiguration.Builder.directory(File)`. * `RealmLog` has been moved to the public API. It is now possible to control which events Realm emit to Logcat. See the `RealmLog` class for more details. * Typed `RealmObject`s can now continue to access their fields properly even though the schema was changed while the Realm was open (#3409). +* A `RealmMigrationNeededException` will be thrown with a cause to show the detailed message when a migration is needed and the migration block is not in the `RealmConfiguration`. + ### Bug fixes * Fixed a lint error in proxy classes when the 'minSdkVersion' of user's project is smaller than 11 (#3356). * Fixed a potential crash when there were lots of async queries waiting in the queue. * Fixed a bug causing the Realm Transformer to not transform field access in the model's constructors (#3361). +* Fixed a bug causing a build failure when the Realm Transformer adds accessors to a model class that was already transformed in other project (#3469). * Fixed a bug causing the `NullPointerException` when calling getters/setters in the model's constructors (#2536). ### Internal * Moved JNI build to CMake. -* Updated Realm Core to 2.0.0-rc7. +* Updated Realm Core to 2.0.0. +* Updated ReLinker to 1.2.2. -## 1.2.0 +## 1.2.0 (2016-08-19) ### Bug fixes @@ -61,7 +3079,7 @@ * Thanks to Brenden Kromhout (@bkromhout) for adding binary array support to `equalTo` and `notEqualTo`. -## 1.1.1 +## 1.1.1 (2016-07-01) ### Bug fixes @@ -85,8 +3103,9 @@ ### Internal * Updated Realm Core to 1.4.2. +* Improved sorting speed. -## 1.1.0 +## 1.1.0 (2016-06-30) ### Bug fixes @@ -112,7 +3131,7 @@ * Updated Realm Core to 1.2.0. -## 1.0.1 +## 1.0.1 (2016-05-25) ### Bug fixes @@ -130,11 +3149,11 @@ * Removes RxJava related APIs during bytecode transforming to make RealmObject plays well with reflection when rx.Observable doesn't exist. -## 1.0.0 +## 1.0.0 (2016-05-25) No changes since 0.91.1. -## 0.91.1 +## 0.91.1 (2016-05-25) * Updated Realm Core to 1.0.1. @@ -142,7 +3161,7 @@ No changes since 0.91.1. * Fixed a bug when opening a Realm causes a staled memory mapping. Symptoms are error messages like "Bad or incompatible history type", "File format version doesn't match", and "Encrypted interprocess sharing is currently unsupported". -## 0.91.0 +## 0.91.0 (2016-05-20) * Updated Realm Core to 1.0.0. @@ -197,7 +3216,7 @@ No changes since 0.91.1. * Removed `HandlerController` from the public API. * Removed constructor of `RealmAsyncTask` from the public API (#1594). * `RealmBaseAdapter` has been moved to its own GitHub repository: https://github.com/realm/realm-android-adapters - See https://github.com/realm/realm-android-adapters/README.md for further info on how to include it. + See https://github.com/realm/realm-android-adapters/blob/master/README.md for further info on how to include it. * File format of Realm files is changed. Files will be automatically upgraded but opening a Realm file with older versions of Realm is not possible. @@ -207,9 +3226,9 @@ No changes since 0.91.1. * `Realm.distinct*()`. Use `Realm.where(clazz).distinct*()` instead. * `DynamicRealm.allObjects*()`. Use `DynamicRealm.where(className).findAll*()` instead. * `DynamicRealm.distinct*()`. Use `DynamicRealm.where(className).distinct*()` instead. -* `Realm.allObjectsSorted(field, sort, field, sort, field, sort)`. Use `RealmQuery.findAllSorted(field[], sort[])`` instead. -* `RealmQuery.findAllSorted(field, sort, field, sort, field, sort)`. Use `RealmQuery.findAllSorted(field[], sort[])`` instead. -* `RealmQuery.findAllSortedAsync(field, sort, field, sort, field, sort)`. Use `RealmQuery.findAllSortedAsync(field[], sort[])`` instead. +* `Realm.allObjectsSorted(field, sort, field, sort, field, sort)`. Use `RealmQuery.findAllSorted(field[], sort[])` instead. +* `RealmQuery.findAllSorted(field, sort, field, sort, field, sort)`. Use `RealmQuery.findAllSorted(field[], sort[])` instead. +* `RealmQuery.findAllSortedAsync(field, sort, field, sort, field, sort)`. Use `RealmQuery.findAllSortedAsync(field[], sort[])` instead. * `RealmConfiguration.setModules()`. Use `RealmConfiguration.modules()` instead. * `Realm.refresh()` and `DynamicRealm.refresh()`. Use `Realm.waitForChange()`/`stopWaitForChange()` or `DynamicRealm.waitForChange()`/`stopWaitForChange()` instead. @@ -242,7 +3261,7 @@ No changes since 0.91.1. ### Breaking changes -* @PrimaryKey field value can now be null for String, Byte, Short, Integer, and Long types. Older Realms should be migrated, using RealmObjectSchema.setNullable(), or by adding the @Required annotation. (#2515). +* @PrimaryKey field value can now be null for String, Byte, Short, Integer, and Long types. Older Realms should be migrated, using RealmObjectSchema.setNullable(), or by adding the @Required annotation (#2515). * `RealmResults.clear()` now throws UnsupportedOperationException. Use `RealmResults.deleteAllFromRealm()` instead. * `RealmResults.remove(int)` now throws UnsupportedOperationException. Use `RealmResults.deleteFromRealm(int)` instead. * `RealmResults.sort()` and `RealmList.sort()` now return the sorted result instead of sorting in-place. @@ -284,7 +3303,7 @@ No changes since 0.91.1. ### Bug fixes * Field annotated with @Ignored should not have accessors generated by the bytecode transformer (#2478). -* RealmResults and RealmObjects can no longer accidentially be GC'ed if using `asObservable()`. Previously this caused the observable to stop emitting. (#2485). +* RealmResults and RealmObjects can no longer accidentially be GC'ed if using `asObservable()`. Previously this caused the observable to stop emitting (#2485). * Fixed an build issue when using Realm in library projects on Windows (#2484). * Custom equals(), toString() and hashCode() are no longer incorrectly overwritten by the proxy class (#2545). @@ -310,7 +3329,7 @@ No changes since 0.91.1. * now DynamicRealmObject.toString() correctly shows null value as "null" and the format is aligned to the String from typed RealmObject (#2439). * Fixed an issue occurring while resolving ReLinker in apps using a library based on Realm (#2415). -## 0.88.0 +## 0.88.0 (2016-03-10) * Updated Realm Core to 0.97.0. @@ -363,16 +3382,16 @@ No changes since 0.91.1. * Thanks to Bill Best (@wmbest2) for snapshot testing. * Thanks to Graham Smith (@grahamsmith) for a detailed bug report (#2200). -## 0.87.5 +## 0.87.5 (2016-01-29) * Updated Realm Core to 0.96.2. - IllegalStateException won't be thrown anymore in RealmResults.where() if the RealmList which the RealmResults is created on has been deleted. Instead, the RealmResults will be treated as empty forever. - Fixed a bug causing a bad version exception, when using findFirstAsync (#2115). -## 0.87.4 +## 0.87.4 (2016-01-28) * Updated Realm Core to 0.96.0. - Fixed bug causing BadVersionException or crashing core when running async queries. -## 0.87.3 +## 0.87.3 (2016-01-25) * IllegalArgumentException is now properly thrown when calling Realm.copyFromRealm() with a DynamicRealmObject (#2058). * Fixed a message in IllegalArgumentException thrown by the accessors of DynamicRealmObject (#2141). * Fixed RealmList not returning DynamicRealmObjects of the correct underlying type (#2143). @@ -381,21 +3400,21 @@ No changes since 0.91.1. - Fixed a bug where undetected deleted object might lead to seg. fault (#1945). - Better performance when deleting objects (#2015). -## 0.87.2 +## 0.87.2 (2016-01-08) * Removed explicit GC call when committing a transaction (#1925). * Fixed a bug when RealmObjectSchema.addField() was called with the PRIMARY_KEY modifier, the field was not set as a required field (#2001). * Fixed a bug which could throw a ConcurrentModificationException in RealmObject's or RealmResults' change listener (#1970). * Fixed RealmList.set() so it now correctly returns the old element instead of the new (#2044). * Fixed the deployment of source and javadoc jars (#1971). -## 0.87.1 +## 0.87.1 (2015-12-23) * Upgraded to NDK R10e. Using gcc 4.9 for all architectures. * Updated Realm Core to 0.95.6 - Fixed a bug where an async query can be copied incomplete in rare cases (#1717). * Fixed potential memory leak when using async query. -* Added a check to prevent removing a RealmChangeListener from a non-Looper thread (#1962). (Thank you @hohnamkung) +* Added a check to prevent removing a RealmChangeListener from a non-Looper thread (#1962). (Thank you @hohnamkung.) -## 0.87.0 +## 0.87.0 (2015-12-17) * Added Realm.asObservable(), RealmResults.asObservable(), RealmObject.asObservable(), DynamicRealm.asObservable() and DynamicRealmObject.asObservable(). * Added RealmConfiguration.Builder.rxFactory() and RxObservableFactory for custom RxJava observable factory classes. * Added Realm.copyFromRealm() for creating detached copies of Realm objects (#931). @@ -404,7 +3423,7 @@ No changes since 0.91.1. * Added support for ISO8601 based dates for JSON import. If JSON dates are invalid a RealmException will be thrown (#1213). * Added APK splits to gridViewExample (#1834). -## 0.86.1 +## 0.86.1 (2015-12-11) * Improved the performance of removing objects (RealmResults.clear() and RealmResults.remove()). * Updated Realm Core to 0.95.5. * Updated ProGuard configuration (#1904). @@ -416,7 +3435,7 @@ No changes since 0.91.1. * Fixed RealmChangeListener never called inside RealmResults (#1894). * Fixed crash when calling clear on a RealmList (#1886). -## 0.86.0 +## 0.86.0 (2015-12-03) * BREAKING CHANGE: The Migration API has been replaced with a new API. * BREAKING CHANGE: RealmResults.SORT_ORDER_ASCENDING and RealmResults.SORT_ORDER_DESCENDING constants have been replaced by Sort.ASCENDING and Sort.DESCENDING enums. * BREAKING CHANGE: RealmQuery.CASE_SENSITIVE and RealmQuery.CASE_INSENSITIVE constants have been replaced by Case.SENSITIVE and Case.INSENSITIVE enums. @@ -433,17 +3452,17 @@ No changes since 0.91.1. - Fixed a bug where RealmQuery.average(String) returned a wrong value for a nullable Long/Integer/Short/Byte field (#1803). - Fixed a bug where RealmQuery.average(String) wrongly counted the null value for average calculation (#1854). -## 0.85.1 +## 0.85.1 (2015-11-23) * Fixed a bug which could corrupt primary key information when updating from a Realm version <= 0.84.1 (#1775). -## 0.85.0 +## 0.85.0 (2016-11-19) * BREAKING CHANGE: Removed RealmEncryptionNotSupportedException since the encryption implementation changed in Realm's underlying storage engine. Encryption is now supported on all devices. * BREAKING CHANGE: Realm.executeTransaction() now directly throws any RuntimeException instead of wrapping it in a RealmException (#1682). * BREAKING CHANGE: RealmQuery.isNull() and RealmQuery.isNotNull() now throw IllegalArgumentException instead of RealmError if the fieldname is a linked field and the last element is a link (#1693). * Added Realm.isEmpty(). * Setters in managed object for RealmObject and RealmList now throw IllegalArgumentException if the value contains an invalid (unmanaged, removed, closed, from different Realm) object (#1749). * Attempting to refresh a Realm while a transaction is in process will now throw an IllegalStateException (#1712). -* The Realm AAR now also contains the ProGuard configuration (#1767). (Thank you @skyisle) +* The Realm AAR now also contains the ProGuard configuration (#1767). (Thank you @skyisle.) * Updated Realm Core to 0.95. - Removed reliance on POSIX signals when using encryption. @@ -454,7 +3473,7 @@ No changes since 0.91.1. * Fixed a memory leak when using relationships (#1285). * Fixed a bug causing cached column indices to be cleared too soon (#1732). -## 0.84.1 +## 0.84.1 (2015-10-28) * Updated Realm Core to 0.94.4. - Fixed a bug that could cause a crash when running the same query multiple times. * Updated ProGuard configuration. See [documentation](https://realm.io/docs/java/latest/#proguard) for more details. @@ -463,9 +3482,9 @@ No changes since 0.91.1. * Fixed a bug where simultaneous opening and closing a Realm from different threads might result in a NullPointerException (#1646). * Fixed a bug which made it possible to externally modify the encryption key in a RealmConfiguration (#1678). -## 0.84.0 +## 0.84.0 (2015-10-22) * Added support for async queries and transactions. -* Added support for parsing JSON Dates with timezone information. (Thank you @LateralKevin) +* Added support for parsing JSON Dates with timezone information. (Thank you @LateralKevin.) * Added RealmQuery.isEmpty(). * Added Realm.isClosed() method. * Added Realm.distinct() method. @@ -485,12 +3504,12 @@ No changes since 0.91.1. * Fixed a bug that made it possible to migrate open Realms, which could cause undefined behavior when querying, reading or writing data. * Fixed a bug causing column indices to be wrong for some edge cases. See #1611 for details. -## 0.83.1 +## 0.83.1 (2015-10-15) * Updated Realm Core to version 0.94.1. - Fixed a bug when using Realm.compactRealm() which could make it impossible to open the Realm file again. - Fixed a bug, so isNull link queries now always return true if any part is null. -## 0.83 +## 0.83 (2015-10-08) * BREAKING CHANGE: Database file format update. The Realm file created by this version cannot be used by previous versions of Realm. * BREAKING CHANGE: Removed deprecated methods and constructors from the Realm class. * BREAKING CHANGE: Introduced boxed types Boolean, Byte, Short, Integer, Long, Float and Double. Added null support. Introduced annotation @Required to indicate a field is not nullable. String, Date and byte[] became nullable by default which means a RealmMigrationNeededException will be thrown if an previous version of a Realm file is opened. @@ -501,7 +3520,7 @@ No changes since 0.91.1. * Opening a Realm file from one thread will no longer be blocked by a transaction from another thread. * Range restrictions of Date fields have been removed. Date fields now accepts any value. Milliseconds are still removed. -## 0.82.2 +## 0.82.2 (2015-09-04) * Fixed a bug which might cause failure when loading the native library. * Fixed a bug which might trigger a timeout in Context.finalize(). * Fixed a bug which might cause RealmObject.isValid() to throw an exception if the object is deleted. @@ -510,12 +3529,12 @@ No changes since 0.91.1. - Embedded crypto functions into Realm dynamic lib to avoid random issues on some devices. - Throw RealmEncryptionNotSupportedException if the device doesn't support Realm encryption. At least one device type (HTC One X) contains system bugs that prevents Realm's encryption from functioning properly. This is now detected, and an exception is thrown when trying to open/create an encrypted Realm file. It's up to the application to catch this and decide if it's OK to proceed without encryption instead. -## 0.82.1 +## 0.82.1 (2015-08-06) * Fixed a bug where using the wrong encryption key first caused the right key to be seen as invalid. * Fixed a bug where String fields were ignored when updating objects from JSON with null values. * Fixed a bug when calling System.exit(0), the process might hang. -## 0.82 +## 0.82 (2015-07-28) * BREAKING CHANGE: Fields with annotation @PrimaryKey are indexed automatically now. Older schemas require a migration. * RealmConfiguration.setModules() now accept ignore null values which Realm.getDefaultModule() might return. * Trying to access a deleted Realm object throw throws a proper IllegalStateException. @@ -526,10 +3545,10 @@ No changes since 0.91.1. * Fixed a bug where RealmQuery objects are prematurely garbage collected. * Removed RealmQuery.between() for link queries. -## 0.81.1 +## 0.81.1 (2015-06-22) * Fixed memory leak causing Realm to never release Realm objects. -## 0.81 +## 0.81 (2015-06-19) * Introduced RealmModules for working with custom schemas in libraries and apps. * Introduced Realm.getDefaultInstance(), Realm.setDefaultInstance(RealmConfiguration) and Realm.getInstance(RealmConfiguration). * Deprecated most constructors. They have been been replaced by Realm.getInstance(RealmConfiguration) and Realm.getDefaultInstance(). @@ -537,34 +3556,34 @@ No changes since 0.91.1. * Deprecated Realm.deleteFile(). It has been replaced by Realm.deleteRealm(RealmConfiguration). * Deprecated Realm.compactFile(). It has been replaced by Realm.compactRealm(RealmConfiguration). * RealmList.add(), RealmList.addAt() and RealmList.set() now copy unmanaged objects transparently into Realm. -* Realm now works with Kotlin (M12+). (Thank you @cypressious) +* Realm now works with Kotlin (M12+). (Thank you @cypressious.) * Fixed a performance regression introduced in 0.80.3 occurring during the validation of the Realm schema. * Added a check to give a better error message when null is used as value for a primary key. * Fixed unchecked cast warnings when building with Realm. * Cleaned up examples (remove old test project). * Added checking for missing generic type in RealmList fields in annotation processor. -## 0.80.3 +## 0.80.3 (2015-05-22) * Calling Realm.copyToRealmOrUpdate() with an object with a null primary key now throws a proper exception. * Fixed a bug making it impossible to open Realms created by Realm-Cocoa if a model had a primary key defined. * Trying to using Realm.copyToRealmOrUpdate() with an object with a null primary key now throws a proper exception. * RealmChangedListener now also gets called on the same thread that did the commit. * Fixed bug where Realm.createOrUpdateWithJson() reset Date and Binary data to default values if not found in the JSON output. * Fixed a memory leak when using RealmBaseAdapter. -* RealmBaseAdapter now allow RealmResults to be null. (Thanks @zaki50) +* RealmBaseAdapter now allow RealmResults to be null. (Thanks @zaki50.) * Fixed a bug where a change to a model class (`RealmList` to `RealmList`) would not throw a RealmMigrationNeededException. * Fixed a bug where setting multiple RealmLists didn't remove the previously added objects. * Solved ConcurrentModificationException thrown when addChangeListener/removeChangeListener got called in the onChange. (Thanks @beeender) * Fixed duplicated listeners in the same realm instance. Trying to add duplicated listeners is ignored now. (Thanks @beeender) -## 0.80.2 +## 0.80.2 (2015-05-04) * Trying to use Realm.copyToRealmOrUpdate() with an object with a null primary key now throws a proper exception. * RealmMigrationNeedException can now return the path to the Realm that needs to be migrated. * Fixed bug where creating a Realm instance with a hashcode collision no longer returned the wrong Realm instance. * Updated Realm Core to version 0.89.2 - fixed bug causing a crash when opening an encrypted Realm file on ARM64 devices. -## 0.80.1 +## 0.80.1 (2015-04-16) * Realm.createOrUpdateWithJson() no longer resets fields to their default value if they are not found in the JSON input. * Realm.compactRealmFile() now uses Realm Core's compact() method which is more failure resilient. * Realm.copyToRealm() now correctly handles referenced child objects that are already in the Realm. @@ -583,7 +3602,7 @@ No changes since 0.91.1. * Added RealmQuery.isNull() and RealmQuery.isNotNull() for querying relationships. * Fixed a potential NPE in the RealmList constructor. -## 0.80 +## 0.80 (2015-03-11) * Queries on relationships can be case sensitive. * Fixed bug when importing JSONObjects containing NULL values. * Fixed crash when trying to remove last element of a RealmList. @@ -593,11 +3612,11 @@ No changes since 0.91.1. * Added support for static fields in RealmObjects. * Realm.writeEncryptedCopyTo() has been reenabled. -## 0.79.1 +## 0.79.1 (2015-02-20) * copyToRealm() no longer crashes on cyclic data structures. * Fixed potential crash when using copyToRealmOrUpdate with an object graph containing a mix of elements with and without primary keys. -## 0.79 +## 0.79 (2015-02-16) * Added support for ARM64. * Added RealmQuery.not() to negate a query condition. * Added copyToRealmOrUpdate() and createOrUpdateFromJson() methods, that works for models with primary keys. @@ -612,7 +3631,7 @@ No changes since 0.91.1. * Removed methods deprecated in 0.76. Now Realm.allObjectsSorted() and RealmQuery.findAllSorted() need to be used instead. * Reimplemented Realm.allObjectSorted() for better performance. -## 0.78 +## 0.78 (2015-01-22) * Added proper support for encryption. Encryption support is now included by default. Keys are now 64 bytes long. * Added support to write an encrypted copy of a Realm. * Realm no longer incorrectly warns that an instance has been closed too many times. @@ -620,7 +3639,7 @@ No changes since 0.91.1. * Fixed bug causing Realms to be cached during a RealmMigration resulting in invalid realms being returned from Realm.getInstance(). * Updated core to 0.88. -## 0.77 +## 0.77 (2015-01-16) * Added Realm.allObjectsSorted() and RealmQuery.findAllSorted() and extending RealmResults.sort() for multi-field sorting. * Added more logging capabilities at the JNI level. * Added proper encryption support. NOTE: The key has been increased from 32 bytes to 64 bytes (see example). @@ -639,7 +3658,7 @@ No changes since 0.91.1. * RealmList.remove() now properly returns the removed object. * Calling realm.close() no longer prevent updates to other open realm instances on the same thread. -## 0.76.0 +## 0.76.0 (2014-12-19) * RealmObjects can now be imported using JSON. * Gradle wrapper updated to support Android Studio 1.0. * Fixed bug in RealmObject.equals() so it now correctly compares two objects from the same Realm. @@ -651,13 +3670,13 @@ No changes since 0.91.1. * Close the Realm instance after migrations. * Added a check to deny the writing of objects outside of a transaction. -## 0.75.1 (03 December 2014) +## 0.75.1 (2014-12-03) * Changed sort to be an in-place method. * Renamed SORT_ORDER_DECENDING to SORT_ORDER_DESCENDING. * Added sorting functionality to allObjects() and findAll(). * Fixed bug when querying a date column with equalTo(), it would act as lessThan() -## 0.75.0 (28 Nov 2014) +## 0.75.0 (2014-11-28) * Realm now implements Closeable, allowing better cleanup of native resources. * Added writeCopyTo() and compactRealmFile() to write and compact a Realm to a new file. * RealmObject.toString(), equals() and hashCode() now support models with cyclic references. @@ -669,7 +3688,7 @@ No changes since 0.91.1. * Fixed bug so Realm no longer throws an Exception when removing the last object. * Fixed bug in RealmResults which prevented sub-querying. -## 0.74.0 (19 Nov 2014) +## 0.74.0 (2014-11-19) * Added support for more field/accessors naming conventions. * Added case sensitive versions of string comparison operators equalTo and notEqualTo. * Added where() to RealmList to initiate queries. @@ -682,17 +3701,17 @@ No changes since 0.91.1. * Consistent handling of UTF-8 strings. * removeFromRealm() now calls moveLastOver() which is faster and more reliable when deleting multiple objects. -## 0.73.1 (05 Nov 2014) +## 0.73.1 (2014-11-05) * Fixed a bug that would send infinite notifications in some instances. -## 0.73.0 (04 Nov 2014) +## 0.73.0 (2014-11-04) * Fixed a bug not allowing queries with more than 1024 conditions. * Rewritten the notification system. The API did not change but it's now much more reliable. * Added support for switching auto-refresh on and off (Realm.setAutoRefresh). * Added RealmBaseAdapter and an example using it. * Added deleteFromRealm() method to RealmObject. -## 0.72.0 (27 Oct 2014) +## 0.72.0 (2014-10-27) * Extended sorting support to more types: boolean, byte, short, int, long, float, double, Date, and String fields are now supported. * Better support for Java 7 and 8 in the annotations processor. * Better support for the Eclipse annotations processor. @@ -702,7 +3721,7 @@ No changes since 0.91.1. * Faster implementation of RealmQuery.findFirst(). * Upgraded core to 0.85.1 (deep copying of strings in queries; preparation for link queries). -## 0.71.0 (07 Oct 2014) +## 0.71.0 (2014-10-07) * Simplified the release artifact to a single Jar file. * Added support for Eclipse. * Added support for deploying to Maven. @@ -715,9 +3734,9 @@ No changes since 0.91.1. * Added a new example about concurrency. * Upgraded to core 0.84.0. -## 0.70.1 (30 Sep 2014) +## 0.70.1 (2014-09-30) * Enabled unit testing for the realm project. * Fixed handling of camel-cased field names. -## 0.70.0 (29 Sep 2014) +## 0.70.0 (2014-09-29) * This is the first public beta release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e25a9af5f1..b350475a6e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ We love contributions to Realm! If you'd like to contribute code, documentation, Realm welcomes all contributions! The only requirement we have is that, like many other projects, we need to have a [Contributor License Agreement](https://en.wikipedia.org/wiki/Contributor_License_Agreement) (CLA) in place before we can accept any external code. Our own CLA is a modified version of the Apache Software Foundation’s CLA. -[Please submit your CLA electronically using our Google form](https://docs.google.com/forms/d/1bVp-Wp5nmNFz9Nx-ngTmYBVWVdwTyKj4T0WtfVm0Ozs/viewform?fbzx=4154977190905366979) so we can accept your submissions. The GitHub username you file there will need to match that of your Pull Requests. If you have any questions or cannot file the CLA electronically, you can email . +[Please submit your CLA electronically using our Google form](https://docs.google.com/forms/d/e/1FAIpQLSeQ9ROFaTu9pyrmPhXc-dEnLD84DbLuT_-tPNZDOL9J10tOKQ/viewform) so we can accept your submissions. The GitHub username you file there will need to match that of your Pull Requests. If you have any questions or cannot file the CLA electronically, you can email . ## Repository Guidelines @@ -31,6 +31,18 @@ Realm welcomes all contributions! The only requirement we have is that, like man While we havn't described our code style yet, please just follow the existing style you see in the files you change. +For source code written in C++, we format it using `clang-format`. You can use the [plugin](https://plugins.jetbrains.com/plugin/8396-clangformatij): mark the entire file and right-click to execute `clang-format` before committing any changes. Of course, if you don't use Android Studio to edit C++ code, run `clang-format` on the command-line. + +### Nullability by Annotataion + +To improve code quality and usability in Kotlin, nullability of parameters and return types must be annotated with JSR305 annotations. + +If a parameter is nullable, you must add `@Nullable` annotation to the parameter. On the other hand, if a parameter is non-null, you don't need to add `@Nonnull` annotation since all parameters are treated as `@Nonnull` by default. + +For return types, there is no default nullability. If a method can return `null` as a return value, you must add `@Nullable` annotation to the return type. Currently, `Nonnull` annotation is not mandatory if the method never return `null`. + +When you add a new package, you must add `package-info.java` and add `@javax.annotation.ParametersAreNonnullByDefault` to the package. Please note that you can't add multiple `package-info.java` in the same package but different location (for example, main and androidTest). When you add a package to both main and androidTest, you only need to add `package-info.java` to main. + ### Unit Tests All PR's must be accompanied by related unit tests. All bug fixes must have a unit test proving that the bug is fixed. diff --git a/Dockerfile b/Dockerfile index f3c06ffa5e..cb899402b4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,61 +1,98 @@ -FROM ubuntu:16.04 +FROM ubuntu:22.04 # Locales -RUN locale-gen en_US.UTF-8 +RUN apt-get clean && apt-get -y update && apt-get install -y locales && locale-gen en_US.UTF-8 ENV LANG "en_US.UTF-8" ENV LANGUAGE "en_US.UTF-8" ENV LC_ALL "en_US.UTF-8" +ENV TZ=Europe/Copenhagen +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone # Set the environment variables -ENV JAVA_HOME /usr/lib/jvm/java-8-openjdk-amd64 +ENV JAVA_HOME /usr/lib/jvm/java-11-openjdk-amd64 +ENV JAVA8_HOME /usr/lib/jvm/java-8-openjdk-amd64 ENV ANDROID_HOME /opt/android-sdk-linux # Need by cmake ENV ANDROID_NDK_HOME /opt/android-ndk -ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools +ENV ANDROID_NDK /opt/android-ndk +ENV PATH ${PATH}:${ANDROID_HOME}/emulator:${ANDROID_HOME}/cmdline-tools/latest:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools ENV PATH ${PATH}:${NDK_HOME} +ENV NDK_CCACHE /usr/bin/ccache +ENV CCACHE_CPP2 yes +ENV REALM_DISABLE_ANALYTICS true -# Install the JDK -# We are going to need some 32 bit binaries because aapt requires it -# file is need by the script that creates NDK toolchains -RUN DEBIAN_FRONTEND=noninteractive dpkg --add-architecture i386 \ +# Keep the packages in alphabetical order to make it easy to avoid duplication +# tzdata needs to be installed first. See https://askubuntu.com/questions/909277/avoiding-user-interaction-with-tzdata-when-installing-certbot-in-a-docker-contai +# `file` is need by the Android Emulator +RUN DEBIAN_FRONTEND=noninteractive \ && apt-get update -qq \ - && apt-get install -y file git curl wget zip unzip \ - bsdmainutils \ - build-essential \ - openjdk-8-jdk-headless \ - libc6:i386 libstdc++6:i386 libgcc1:i386 libncurses5:i386 libz1:i386 \ + && apt-get install -y tzdata \ + && apt-get install -y \ + bsdmainutils \ + bridge-utils \ + build-essential \ + ccache \ + curl \ + file \ + git \ + jq \ + libc6 \ + libgcc1 \ + libglu1 \ + libncurses5 \ + libstdc++6 \ + libz1 \ + libvirt-clients \ + libvirt-daemon-system \ + openjdk-11-jdk-headless \ + openjdk-8-jdk-headless \ + qemu-kvm \ + s3cmd \ + unzip \ + virt-manager \ + wget \ + zip \ + ninja-build \ && apt-get clean # Install the Android SDK +# See https://developer.android.com/studio/index.html#downloads for latest version RUN cd /opt && \ - wget -q https://dl.google.com/android/repository/tools_r25.1.7-linux.zip -O android-tools-linux.zip && \ - unzip android-tools-linux.zip -d ${ANDROID_HOME} && \ + wget -q https://dl.google.com/android/repository/commandlinetools-linux-8512546_latest.zip -O android-tools-linux.zip && \ + mkdir --parents ${ANDROID_HOME}/cmdline-tools/latest && \ + unzip android-tools-linux.zip -d ${ANDROID_HOME}/cmdline-tools/latest && \ + mv ${ANDROID_HOME}/cmdline-tools/latest/cmdline-tools/* ${ANDROID_HOME}/cmdline-tools/latest/ && \ rm -f android-tools-linux.zip # Grab what's needed in the SDK -# ↓ updates tools to at least 25.1.7, but that prints 'Nothing was installed' (so I don't check the outputs). -RUN echo y | android update sdk --no-ui --all --filter tools > /dev/null -RUN echo y | android update sdk --no-ui --all --filter platform-tools | grep 'package installed' -RUN echo y | android update sdk --no-ui --all --filter build-tools-24.0.0 | grep 'package installed' -RUN echo y | android update sdk --no-ui --all --filter extra-android-m2repository | grep 'package installed' -RUN echo y | android update sdk --no-ui --all --filter android-24 | grep 'package installed' - -# Install the NDK -RUN mkdir /opt/android-ndk-tmp && \ - cd /opt/android-ndk-tmp && \ - wget -q http://dl.google.com/android/ndk/android-ndk-r10e-linux-x86_64.bin -O android-ndk.bin && \ - chmod a+x ./android-ndk.bin && \ - ./android-ndk.bin && \ - mv android-ndk-r10e /opt/android-ndk && \ - rm -rf /opt/android-ndk-tmp && \ - chmod -R a+rX /opt/android-ndk - -# Install cmake -RUN mkdir /opt/cmake-tmp && \ - cd /opt/cmake-tmp && \ - wget -q https://dl.google.com/android/repository/cmake-3.6.3133135-linux-x86_64.zip -O cmake-linux.zip && \ - unzip cmake-linux.zip -d ${ANDROID_HOME}/cmake && \ - rm -rf /opt/cmake-tmp - -# Make the SDK universally readable -RUN chmod -R a+rX ${ANDROID_HOME} +RUN sdkmanager --update + +# Accept licenses before installing components, no need to echo y for each component +# License is valid for all the standard components in versions installed from this file +# Non-standard components: MIPS system images, preview versions, GDK (Google Glass) and Android Google TV require separate licenses, not accepted there +RUN yes | sdkmanager --licenses + +# SDKs +# The `yes` is for accepting all non-standard tool licenses. +# Please keep all sections in descending order! +RUN yes | sdkmanager \ + 'build-tools;30.0.3' \ + 'emulator' \ + 'extras;android;m2repository' \ + 'platforms;android-30' \ + 'platform-tools' \ + 'ndk;23.1.7779620' \ + 'system-images;android-31;default;x86_64' + +# Make the SDK universally writable +RUN chmod -R a+rwX ${ANDROID_HOME} + +# Ensure a new enough version of CMake is available. +RUN cd /opt \ + && wget -nv https://cmake.org/files/v3.22/cmake-3.27.7-linux-x86_64.tar.gz \ + && tar zxf cmake-3.27.7-linux-x86_64.tar.gz + +# Workaround for https://issuetracker.google.com/issues/206099937 +RUN ln -s /usr/bin/ninja /opt/cmake-3.22.1-linux-x86_64/bin/ninja + +ENV PATH "/opt/cmake-3.27.7-linux-x86_64/bin:$PATH" diff --git a/Jenkinsfile b/Jenkinsfile index 43765b97ca..c6c696b4b7 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,157 +1,489 @@ #!groovy +@Library('realm-ci') _ + import groovy.json.JsonOutput -def buildSuccess = false +// CONSTANTS + +// Branches from which we release SNAPSHOT's. Only release branches need to run on actual hardware. +releaseBranches = ['main', 'next-major', 'support-new-datatypes', 'releases', 'release/transformer-api' ] +// Branches that are "important", so if they do not compile they will generate a Slack notification +slackNotificationBranches = [ 'main', 'releases', 'next-major', 'support-new-datatypes', 'release/transformer-api' ] +// WARNING: Only set to `false` as an absolute last resort. Doing this will disable all integration +// tests. +enableIntegrationTests = true + +// RUNTIME PROPERTIES + +// Will store whether or not this build was successful. +buildSuccess = false +// Will be set to `true` if this build is a full release that should be available on Maven Central. +// This is determined by comparing the current git tag to the version number of the build. +publishBuild = false +mongoDbRealmContainer = null +mongoDbRealmCommandServerContainer = null +emulatorContainer = null +dockerNetworkId = UUID.randomUUID().toString() +currentBranch = (env.CHANGE_BRANCH == null) ? env.BRANCH_NAME : env.CHANGE_BRANCH +isReleaseBranch = releaseBranches.contains(currentBranch) +// FIXME: Always used the emulator until we can enable more reliable devices +// 'android' nodes have android devices attached and 'brix' are physical machines in Copenhagen. +// nodeSelector = (releaseBranches.contains(currentBranch)) ? 'android' : 'docker-cph-03' // Switch to `brix` when all CPH nodes work: https://jira.mongodb.org/browse/RCI-14 +nodeSelector = 'docker-cph-01' try { - node('android') { - // Allocate a custom workspace to avoid having % in the path (it breaks ld) - ws('/tmp/realm-java') { - stage 'SCM' - checkout scm - // Make sure not to delete the folder that Jenkins allocates to store scripts - sh 'git clean -ffdx -e .????????' - // Update submodule for object-store - sh 'git submodule sync' - sh 'git submodule update --init --force' - - stage 'Docker build' - def buildEnv = docker.build 'realm-java:snapshot' - buildEnv.inside("-e HOME=/tmp -e _JAVA_OPTIONS=-Duser.home=/tmp --privileged -v /dev/bus/usb:/dev/bus/usb -v ${env.HOME}/gradle-cache:/tmp/.gradle -v ${env.HOME}/.android:/tmp/.android") { - stage 'JVM tests' - try { - gradle 'assemble check javadoc' - } finally { - storeJunitResults 'realm/realm-annotations-processor/build/test-results/test/TEST-*.xml' - storeJunitResults 'examples/unitTestExample/build/test-results/**/TEST-*.xml' - step([$class: 'LintPublisher']) + node(nodeSelector) { + timeout(time: 150, unit: 'MINUTES') { + // Allocate a custom workspace to avoid having % in the path (it breaks ld) + ws('/tmp/realm-java') { + stage('SCM') { + checkout([ + $class : 'GitSCM', + branches : scm.branches, + gitTool : 'native git', + extensions : scm.extensions + [ + [$class: 'CleanCheckout'], + [$class: 'SubmoduleOption', recursiveSubmodules: true] + ], + userRemoteConfigs: scm.userRemoteConfigs + ]) } - stage 'Static code analysis' - try { - gradle('realm', 'findbugs pmd checkstyle') - } finally { - publishHTML(target: [allowMissing: false, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'realm/realm-library/build/findbugs', reportFiles: 'findbugs-output.html', reportName: 'Findbugs issues']) - publishHTML(target: [allowMissing: false, alwaysLinkToLastBuild: false, keepAll: true, reportDir: 'realm/realm-library/build/reports/pmd', reportFiles: 'pmd.html', reportName: 'PMD Issues']) - step([$class: 'CheckStylePublisher', - canComputeNew: false, - defaultEncoding: '', - healthy: '', - pattern: 'realm/realm-library/build/reports/checkstyle/checkstyle.xml', - unHealthy: '' - ]) + // Check type of Build. We are treating this as a release build if we are building + // the exact Git SHA that was tagged. + echo "Building from branch: $currentBranch" + gitTag = readGitTag() + echo "Git tag: ${gitTag ?: 'none'}" + if (!gitTag) { + gitSha = sh(returnStdout: true, script: 'git rev-parse HEAD').trim().take(8) + echo "Building non-release: ${gitSha}" + setBuildName(gitSha) + publishBuild = false + } else { + def version = readFile('version.txt').trim() + if (gitTag != "v${version}") { + error "Git tag '${gitTag}' does not match v${version}" + } else { + echo "Building release: '${gitTag}'" + setBuildName("Tag ${gitTag}") + sh """ + set +x + sh tools/publish_release.sh verify + """ + publishBuild = true + } } - stage 'Run instrumented tests' - boolean archiveLog = true - String backgroundPid - try { - backgroundPid = startLogCatCollector() - gradle('realm', 'connectedUnitTests') - archiveLog = false; - } finally { - stopLogCatCollector(backgroundPid, archiveLog) - storeJunitResults 'realm/realm-library/build/outputs/androidTest-results/connected/TEST-*.xml' + // Toggles for PR vs. Master builds. + // - For PR's, we favor speed > absolute correctness. So we just build for x86, use an + // emulator and run unit tests for the ObjectServer variant. + // - For branches from which we make releases, we build all architectures and run tests + // on an actual device. + def useEmulator = false + def emulatorImage = "" + def buildFlags = "" + def instrumentationTestTarget = "connectedAndroidTest" + def deviceSerial = "" + + if (!isReleaseBranch) { + // Build development branch + useEmulator = true + emulatorImage = "system-images;android-31;default;x86_64" + // Build core from source instead of doing it from binary + buildFlags = "-PbuildTargetABIs=x86_64 -PenableLTO=false -PbuildCore=true" + instrumentationTestTarget = "connectedObjectServerDebugAndroidTest" + deviceSerial = "emulator-5554" + } else { + // Build main/release branch + // FIXME: Use emulator until we can get reliable devices on CI. + // But still build all ABI's and run all types of tests. + useEmulator = true + emulatorImage = "system-images;android-31;default;x86_64" + buildFlags = "-PenableLTO=true -PbuildCore=true" + instrumentationTestTarget = "connectedAndroidTest" + deviceSerial = "emulator-5554" } - // TODO: add support for running monkey on the example apps + try { + + def buildEnv = null + stage('Prepare Docker Images') { + // TODO Caching is currently disabled (with -do-not-cache suffix) due to the upload speed + // in Copenhagen being too slow. So the upload times out. + buildEnv = buildDockerEnv("ci/realm-java:main", push: currentBranch == 'main-do-not-cache') + def props = readProperties file: 'dependencies.list' + echo "Version in dependencies.list: ${props.MONGODB_REALM_SERVER}" + def mdbRealmImage = docker.image("docker.pkg.github.com/realm/ci/mongodb-realm-test-server:${props.MONGODB_REALM_SERVER}") + docker.withRegistry('https://docker.pkg.github.com', 'github-packages-token') { + mdbRealmImage.pull() + } + def commandServerEnv = docker.build 'mongodb-realm-command-server', "tools/sync_test_server" + + // Prepare Docker containers used by Instrumentation tests + // TODO: How much of this logic can be moved to start_server.sh for shared logic with local testing. + withCredentials([ + [$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'realm-kotlin-baas-aws-credentials', accessKeyVariable: 'BAAS_AWS_ACCESS_KEY_ID', secretKeyVariable: 'BAAS_AWS_SECRET_ACCESS_KEY'] + ]) { + def tempDir = runCommand('mktemp -d -t app_config.XXXXXXXXXX') + sh "tools/sync_test_server/app_config_generator.sh ${tempDir} tools/sync_test_server/app_template partition auto testapp1" + sh "tools/sync_test_server/app_config_generator.sh ${tempDir} tools/sync_test_server/app_template partition email testapp2" + sh "tools/sync_test_server/app_config_generator.sh ${tempDir} tools/sync_test_server/app_template flex function testapp3" + sh "docker network create ${dockerNetworkId}" + mongoDbRealmContainer = mdbRealmImage.run("--network ${dockerNetworkId} -v$tempDir:/apps -e AWS_ACCESS_KEY_ID='$BAAS_AWS_ACCESS_KEY_ID' -e AWS_SECRET_ACCESS_KEY='$BAAS_AWS_SECRET_ACCESS_KEY'") + mongoDbRealmCommandServerContainer = commandServerEnv.run("--network container:${mongoDbRealmContainer.id} -v$tempDir:/apps") + sh "timeout 60 sh -c \"while [[ ! -f $tempDir/testapp1/app_id || ! -f $tempDir/testapp2/app_id ]]; do echo 'Waiting for server to start'; sleep 1; done\"" + } + } + + // There is a chance that real devices are attached to the host, so if the emulator is + // running we need to make sure that ADB and tests targets the correct device. + String restrictDevice = "" + if (deviceSerial != null) { + restrictDevice = "-e ANDROID_SERIAL=${deviceSerial} " + } + + buildEnv.inside("-e HOME=/tmp " + + "-e _JAVA_OPTIONS=-Duser.home=/tmp " + + "--privileged " + + "-v /dev/kvm:/dev/kvm " + + "-v /dev/bus/usb:/dev/bus/usb " + + "-v ${env.HOME}/gradle-cache:/tmp/.gradle " + + "-v ${env.HOME}/.android:/tmp/.android " + + "-v ${env.HOME}/ccache:/tmp/.ccache " + + restrictDevice + + "-e REALM_CORE_DOWNLOAD_DIR=/tmp/.gradle " + + "--network container:${mongoDbRealmContainer.id} ") { - if (env.BRANCH_NAME == 'master') { - stage 'Collect metrics' - collectAarMetrics() + // Lock required around all usages of Gradle as it isn't + // able to share its cache between builds. + lock("${env.NODE_NAME}-android") { + if (useEmulator) { + // TODO: We should wait until the emulator is online. For now assume it starts fast enough + // before the tests will run, since the library needs to build first. + sh """yes '\n' | avdmanager create avd -n CIEmulator -k '${emulatorImage}' --force""" + sh "adb start-server" // https://stackoverflow.com/questions/56198290/problems-with-adb-exe + // Need to go to ANDROID_HOME due to https://askubuntu.com/questions/1005944/emulator-avd-does-not-launch-the-virtual-device + sh "cd \$ANDROID_HOME/tools && emulator -avd CIEmulator -no-boot-anim -no-window -wipe-data -noaudio -partition-size 4096 -memory 1536 &" + try { + runBuild(buildFlags, instrumentationTestTarget) + } finally { + sh "adb emu kill" + } + } else { + runBuild(buildFlags, instrumentationTestTarget) + } - stage 'Publish to OJO' - withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'bintray', passwordVariable: 'BINTRAY_KEY', usernameVariable: 'BINTRAY_USER']]) { - sh "chmod +x gradlew && ./gradlew -PbintrayUser=${env.BINTRAY_USER} -PbintrayKey=${env.BINTRAY_KEY} assemble ojoUpload --stacktrace" + // Release the library if needed + if (publishBuild) { + runPublish() + } + } + } + } finally { + // We assume that creating these containers and the docker network can be considered an atomic operation. + if (mongoDbRealmContainer != null && mongoDbRealmCommandServerContainer != null) { + archiveServerLogs(mongoDbRealmContainer.id, mongoDbRealmCommandServerContainer.id) + mongoDbRealmContainer.stop() + mongoDbRealmCommandServerContainer.stop() + sh "docker network rm ${dockerNetworkId}" + } + if (emulatorContainer != null) { + emulatorContainer.stop() } } } } + currentBuild.rawBuild.setResult(Result.SUCCESS) + buildSuccess = true } - currentBuild.rawBuild.setResult(Result.SUCCESS) - buildSuccess = true } catch(Exception e) { currentBuild.rawBuild.setResult(Result.FAILURE) buildSuccess = false throw e } finally { - if (['master', 'releases'].contains(env.BRANCH_NAME) && !buildSuccess) { + if (slackNotificationBranches.contains(currentBranch)) { node { - withCredentials([[$class: 'StringBinding', credentialsId: 'slack-java-url', variable: 'SLACK_URL']]) { - def payload = JsonOutput.toJson([ - username: 'Mr. Jenkins', - icon_emoji: ':jenkins:', - attachments: [[ - 'title': "The ${env.BRANCH_NAME} branch is broken!", - 'text': "<${env.BUILD_URL}|Click here> to check the build.", - 'color': "danger" - ]] - ]) - sh "curl -X POST --data-urlencode \'payload=${payload}\' ${env.SLACK_URL}" + withCredentials([[$class: 'StringBinding', credentialsId: 'slack-webhook-java-ci-channel', variable: 'SLACK_URL']]) { + def payload = null + if (!buildSuccess) { + payload = JsonOutput.toJson([ + username: "Realm CI", + icon_emoji: ":realm_new:", + text: "*The ${currentBranch} branch is broken!*\n<${env.BUILD_URL}|Click here> to check the build." + ]) + } else if (currentBuild.getPreviousBuild() && currentBuild.getPreviousBuild().getResult().toString() != "SUCCESS" && buildSuccess) { + payload = JsonOutput.toJson([ + username: "Realm CI", + icon_emoji: ":realm_new:", + text: "*${currentBranch} is back to normal!*\n<${env.BUILD_URL}|Click here> to check the build." + ]) + } + + if (payload != null) { + sh "curl -X POST --data-urlencode \'payload=${payload}\' ${env.SLACK_URL}" + } } } } } +// Runs all build steps +def runBuild(buildFlags, instrumentationTestTarget) { + + stage('Build') { + withCredentials([ + [$class: 'StringBinding', credentialsId: 'maven-central-java-ring-file', variable: 'SIGN_KEY'], + [$class: 'StringBinding', credentialsId: 'maven-central-java-ring-file-password', variable: 'SIGN_KEY_PASSWORD'], + ]) { + sh "chmod +x gradlew" + def signingFlags = "" + if (isReleaseBranch) { + signingFlags = "-PsignBuild=true -PsignSecretRingFile=\"${SIGN_KEY}\" -PsignPassword=${SIGN_KEY_PASSWORD}" + } + sh "./gradlew assemble ${buildFlags} ${signingFlags} --stacktrace" + } + } + + stage('Tests') { + parallel 'JVM' : { + try { + sh "chmod +x gradlew && ./gradlew check ${buildFlags} --stacktrace" + } finally { + storeJunitResults 'realm/realm-annotations-processor/build/test-results/test/TEST-*.xml' + storeJunitResults 'examples/unitTestExample/build/test-results/**/TEST-*.xml' + storeJunitResults 'realm/realm-library/build/test-results/**/TEST-*.xml' + step([$class: 'LintPublisher']) + } + }, + // FIXME https://github.com/realm/realm-java/issues/7593 + // 'JVM8 introExample check' : { + // // Force build with JVM8, by disabling the cache, and check introExample. + // sh """ + // cd examples/moduleExample + // JAVA_HOME=\$JAVA8_HOME ../gradlew check ${buildFlags} --stacktrace + // """ + // }, + 'Realm Transformer' : { + try { + gradle('realm-transformer', 'check') + } finally { + storeJunitResults 'realm-transformer/build/test-results/test/TEST-*.xml' + } + }, + // 'Static code analysis' : { + // try { + // gradle('realm', "spotbugsMain pmd checkstyle ${buildFlags}") + // } finally { + // publishHTML(target: [ + // allowMissing: false, + // alwaysLinkToLastBuild: false, + // keepAll: true, + // reportDir: 'realm/realm-library/build/reports/spotbugs', + // reportFiles: 'main.html', + // reportName: 'Spotbugs report' + // ]) + + // publishHTML(target: [ + // allowMissing: false, + // alwaysLinkToLastBuild: false, + // keepAll: true, + // reportDir: 'realm/realm-library/build/reports/pmd', + // reportFiles: 'pmd.html', + // reportName: 'PMD report' + // ]) -def String startLogCatCollector() { - sh '''adb logcat -c - adb logcat -v time > "logcat.txt" & - echo $! > pid - ''' - return readFile("pid").trim() + // publishHTML(target: [ + // allowMissing: false, + // alwaysLinkToLastBuild: false, + // keepAll: true, + // reportDir: 'realm/realm-library/build/reports/checkstyle', + // reportFiles: 'checkstyle.html', + // reportName: 'Checkstyle report' + // ]) + // } + // }, + 'Gradle Plugin' : { + try { + gradle('gradle-plugin', 'check') + } finally { + storeJunitResults 'gradle-plugin/build/test-results/test/TEST-*.xml' + } + }, + 'JavaDoc': { + sh "./gradlew javadoc ${buildFlags} --stacktrace" + } + } + + stage('Device Tests') { + if (enableIntegrationTests) { + String backgroundPid + try { + backgroundPid = startLogCatCollector() + forwardAdbPorts() + gradle('realm', "${instrumentationTestTarget} ${buildFlags}") + gradle('examples', ":unitTestExample:connectedDebugAndroidTest") + } finally { + stopLogCatCollector(backgroundPid) + storeJunitResults 'realm/realm-library/build/outputs/androidTest-results/connected/**/TEST-*.xml' + storeJunitResults 'realm/kotlin-extensions/build/outputs/androidTest-results/connected/**/TEST-*.xml' + } + } else { + echo "Instrumentation tests were disabled." + } + } + + + // TODO: add support for running monkey on the example apps + + def collectMetrics = ['main'].contains(currentBranch) + echo "Collecting metrics: $collectMetrics" + if (collectMetrics) { + stage('Collect metrics') { + collectAarMetrics() + } + } + + echo "Releasing SNAPSHOT: ($isReleaseBranch, $publishBuild)" + if (isReleaseBranch && !publishBuild) { + stage('Publish SNAPSHOT') { + withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: 'maven-central-credentials', passwordVariable: 'MAVEN_CENTRAL_PASSWORD', usernameVariable: 'MAVEN_CENTRAL_USER']]) { + sh "chmod +x gradlew && ./gradlew mavenCentralUpload ${buildFlags} -PossrhUsername='$MAVEN_CENTRAL_USER' -PossrhPassword='$MAVEN_CENTRAL_PASSWORD' --stacktrace" + } + } + } +} + +def runPublish() { + stage('Publish Release') { + withCredentials([ + [$class: 'StringBinding', credentialsId: 'maven-central-java-ring-file', variable: 'SIGN_KEY'], + [$class: 'StringBinding', credentialsId: 'maven-central-java-ring-file-password', variable: 'SIGN_KEY_PASSWORD'], + [$class: 'StringBinding', credentialsId: 'slack-webhook-java-ci-channel', variable: 'SLACK_URL_CI'], + [$class: 'StringBinding', credentialsId: 'slack-webhook-releases-channel', variable: 'SLACK_URL_RELEASE'], + [$class: 'UsernamePasswordMultiBinding', credentialsId: 'maven-central-credentials', passwordVariable: 'MAVEN_CENTRAL_PASSWORD', usernameVariable: 'MAVEN_CENTRAL_USER'], + [$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'DOCS_S3_ACCESS_KEY', credentialsId: 'mongodb-realm-docs-s3', secretKeyVariable: 'DOCS_S3_SECRET_KEY'], + [$class: 'AmazonWebServicesCredentialsBinding', accessKeyVariable: 'REALM_S3_ACCESS_KEY', credentialsId: 'tightdb-s3-ci', secretKeyVariable: 'REALM_S3_SECRET_KEY'] + ]) { + // TODO Make sure that buildFlags and signingFlags are unified across builds + sh """ + set +x + sh tools/publish_release.sh '$MAVEN_CENTRAL_USER' '$MAVEN_CENTRAL_PASSWORD' \ + '$REALM_S3_ACCESS_KEY' '$REALM_S3_SECRET_KEY' \ + '$DOCS_S3_ACCESS_KEY' '$DOCS_S3_SECRET_KEY' \ + '$SLACK_URL_RELEASE' '$SLACK_URL_CI' \ + '-PsignBuild=true -PsignSecretRingFile="${SIGN_KEY}" -PsignPassword=${SIGN_KEY_PASSWORD} -PenableLTO=true -PbuildCore=true' + """ + } + } +} + + +def forwardAdbPorts() { + sh """ adb reverse tcp:9080 tcp:9080 && adb reverse tcp:9443 tcp:9443 && + adb reverse tcp:8888 tcp:8888 && adb reverse tcp:9090 tcp:9090 + """ } -def stopLogCatCollector(String backgroundPid, boolean archiveLog) { - sh "kill ${backgroundPid}" - if (archiveLog) { +String startLogCatCollector() { + // Cancel build quickly if no device is available. The lock acquired already should + // ensure we have access to a device. If not, it is most likely a more severe problem. + timeout(time: 1, unit: 'MINUTES') { + // Need ADB as root to clear all buffers: https://stackoverflow.com/a/47686978/1389357 + sh 'adb devices' + sh """adb root + adb logcat -b all -c + adb logcat -v time > 'logcat.txt' & + echo \$! > pid + """ + return readFile("pid").trim() + } +} + +def stopLogCatCollector(String backgroundPid) { + // The pid might not be available if the build was terminated early or stopped due to + // a build error. + if (backgroundPid != null) { + sh "kill ${backgroundPid}" zip([ 'zipFile': 'logcat.zip', 'archive': true, 'glob' : 'logcat.txt' ]) + sh 'rm logcat.txt' } - sh 'rm logcat.txt ' } -def sendMetrics(String metric, String value) { - withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: '5b8ad2d9-61a4-43b5-b4df-b8ff6b1f16fa', passwordVariable: 'influx_pass', usernameVariable: 'influx_user']]) { - sh "curl -i -XPOST 'https://greatscott-pinheads-70.c.influxdb.com:8086/write?db=realm' --data-binary '${metric} value=${value}i' --user '${env.influx_user}:${env.influx_pass}'" - } +def archiveServerLogs(String mongoDbRealmContainerId, String commandServerContainerId) { + sh "docker logs ${commandServerContainerId} > ./command-server.log" + zip([ + 'zipFile': 'command-server-log.zip', + 'archive': true, + 'glob' : 'command-server.log' + ]) + sh 'rm command-server.log' + + sh "docker cp ${mongoDbRealmContainerId}:/var/log/stitch.log ./stitch.log" + zip([ + 'zipFile': 'stitchlog.zip', + 'archive': true, + 'glob' : 'stitch.log' + ]) + sh 'rm stitch.log' + + sh "docker cp ${mongoDbRealmContainerId}:/var/log/mongodb.log ./mongodb.log" + zip([ + 'zipFile': 'mongodb.zip', + 'archive': true, + 'glob' : 'mongodb.log' + ]) + sh 'rm mongodb.log' } -def sendTaggedMetric(String metric, String value, String tagName, String tagValue) { +def sendMetrics(String metricName, String metricValue, Map tags) { + def tagsString = getTagsString(tags) withCredentials([[$class: 'UsernamePasswordMultiBinding', credentialsId: '5b8ad2d9-61a4-43b5-b4df-b8ff6b1f16fa', passwordVariable: 'influx_pass', usernameVariable: 'influx_user']]) { - sh "curl -i -XPOST 'https://greatscott-pinheads-70.c.influxdb.com:8086/write?db=realm' --data-binary '${metric},${tagName}=${tagValue} value=${value}i' --user '${env.influx_user}:${env.influx_pass}'" + sh "curl -i -XPOST 'https://influxdb.realmlab.net/write?db=realm' --data-binary '${metricName},${tagsString} value=${metricValue}i' --user '${env.influx_user}:${env.influx_pass}'" } } +@NonCPS +def getTagsString(Map tags) { + return tags.collect { k,v -> "$k=$v" }.join(',') +} + def storeJunitResults(String path) { step([ $class: 'JUnitResultArchiver', + allowEmptyResults: true, testResults: path ]) } def collectAarMetrics() { - sh '''set -xe - cd realm/realm-library/build/outputs/aar - unzip realm-android-library-release.aar -d unzipped - find $ANDROID_HOME -name dx | sort -r | head -n 1 > dx - $(cat dx) --dex --output=temp.dex unzipped/classes.jar - cat temp.dex | head -c 92 | tail -c 4 | hexdump -e '1/4 "%d"' > methods - ''' - - sendMetrics('methods', readFile('realm/realm-library/build/outputs/aar/methods')) - - def aarFile = findFiles(glob: 'realm/realm-library/build/outputs/aar/realm-android-library-release.aar')[0] - sendMetrics('aar_size', aarFile.length as String) - - def soFiles = findFiles(glob: 'realm/realm-library/build/outputs/aar/unzipped/jni/*/librealm-jni.so') - for (int i = 0; i < soFiles.length; i++) { - def abiName = soFiles[i].path.tokenize('/')[-2] - def libSize = soFiles[i].length as String - sendTaggedMetric('abi_size', libSize, 'type', abiName) + def flavors = ['base', 'objectServer'] + for (def i = 0; i < flavors.size(); i++) { + def flavor = flavors[i] + sh """set -xe + cd realm/realm-library/build/outputs/aar + unzip realm-android-library-${flavor}-release.aar -d unzipped${flavor} + find \$ANDROID_HOME -name d8 | sort -r | head -n 1 > d8 + \$(cat d8) --release --output ./unzipped${flavor} unzipped${flavor}/classes.jar + cat ./unzipped${flavor}/temp${flavor}.dex | head -c 92 | tail -c 4 | hexdump -e '1/4 \"%d\"' > methods${flavor} + """ + + def methods = readFile("realm/realm-library/build/outputs/aar/methods${flavor}") + sendMetrics('methods', methods, ['flavor':flavor]) + + def aarFile = findFiles(glob: "realm/realm-library/build/outputs/aar/realm-android-library-${flavor}-release.aar")[0] + sendMetrics('aar_size', aarFile.length as String, ['flavor':flavor]) + + def soFiles = findFiles(glob: "realm/realm-library/build/outputs/aar/unzipped${flavor}/jni/*/librealm-jni.so") + for (def j = 0; j < soFiles.size(); j++) { + def soFile = soFiles[j] + def abiName = soFile.path.tokenize('/')[-2] + def libSize = soFile.length as String + sendMetrics('abi_size', libSize, ['flavor':flavor, 'type':abiName]) + } } } @@ -162,3 +494,16 @@ def gradle(String commands) { def gradle(String relativePath, String commands) { sh "cd ${relativePath} && chmod +x gradlew && ./gradlew ${commands} --stacktrace" } + +def readGitTag() { + def command = 'git describe --exact-match --tags HEAD' + def returnStatus = sh(returnStatus: true, script: command) + if (returnStatus != 0) { + return null + } + return sh(returnStdout: true, script: command).trim() +} + +def runCommand(String command){ + return sh(script: command, returnStdout: true).trim() +} diff --git a/LICENSE b/LICENSE index 62e2f9e364..66a27ec5ff 100644 --- a/LICENSE +++ b/LICENSE @@ -1,11 +1,3 @@ -TABLE OF CONTENTS - -1. Apache License version 2.0 -2. Realm Components -3. Export Compliance - -------------------------------------------------------------------------------- - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -183,87 +175,3 @@ TABLE OF CONTENTS 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. - - -REALM COMPONENTS - -This software contains components with separate copyright and license terms. -Your use of these components is subject to the terms and conditions of the -following licenses. - -For the Realm Core component - - Realm Core Binary License - - Copyright (c) 2011-2015 Realm Inc All rights reserved - - Redistribution and use in binary form, with or without modification, is - permitted provided that the following conditions are met: - - 1. You agree not to attempt to decompile, disassemble, reverse engineer or - otherwise discover the source code from which the binary code was derived. - You may, however, access and obtain a separate license for most of the - source code from which this Software was created, at - http://realm.io/pricing/. - - 2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - - 3. Neither the name of the copyright holder nor the names of its - contributors may be used to endorse or promote products derived from this - software without specific prior written permission. - - THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE - LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - POSSIBILITY OF SUCH DAMAGE. - -EXPORT COMPLIANCE - -You understand that the Software may contain cryptographic functions that may be -subject to export restrictions, and you represent and warrant that you are not -located in a country that is subject to United States export restriction or embargo, -including Cuba, Iran, North Korea, Sudan, Syria or the Crimea region, and that you -are not on the Department of Commerce list of Denied Persons, Unverified Parties, -or affiliated with a Restricted Entity. - -You agree to comply with all export, re-export and import restrictions and -regulations of the Department of Commerce or other agency or authority of the -United States or other applicable countries. You also agree not to transfer, or -authorize the transfer of, directly or indirectly, the Software to any prohibited -country, including Cuba, Iran, North Korea, Sudan, Syria or the Crimea region, -or to any person or organization on or affiliated with the Department of -Commerce lists of Denied Persons, Unverified Parties or Restricted Entities, or -otherwise in violation of any such restrictions or regulations. diff --git a/README.md b/README.md index 9f0eeec413..b3d6ff0bb5 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,77 @@ -![Realm](logo.png) +> [!WARNING] +> We announced the deprecation of Atlas Device Sync + Realm SDKs in September 2024. For more information please see: +> - [SDK Deprecation](https://www.mongodb.com/docs/atlas/device-sdks/deprecation/) +> - [Device Sync Deprecation](https://www.mongodb.com/docs/atlas/app-services/sync/device-sync-deprecation/) +> + + + + realm + + +[![Maven Central](https://img.shields.io/maven-central/v/io.realm/realm-gradle-plugin?colorB=4dc427&label=Maven%20Central)](https://search.maven.org/artifact/io.realm/realm-gradle-plugin) +[![License](https://img.shields.io/badge/License-Apache-blue.svg)](https://github.com/realm/realm-java/blob/master/LICENSE) Realm is a mobile database that runs directly inside phones, tablets or wearables. This repository holds the source code for the Java version of Realm, which currently runs only on Android. +## Realm Kotlin + +The [Realm Kotlin SDK](https://github.com/realm/realm-kotlin) is now GA and can be used for both Android and Kotlin Multiplatform. While we are still adding features, please consider using Realm Kotlin for any new project, and let us know if you miss anything there! + ## Features -* **Mobile-first:** Realm is the first database built from the ground up to run directly inside phones, tablets and wearables. -* **Simple:** Data is directly exposed as objects and queryable by code, removing the need for ORM's riddled with performance & maintenance issues. Plus, we've worked hard to [keep our API down to very few classes](https://realm.io/docs/java/): most of our users pick it up intuitively, getting simple apps up & running in minutes. +* **Mobile-first:** Realm is the first database built from the ground up to run directly inside phones, tablets, and wearables. +* **Simple:** Data is directly exposed as objects and queryable by code, removing the need for ORM's riddled with performance & maintenance issues. Plus, we've worked hard to [keep our API down to very few classes](https://www.mongodb.com/docs/atlas/device-sdks/sdk/java/): most of our users pick it up intuitively, getting simple apps up & running in minutes. * **Modern:** Realm supports easy thread-safety, relationships & encryption. -* **Fast:** Realm is faster than even raw SQLite on common operations, while maintaining an extremely rich feature set. +* **Fast:** Realm is faster than even raw SQLite on common operations while maintaining an extremely rich feature set. +* **[Device Sync](https://www.mongodb.com/atlas/app-services/device-sync)**: Makes it simple to keep data in sync across users, devices, and your backend in real-time. Get started for free with [a template application](https://github.com/mongodb/template-app-react-native-todo) and [create the cloud backend](http://mongodb.com/realm/register?utm_medium=github_atlas_CTA&utm_source=realm_js_github). ## Getting Started -Please see the [detailed instructions in our docs](https://realm.io/docs/java/#installation) to add Realm to your project. +Please see the [Quick Start](docs/guides/quick-start-local.md) to add Realm to your project. ## Documentation -Documentation for Realm can be found at [realm.io/docs/java](https://realm.io/docs/java). -The API reference is located at [realm.io/docs/java/api](https://realm.io/docs/java/api). +Documentation for Realm can be found in the [docs/](docs/README.md) directory. + +The Javadoc and Kotlin Extensions API Reference docs can be generated +from source. ## Getting Help -- **Need help with your code?**: Look for previous questions on the [#realm tag](https://stackoverflow.com/questions/tagged/realm?sort=newest) — or [ask a new question](http://stackoverflow.com/questions/ask?tags=realm). We activtely monitor & answer questions on SO! -- **Have a bug to report?** [Open an issue](https://github.com/realm/realm-java/issues/new). If possible, include the version of Realm, a full log, the Realm file, and a project that shows the issue. -- **Have a feature request?** [Open an issue](https://github.com/realm/realm-java/issues/new). Tell us what the feature should do, and why you want the feature. -- Sign up for our [**Community Newsletter**](http://eepurl.com/VEKCn) to get regular tips, learn about other use-cases and get alerted of blogposts and tutorials about Realm. +- **Got a question?**: Look for previous questions on the [#realm tag](https://stackoverflow.com/questions/tagged/realm?sort=newest) — or [ask a new question](http://stackoverflow.com/questions/ask?tags=realm). We actively monitor & answer questions on StackOverflow! You can also check out our [Community Forum](https://developer.mongodb.com/community/forums/tags/c/realm/9/realm-sdk) where general questions about how to do something can be discussed. +- **Think you found a bug?** [Open an issue](https://github.com/realm/realm-java/issues/new?template=bug_report.md). If possible, include the version of Realm, a full log, the Realm file, and a project that shows the issue. +- **Have a feature request?** [Open an issue](https://github.com/realm/realm-java/issues/new?template=feature_request.md). Tell us what the feature should do, and why you want the feature. ## Using Snapshots -If you want to test recent bugfixes or features that have not been packaged in an official release yet, you can use a **-SNAPSHOT** release of the current development version of Realm via Gradle, available on [OJO](http://oss.jfrog.org/oss-snapshot-local/io/realm/realm-android/) +If you want to test recent bugfixes or features that have not been packaged in an official release yet, you can use a **-SNAPSHOT** release of the current development version of Realm via Gradle, available on [Sonatype OSS](https://oss.sonatype.org/#nexus-search;quick~realm-gradle-plugin) -```gradle + +``` buildscript { repositories { + mavenCentral() + google() maven { - url 'http://oss.jfrog.org/artifactory/oss-snapshot-local' + url 'https://oss.sonatype.org/content/repositories/snapshots/' } + jcenter() } dependencies { classpath "io.realm:realm-gradle-plugin:-SNAPSHOT" } } -repositories { - maven { - url 'http://oss.jfrog.org/artifactory/oss-snapshot-local' +allprojects { + repositories { + mavenCentral() + google() + maven { + url 'https://oss.sonatype.org/content/repositories/snapshots/' + } + jcenter() } } ``` @@ -57,50 +84,45 @@ In case you don't want to use the precompiled version, you can build Realm yours ### Prerequisites - * Make sure `make` is available in your `$PATH`. - * Download the [**JDK 7**](http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html) or [**JDK 8**](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) from Oracle and install it. - * Download & install the Android SDK **Build-Tools 24.0.0**, **Android N (API 24)** (for example through Android Studio’s **Android SDK Manager**). - * Download the **Android NDK (= r10e)** for [OS X](http://dl.google.com/android/ndk/android-ndk-r10e-darwin-x86_64.bin) or [Linux](http://dl.google.com/android/ndk/android-ndk-r10e-linux-x86_64.bin). - * Install CMake from SDK manager in Android Studio ("SDK Tools" -> "CMake"). - * Or you can use [Hombrew-versions](https://github.com/Homebrew/homebrew-versions) to install Android NDK for Mac: - - ``` - brew tap homebrew/versions - brew install android-ndk-r10e - ``` + * Download the [**JDK 8**](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) from Oracle and install it. + * The latest stable version of Android Studio. Currently [4.1.1](https://developer.android.com/studio/). + * Download & install the Android SDK **Build-Tools 29.0.3**, **Android Pie (API 29)** (for example through Android Studio’s **Android SDK Manager**). + * Install CMake version 3.18.4 and build Ninja. + * Install the NDK (Side-by-side) **21.0.6113669** from the SDK Manager in Android Studio. Remember to check `☑ Show package details` in the manager to display all available versions. - * Add two environment variables to your profile: + * Add the Android home environment variable to your profile: ``` export ANDROID_HOME=~/Library/Android/sdk - export ANDROID_NDK_HOME=/usr/local/Cellar/android-ndk-r10e/r10e - ``` - - * If you want to build with Android Studio, `ndk.dir` has to be defined in the `realm/local.properties` as well. - - ``` - ndk.dir=/usr/local/Cellar/android-ndk-r10e/r10e ``` - * If you are using OS X, you'd be better to add following lines to `~/.profile` (or `~/.zprofile` if the login shell is `zsh`) in order for Android Studio to see those environment variables. + * If you are launching Android Studio from the macOS Finder, you should also run the following command: ``` launchctl setenv ANDROID_HOME "$ANDROID_HOME" - launchctl setenv ANDROID_NDK_HOME "$ANDROID_NDK_HOME" ``` - * And if you'd like to specify the location to store the archives of Realm's core, set `REALM_CORE_DOWNLOAD_DIR` environment variable. It enables you to keep core's archive when executing `git clean -xfd`. + * If you'd like to specify the location in which to store the archives of Realm Core, define the `REALM_CORE_DOWNLOAD_DIR` environment variable. It enables caching core release artifacts. ``` export REALM_CORE_DOWNLOAD_DIR=~/.realmCore ``` - OS X users should also add following line to `~/.profile` (or `~/.zprofile` if the login shell is `zsh`) in order for Android Studio to see this environment variable.. + macOS users must also run the following command for Android Studio to see this environment variable. ``` launchctl setenv REALM_CORE_DOWNLOAD_DIR "$REALM_CORE_DOWNLOAD_DIR" ``` +It would be a good idea to add all of the symbol definitions (and their accompanying `launchctl` commands, if you are using macOS) to your `~/.profile` (or `~/.zprofile` if the login shell is `zsh`) + + * If you develop Realm Java with Android Studio, we recommend you to exclude some directories from indexing target by executing following steps on Android Studio. It really speeds up indexing phase after the build. + + - Under `/realm/realm-library/`, select `build`, `.cxx` and `distribution` folders in `Project` view. + - Press `Command + Shift + A` to open `Find action` dialog. If you are not using default keymap nor using macOS, you can find your shortcut key in `Keymap` preference by searching `Find action`. + - Search `Excluded` (not `Exclude`) action and select it. Selected folder icons should become orange (in default theme). + - Restart Android Studio. + ### Download sources You can download the source code of Realm Java by using git. Since realm-java has git submodules, use `--recursive` when cloning the repository. @@ -117,7 +139,7 @@ git clone https://github.com/realm/realm-java.git --recursive ### Build -Once you have completed all the pre-requisites building Realm is done with a simple command +Once you have completed all the pre-requisites building Realm is done with a simple command. ``` ./gradlew assemble @@ -130,59 +152,156 @@ That command will generate: * a jar file for the annotations * a jar file for the annotations processor +The full build may take an hour or more, to complete. + +### Building from source + +It is possible to build Realm Java with the submodule version of Realm Core. This is done by providing the following parameter when building: `-PbuildCore=true`. + +``` +./gradlew assembleBase -PbuildCore=true +``` + +You can turn off interprocedural optimizations with the following parameter: `-PenableLTO=false`. + +``` +./gradlew assembleBase -PenableLTO=false` +``` + +Note: Building the `Base` variant would always build realm-core. + +Note: Interprocedural optimizations are enabled by default. + +Note: If you want to build from source inside Android Studio, you need to update the Gradle parameters by going into the Realm projects settings `Settings > Build, Execution, Deployment > Compiler > Command-line options` and add `-PbuildCore=true` or `-PenableLTO=false` to it. Alternatively you can add it into your `gradle.properties`: + +``` +buildCore=true +enableLTO=false +``` + +Note: If building on OSX you might like to prevent Gatekeeper to block all NDK executables by disabling it: `sudo spctl --master-disable`. Remember to enable it afterwards: `sudo spctl --master-enable` + ### Other Commands * `./gradlew tasks` will show all the available tasks * `./gradlew javadoc` will generate the Javadocs * `./gradlew monkeyExamples` will run the monkey tests on all the examples * `./gradlew installRealmJava` will install the Realm library and plugin to mavenLocal() - * `./gradlew clean -PdontCleanJniFiles` will remove all generated files except for JNI related files. This saves recompilation time a lot. - * `./gradlew connectedUnitTests -PbuildTargetABIs=$(adb shell getprop ro.product.cpu.abi)` will build JNI files only for the ABI which corresponds to the connected device. + * `./gradlew clean -PdontCleanJniFiles` will remove all generated files except for JNI related files. This reduces recompilation time a lot. + * `./gradlew connectedUnitTests -PbuildTargetABIs=$(adb shell getprop ro.product.cpu.abi)` will build JNI files only for the ABI which corresponds to the connected device. These tests require a running Object Server (see below) + +Generating the Javadoc using the command above may generate warnings. The Javadoc is generated despite the warnings. + + +### Upgrading Gradle Wrappers + + All gradle projects in this repository have `wrapper` task to generate Gradle Wrappers. Those tasks refer to `gradle` property defined in `/dependencies.list` to determine Gradle Version of generating wrappers. +We have a script `./tools/update_gradle_wrapper.sh` to automate these steps. When you update Gradle Wrappers, please obey the following steps. -Generating the Javadoc using the command above will report a large number of warnings. The Javadoc is generated, and we will fix the issue in the near future. + 1. Edit `gradle` property in defined in `/dependencies.list` to new Gradle Wrapper version. + 2. Execute `/tools/update_gradle_wrapper.sh`. ### Gotchas -The repository is organized in six Gradle projects: +The repository is organized into six Gradle projects: * `realm`: it contains the actual library (including the JNI layer) and the annotations processor. * `realm-annotations`: it contains the annotations defined by Realm. * `realm-transformer`: it contains the bytecode transformer. * `gradle-plugin`: it contains the Gradle plugin. * `examples`: it contains the example projects. This project directly depends on `gradle-plugin` which adds a dependency to the artifacts produced by `realm`. - * The root folder is another Gradle project and all it does is orchestrating the other jobs + * The root folder is another Gradle project. All it does is orchestrate the other jobs. This means that `./gradlew clean` and `./gradlew cleanExamples` will fail if `assembleExamples` has not been executed first. Note that IntelliJ [does not support multiple projects in the same window](https://youtrack.jetbrains.com/issue/IDEABKL-6118#) -so each sub-project must be opened in its own window. +so each of the six Gradle projects must be imported as a separate IntelliJ project. + +Since the repository contains several completely independent Gradle projects, several independent builds are run to assemble it. +Seeing a line like: `:realm:realm-library:compileBaseDebugAndroidTestSources UP-TO-DATE` in the build log does *not* imply +that you can run `./gradlew :realm:realm-library:compileBaseDebugAndroidTestSources`. ## Examples -The `./examples` folder contain a number of example projects showing how Realm can be used. If this is the first time you checkout or pull a new version of this repository to try the examples, you must call `./gradlew installRealmJava` from the top-level directory first. Otherwise the examples will not compile as they depend on all Realm artifacts being installed in `mavenLocal()`. +The `./examples` folder contains many example projects showing how Realm can be used. If this is the first time you checkout or pull a new version of this repository to try the examples, you must call `./gradlew installRealmJava` from the top-level directory first. Otherwise, the examples will not compile as they depend on all Realm artifacts being installed in `mavenLocal()`. + +## Running Tests on a Device + +To run these tests, you must have a device connected to the build computer, and the `adb` command must be in your `PATH` + +1. Connect an Android device and verify that the command `adb devices` shows a connected device: + + ```sh + adb devices + List of devices attached + 004c03eb5615429f device + ``` + +2. Run instrumentation tests: + + ```sh + cd realm + ./gradlew connectedBaseDebugAndroidTest + ``` + +These tests may take as much as half an hour to complete. + +## Running Tests Using The Realm Object Server + +Tests in `realm/realm-library/src/syncIntegrationTest` require a running testing server to work. +A docker image can be built from `tools/sync_test_server/Dockerfile` to run the test server. +`tools/sync_test_server/start_server.sh` will build the docker image automatically. + +To run a testing server locally: + +1. Install [docker](https://www.docker.com/products/overview) and run it. + +2. Run `tools/sync_test_server/start_server.sh`: + + ```sh + cd tools/sync_test_server + ./start_server.sh + ``` + + This command will not complete until the server has stopped. + +3. Run instrumentation tests + + In a new terminal window, run: -Standalone examples can be [downloaded from website](https://realm.io/docs/java/latest/#getting-started). + ```sh + cd realm + ./gradlew connectedObjectServerDebugAndroidTest + ``` + +Note that if using VirtualBox (Genymotion), the network needs to be bridged for the tests to work. +This is done in `VirtualBox > Network`. Set "Adapter 2" to "Bridged Adapter". + +These tests may take as much as half an hour to complete. ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) for more details! -This project adheres to the [Contributor Covenant Code of Conduct](https://realm.io/conduct). +This project adheres to the [MongoDB Code of Conduct](https://www.mongodb.com/community-code-of-conduct). By participating, you are expected to uphold this code. Please report -unacceptable behavior to [info@realm.io](mailto:info@realm.io). +unacceptable behavior to [community-conduct@mongodb.com](mailto:community-conduct@mongodb.com). + +The directory `realm/config/studio` contains lint and style files recommended for project code. +Import them from Android Studio with Android Studio > Preferences... > Code Style > Manage... > Import, +or Android Studio > Preferences... > Inspections > Manage... > Import. Once imported select the +style/lint in the drop-down to the left of the Manage... button. ## License Realm Java is published under the Apache 2.0 license. -The underlying core is available under the [Realm Core Binary License](LICENSE#L210-L243) while we [work to open-source it under the Apache 2.0 license](https://realm.io/docs/java/#faq). -**This product is not being made available to any person located in Cuba, Iran, -North Korea, Sudan, Syria or the Crimea region, or to any other person that is -not eligible to receive the product under U.S. law.** +Realm Core is also published under the Apache 2.0 license and is available +[here](https://github.com/realm/realm-core). ## Feedback -**_If you use Realm and are happy with it, all we ask is that you please consider sending out a tweet mentioning [@realm](http://twitter.com/realm), announce your app on [our mailing-list](https://groups.google.com/forum/#!forum/realm-java), or email [help@realm.io](mailto:help@realm.io) to let us know about it!_** +**_If you use Realm and are happy with it, all we ask is that you, please consider sending out a tweet mentioning [@realm](http://twitter.com/realm) to share your thoughts!_** **_And if you don't like it, please let us know what you would like improved, so we can fix it!_** -![analytics](https://ga-beacon.appspot.com/UA-50247013-2/realm-java/README?pixel) + diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000000..0d94e10195 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,19 @@ +# Support + +The Realm team is here to help you with your Realm-related issues! + +## Documentation + +Before asking questions, please familiarize yourself with our [Java](https://www.mongodb.com/docs/atlas/device-sdks/sdk/java/) documentation. We also have a number of [Troubleshooting Notes](https://www.mongodb.com/docs/atlas/device-sdks/sdk/java/troubleshooting/) which cover various topics that may be of interest. + +## Stack Overflow + +If you have questions about configuring or using Realm you can ask them on Stack Overflow. We continually monitor the [`realm` tag](https://stackoverflow.com/tags/realm). Please also tag your question with `java`, `android`, or other tags as appropriate. + +When asking questions on Stack Overflow, please keep in mind Stack Overflow's [question guidelines](https://stackoverflow.com/help/how-to-ask), and please use their search functionality to see if your question has been asked before. + +## GitHub Issues + +If you are running into issues with Realm, including potential bugs or feature requests, we encourage you to file an issue on our [GitHub issue tracker](https://github.com/realm/realm-java/issues). Please check out our [Contribution Guidelines](CONTRIBUTING.md) for information on how to properly file an issue. + +We greatly appreciate demonstration projects that we can run for ourselves in order to see issues or potential bugs; we prioritize clearly-written tickets that include reproduction cases. You may attach these to the ticket; let us know if you need to share them confidentially, and we’ll provide instructions on how to do so. diff --git a/build.gradle b/build.gradle index 215e8e8d6b..6575c37544 100644 --- a/build.gradle +++ b/build.gradle @@ -1,20 +1,67 @@ buildscript { + def properties = new Properties() + properties.load(new FileInputStream("${projectDir}/dependencies.list")) + repositories { jcenter() - } - dependencies { - classpath 'ch.netzwerg:gradle-release-plugin:1.2.0' + maven { + url "https://plugins.gradle.org/m2/" + } } } -apply plugin: 'ch.netzwerg.release' +def currentVersion = file("${projectDir}/version.txt").text.trim() -def currentVersion = file("${projectDir}/version.txt").text.trim(); +// Find property in either System environment or Gradle properties. +// If set in both places, Gradle properties win. +def getPropertyValueOrThrow(String propertyName) { + def value = System.getenv(propertyName) + if (project.hasProperty(propertyName)) { + value = project.getProperty(propertyName) + } + if (value == null || value.trim().isEmpty()) { + throw new GradleException("Could not find '$propertyName'. " + + "Most be provided as either environment variable or " + + "a Gradle property.") + } + return value +} -def props = new Properties() -props.load(new FileInputStream("${rootDir}/realm.properties")) -props.each { key, val -> - project.ext.set(key, val) +// Shared configuration that copies relevant properties from the root level and parse them on to +// child projects. +def copyProperties = { + if (project.hasProperty('buildTargetABIs')) { + // Valid options: armeabi-v7a, arm64-v8a, x86, x86_64 + startParameter.projectProperties += [buildTargetABIs: project.getProperty('buildTargetABIs')] + } + if (project.hasProperty('coreSourcePath')) { + def absolutePath = file(project.getProperty('coreSourcePath')).absolutePath + startParameter.projectProperties += [coreSourcePath: absolutePath] + } + if (project.hasProperty('s3cfg')) { + startParameter.projectProperties += [s3cfg: project.getProperty('s3cfg')] + } + if (project.hasProperty('enableLTO')) { + startParameter.projectProperties += [enableLTO: project.getProperty('enableLTO')] + } + if (project.hasProperty('buildCore')) { + startParameter.projectProperties += [buildCore: project.getProperty('buildCore')] + } + if (project.hasProperty('signBuild')) { + startParameter.projectProperties += [signBuild: project.getProperty('signBuild')] + } + if (project.hasProperty('signPassword')) { + startParameter.projectProperties += [signPassword: project.getProperty('signPassword')] + } + if (project.hasProperty('signSecretRingFile')) { + startParameter.projectProperties += [signSecretRingFile: project.getProperty('signSecretRingFile')] + } + if (project.hasProperty('ossrhUsername')) { + startParameter.projectProperties += [ossrhUsername: project.getProperty('ossrhUsername')] + } + if (project.hasProperty('ossrhPassword')) { + startParameter.projectProperties += [ossrhPassword: project.getProperty('ossrhPassword')] + } } task assembleAnnotations(type:GradleBuild) { @@ -28,7 +75,7 @@ task installAnnotations(type:GradleBuild) { group = 'Install' description = 'Install the jar realm-annotations into mavenLocal()' buildFile = file('realm-annotations/build.gradle') - tasks = ['install'] + tasks = ['publishToMavenLocal'] } task assembleTransformer(type:GradleBuild) { @@ -44,7 +91,14 @@ task installTransformer(type:GradleBuild) { description = 'Install the jar realm-transformer into mavenLocal()' dependsOn installAnnotations buildFile = file('realm-transformer/build.gradle') - tasks = ['install'] + tasks = ['publishToMavenLocal'] +} + +task installBuildTransformer(type:GradleBuild) { + group = 'Install' + description = 'Install the jar realm-library-build-transformer into mavenLocal()' + buildFile = file('library-build-transformer/build.gradle') + tasks = ['publishToMavenLocal'] } task assembleRealm(type:GradleBuild) { @@ -52,11 +106,10 @@ task assembleRealm(type:GradleBuild) { description = 'Assemble the Realm project' dependsOn installAnnotations dependsOn installTransformer + dependsOn installBuildTransformer buildFile = file('realm/build.gradle') tasks = ['assemble', 'javadocJar', 'sourcesJar'] - if (project.hasProperty('buildTargetABIs')) { - startParameter.projectProperties += [buildTargetABIs: project.getProperty('buildTargetABIs')] - } + configure copyProperties } task checkExamples(type:GradleBuild) { @@ -64,9 +117,7 @@ task checkExamples(type:GradleBuild) { description = 'Run the JVM tests and checks the examples' buildFile = file('examples/build.gradle') tasks = ['check'] - if (project.hasProperty('buildTargetABIs')) { - startParameter.projectProperties += [buildTargetABIs: project.getProperty('buildTargetABIs')] - } + configure copyProperties } task checkRealm(type:GradleBuild) { @@ -74,9 +125,7 @@ task checkRealm(type:GradleBuild) { description = 'Run the JVM tests and checks Realm project' buildFile = file('realm/build.gradle') tasks = ['check'] - if (project.hasProperty('buildTargetABIs')) { - startParameter.projectProperties += [buildTargetABIs: project.getProperty('buildTargetABIs')] - } + configure copyProperties } task check { @@ -86,26 +135,50 @@ task check { dependsOn checkExamples } +task assembleUnitTests(type:GradleBuild) { + group = 'Build' + description = 'Assemble Android unit tests of the Realm project' + dependsOn installTransformer + buildFile = file('realm/build.gradle') + tasks = ['assembleAndroidTest'] + configure copyProperties +} + task connectedUnitTests(type:GradleBuild) { group = 'Test' description = 'Run the Android unit tests of the Realm project' dependsOn installTransformer buildFile = file('realm/build.gradle') - tasks = ['connectedUnitTests'] - if (project.hasProperty('buildTargetABIs')) { - startParameter.projectProperties += [buildTargetABIs: project.getProperty('buildTargetABIs')] - } + tasks = ['connectedAndroidTest'] + configure copyProperties +} + +task assembleBenchmarks(type:GradleBuild) { + group = 'Build' + description = 'Assemble benchmark tests for the library ' + dependsOn installTransformer + buildFile = file('library-benchmarks/build.gradle') + tasks = ['assembleAndroidTest'] + configure copyProperties +} + +task connectedBenchmarks(type:GradleBuild) { + group = 'Test' + description = 'Run all the benchmark tests for the library ' + dependsOn installTransformer + buildFile = file('library-benchmarks/build.gradle') + tasks = ['connectedAndroidTest'] + configure copyProperties } task installRealm(type:GradleBuild) { group = 'Install' description = 'Install the artifacts of Realm libraries into mavenLocal()' dependsOn installTransformer + dependsOn installBuildTransformer buildFile = file('realm/build.gradle') - tasks = ['install'] - if (project.hasProperty('buildTargetABIs')) { - startParameter.projectProperties += [buildTargetABIs: project.getProperty('buildTargetABIs')] - } + tasks = ['publishToMavenLocal'] + configure copyProperties } task assembleGradlePlugin(type:GradleBuild) { @@ -123,7 +196,7 @@ task installGradlePlugin(type:GradleBuild) { dependsOn installRealm dependsOn installTransformer buildFile = file('gradle-plugin/build.gradle') - tasks = ['install'] + tasks = ['publishToMavenLocal'] } task installRealmJava(type:Task) { @@ -156,8 +229,34 @@ task javadoc(type:GradleBuild) { group = 'Docs' buildFile = file('realm/build.gradle') tasks = ['javadocJar'] - if (project.hasProperty('buildTargetABIs')) { - startParameter.projectProperties += [buildTargetABIs: project.getProperty('buildTargetABIs')] + configure copyProperties +} + +task uploadJavadoc { + group = 'Release' + description = 'Upload Java and Kotlin docs to S3' + dependsOn javadoc + + doLast { + def awsAccessKey = getPropertyValueOrThrow("SDK_DOCS_AWS_ACCESS_KEY") + def awsSecretKey = getPropertyValueOrThrow("SDK_DOCS_AWS_SECRET_KEY") + + // Upload two copies, to 'latest' and a versioned folder for posterity. + // Symlinks would have been safer and faster, but this is not supported by S3. + [ "${currentVersion}", "latest"].forEach { version -> + exec { + commandLine 's3cmd', 'put', '--recursive', '--acl-public', "--access_key=${awsAccessKey}", "--secret_key=${awsSecretKey}", 'realm/realm-library/build/docs/javadoc/', "s3://realm-sdks/realm-sdks/java/${version}/" + } + // The stylesheet is being uploaded with the wrong Content-Type header, which causes the stylesheet to not be applied in some browsers. + // So we need to modify the stylesheet after it has been uploaded. + exec { + commandLine 's3cmd', 'modify', "--access_key=${awsAccessKey}", "--secret_key=${awsSecretKey}", "--debug", '--add-header=Content-Type: text/css', "s3://realm-sdks/realm-sdks/java/${version}/stylesheet.css" + } + // Upload Kotlin extension docs to a subdirectory of the Javadoc. This should not conflict with the Javadoc folder layout. + exec { + commandLine 's3cmd', 'put', '--recursive', '--acl-public', "--access_key=${awsAccessKey}", "--secret_key=${awsSecretKey}", 'realm/kotlin-extensions/build/dokka/', "s3://realm-sdks/realm-sdks/java/${version}/kotlin-extensions/" + } + } } } @@ -166,9 +265,7 @@ task sourcesJar(type:GradleBuild) { group = 'Docs' buildFile = file('realm/build.gradle') tasks = ['sourcesJar'] - if (project.hasProperty('buildTargetABIs')) { - startParameter.projectProperties += [buildTargetABIs: project.getProperty('buildTargetABIs')] - } + configure copyProperties } task assemble { @@ -177,37 +274,8 @@ task assemble { dependsOn assembleExamples } -task distributionPackage(type:Zip) { - description = 'Generate the distribution package' - dependsOn assembleRealm - dependsOn javadoc - - group = 'Artifact' - archiveName = "realm-java-${currentVersion}.zip" - destinationDir = file("${buildDir}/outputs/distribution") - - from('changelog.txt') - from('LICENSE') - from('version.txt') - from('realm/realm-library/build/libs') { - include 'realm-android-${currentVersion}-javadoc.jar' - into 'docs' - } - from('realm/realm-library/build/docs') { - include '**/*' - into 'docs' - } - from('examples') { - exclude 'local.properties' - exclude '**/.gradle' - exclude '**/build' - into 'examples' - } -} - task distributionJniUnstrippedPackage(type:Zip) { description = 'Generate native libs package with debug symbols' - dependsOn assembleRealm group = 'Artifact' archiveName = "realm-java-jni-libs-unstripped-${currentVersion}.zip" @@ -223,12 +291,7 @@ task cleanRealm(type:GradleBuild) { group = 'Clean' buildFile = file('realm/build.gradle') tasks = ['clean'] - if (project.hasProperty('buildTargetABIs')) { - startParameter.projectProperties += [buildTargetABIs: project.getProperty('buildTargetABIs')] - } - if (project.hasProperty('dontCleanJniFiles')) { - startParameter.projectProperties += [dontCleanJniFiles: project.getProperty('dontCleanJniFiles')] - } + configure copyProperties } task cleanGradlePlugin(type:GradleBuild) { @@ -260,143 +323,113 @@ task clean { dependsOn cleanLocalMavenRepos } -task uploadDistributionPackage(type: Exec) { - group = 'Release' - description = 'Upload the distribution package to S3' - dependsOn distributionPackage - dependsOn distributionJniUnstrippedPackage - commandLine 's3cmd', 'put', "${buildDir}/outputs/distribution/realm-java-${currentVersion}.zip", 's3://static.realm.io/downloads/java/' - commandLine 's3cmd', 'put', "${buildDir}/outputs/distribution/realm-java-jni-libs-unstripped-${currentVersion}.zip", 's3://static.realm.io/downloads/java/' -} +task manualClean { + description = 'Clean build files without using clean tasks defined in sub projects' + group = 'Clean' -task createEmptyFile(type: Exec) { - group = 'Release' - description = 'Create an empty file that will serve as a link on S3' - dependsOn uploadDistributionPackage - commandLine 'touch', 'latest' + doLast { + // clean 'build' directories + exec { + workingDir "${rootDir}" + commandLine 'find', '.', '-type', 'd', '-name', 'build', '-print', '-exec', 'rm', '-rf', '{}', ';', '-prune' + } + + // clean '.externalNativeBuild' directories + exec { + workingDir "${rootDir}" + commandLine 'find', '.', '-type', 'd', '-name', '.externalNativeBuild', '-print', '-exec', 'rm', '-rf', '{}', ';', '-prune' + } + + // clean '.gradle' directories except one in the root + exec { + workingDir "${rootDir}" + commandLine 'find', '.', '-mindepth', '2', '-type', 'd', '-name', '.gradle', '-print', '-exec', 'rm', '-rf', '{}', ';', '-prune' + } + + // clean ${System.env.HOME}/.m2/repository/io/realm + exec { + workingDir "${rootDir}" + commandLine 'sh', '-c', "echo \"${System.env.HOME}/.m2/repository/io/realm\" && rm -rf \"${System.env.HOME}/.m2/repository/io/realm\"" + } + } } -['java', 'android'].each() { link -> - task "upload${link.capitalize()}LatestLink"(type: Exec) { - group = 'Release' - description = "Update the link to the latest version for ${link.capitalize()}" - dependsOn createEmptyFile - commandLine 's3cmd', 'put', 'latest', "--add-header=x-amz-website-redirect-location:/downloads/java/realm-java-${currentVersion}.zip", "s3://static.realm.io/downloads/${link}/latest" +task uploadDistributionPackage { + group = 'Release' + description = 'Upload the distribution package to S3' + dependsOn distributionJniUnstrippedPackage + def s3AccessKey = "${ -> getPropertyValueOrThrow('REALM_S3_ACCESS_KEY')}" + def s3SecretKey = "${ -> getPropertyValueOrThrow('REALM_S3_SECRET_KEY')}" + doLast { + + // Check that zip file exists. Creating the zip file will silently fail if no files exists, so check here. + def zipFile = file("${buildDir}/outputs/distribution/realm-java-jni-libs-unstripped-${currentVersion}.zip") + if (!zipFile.exists()) { + throw new GradleException("Could not locate unstripped binary zip file in: ${zipFile.getPath()}") + } + + exec { + commandLine 's3cmd', "--access_key=${s3AccessKey}", "--secret_key=${s3SecretKey}", 'put', zipFile.getAbsolutePath(), 's3://static.realm.io/downloads/java/' + } } } task uploadUpdateVersion(type: Exec) { group = 'Release' description = 'Update the file on S3 containing the latest version' - ['java', 'android'].each() { link -> - dependsOn "upload${link.capitalize()}LatestLink" - } - commandLine 's3cmd', 'put', "${rootDir}/version.txt", 's3://static.realm.io/update/java' + def s3AccessKey = "${ -> getPropertyValueOrThrow('REALM_S3_ACCESS_KEY')}" + def s3SecretKey = "${ -> getPropertyValueOrThrow('REALM_S3_SECRET_KEY')}" + commandLine 's3cmd', "--access_key=${s3AccessKey}", "--secret_key=${s3SecretKey}", 'put', "${rootDir}/version.txt", 's3://static.realm.io/update/java' } task distribute { group = 'Release' description = 'Distribute release artifacts to S3' + dependsOn uploadDistributionPackage dependsOn uploadUpdateVersion } -task bintrayRealm(type: GradleBuild) { - description = 'Publish the Realm AAR and AP to Bintray' +task mavenCentralRealm(type: GradleBuild) { + description = 'Publish the Realm AAR and AP to Maven Central' group = 'Publishing' buildFile = file('realm/build.gradle') - tasks = ['bintrayUpload'] + tasks = ['publishToSonatype'] startParameter.projectProperties = gradle.startParameter.projectProperties - if (project.hasProperty('buildTargetABIs')) { - startParameter.projectProperties += [buildTargetABIs: project.getProperty('buildTargetABIs')] - } + configure copyProperties } -task bintrayAnnotations(type: GradleBuild) { - description = 'Publish the Realm Annotations to Bintray' +task mavenCentralAnnotations(type: GradleBuild) { + description = 'Publish the Realm Annotations to Maven Central' group = 'Publishing' buildFile = file('realm-annotations/build.gradle') + tasks = ['publishToSonatype'] startParameter.projectProperties = gradle.startParameter.projectProperties - tasks = ['bintrayUpload'] + configure copyProperties } -task bintrayGradlePlugin(type: GradleBuild) { - description = 'Publish the Realm Gradle Plugin to Bintray' +task mavenCentralGradlePlugin(type: GradleBuild) { + description = 'Publish the Realm Gradle Plugin to Maven Central' group = 'Publishing' buildFile = file('gradle-plugin/build.gradle') + tasks = ['publishToSonatype'] startParameter.projectProperties = gradle.startParameter.projectProperties - tasks = ['bintrayUpload'] + configure copyProperties } -task bintrayTransformer(type: GradleBuild) { - description = 'Publish the Realm Transformer to Bintray' +task mavenCentralTransformer(type: GradleBuild) { + description = 'Publish the Realm Transformer to Maven Central' group = 'Publishing' buildFile = file('realm-transformer/build.gradle') + tasks = ['publishToSonatype'] startParameter.projectProperties = gradle.startParameter.projectProperties - tasks = ['bintrayUpload'] + configure copyProperties } -task bintrayUpload { - description = 'Publish all the Realm artifacts to Bintray' +task mavenCentralUpload { + description = 'Publish all the Realm artifacts to Maven Central' group = 'Publishing' - dependsOn bintrayRealm - dependsOn bintrayAnnotations - dependsOn bintrayGradlePlugin - dependsOn bintrayTransformer -} - -task ojoRealm(type: GradleBuild) { - description = 'Publish the Realm AAR and AP SNAPSHOT to Bintray' - group = 'Publishing' - buildFile = file('realm/build.gradle') - tasks = ['artifactoryPublish'] - startParameter.projectProperties = gradle.startParameter.projectProperties - if (project.hasProperty('buildTargetABIs')) { - startParameter.projectProperties += [buildTargetABIs: project.getProperty('buildTargetABIs')] - } -} - -task ojoAnnotations(type: GradleBuild) { - description = 'Publish the Realm Annotations SNAPSHOT to Bintray' - group = 'Publishing' - buildFile = file('realm-annotations/build.gradle') - startParameter.projectProperties = gradle.startParameter.projectProperties - tasks = ['artifactoryPublish'] -} - -task ojoGradlePlugin(type: GradleBuild) { - description = 'Publish the Realm Gradle Plugin SNAPSHOT to Bintray' - group = 'Publishing' - buildFile = file('gradle-plugin/build.gradle') - startParameter.projectProperties = gradle.startParameter.projectProperties - tasks = ['artifactoryPublish'] -} - -task ojoTransformer(type: GradleBuild) { - description = 'Publish the Realm Transformer SNAPSHOT to Bintray' - group = 'Publishing' - buildFile = file('realm-transformer/build.gradle') - startParameter.projectProperties = gradle.startParameter.projectProperties - tasks = ['artifactoryPublish'] -} - -task ojoUpload { - description = 'Publish all the Realm SNAPSHOT artifacts to OJO' - group = 'Publishing' - dependsOn ojoRealm - dependsOn ojoAnnotations - dependsOn ojoGradlePlugin - dependsOn ojoTransformer -} - -// This is just a placeholder for the release plugin -task build {} - -release { - push = false - versionSuffix = '-SNAPSHOT' - tagPrefix = 'v' -} - -task wrapper(type: Wrapper) { - gradleVersion = project.gradleVersion + dependsOn mavenCentralRealm + dependsOn mavenCentralAnnotations + dependsOn mavenCentralGradlePlugin + dependsOn mavenCentralTransformer } diff --git a/dependencies.list b/dependencies.list new file mode 100644 index 0000000000..cf5448e601 --- /dev/null +++ b/dependencies.list @@ -0,0 +1,35 @@ +# Realm Core release used by Realm Java +# https://github.com/realm/realm-core/releases +REALM_CORE=13.26.0 + +# Version of MongoDB Realm used by integration tests +# See https://github.com/realm/ci/packages/147854 for available versions +MONGODB_REALM_SERVER=2023-05-31 + +# `BAAS` and `BAAS-UI` projects commit hashes matching MONGODB_REALM_SERVER image version +# note that the MONGODB_REALM_SERVER image is a nightly build, find the matching commits +# for that date within the following repositories: +# https://github.com/10gen/baas/ +# https://github.com/10gen/baas-ui/ +REALM_BAAS_GIT_HASH=1de3337309b9a89094f739efaa69afa2dbc2daa9 +REALM_BAAS_UI_GIT_HASH=9c4ef71f69776651cf0110052ce760920ac8c7da + +# Common Android settings across projects +GRADLE_BUILD_TOOLS=7.4.0 +ANDROID_BUILD_TOOLS=30.0.3 +KOTLIN=1.6.21 +KOTLIN_COROUTINES=1.6.0 + +# Common classpath dependencies +gradle=7.5 +ndkVersion=23.1.7779620 +BUILD_INFO_EXTRACTOR_GRADLE=4.23.4 +GRADLE_NEXUS_PLUGIN=1.0.0 +CMAKE=3.27.7 + +# Bson dependency version +BSON_DEPENDENCY=3.12.1 + +# RxJava dependency version +RXJAVA_DEPENDENCY=2.1.5 +RXANDROID_DEPENDENCY=2.1.1 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..2297402101 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,39 @@ +# Realm SDK for Java +Use the Realm SDK for Java to develop Android apps in Java or Kotlin. +To develop multiplatform apps using Kotlin Multiplatform (KMP), refer to the +Kotlin SDK. + +## SDK in Maintenance Mode +This SDK is in best-effort maintenance mode and **no longer receives +new development or non-critical bug fixes**. To develop your app with new +features, use the Kotlin SDK. You can use the Java SDK with the Kotlin SDK. + +## Develop Apps with the SDK +Use the SDK's open-source database - Realm - as an object store on the device. + +### Install the Java SDK +Use the Gradle build system to +install the Java SDK in your project. + +### Define an Object Schema +Use Java or Kotlin to idiomatically define an object schema. + +### Open a Database +The SDK's database - Realm - stores objects in files on your device. +Or you can open an in-memory database which does not create a file. +To get started reading and writing data, +configure and open a database. + +### Read and Write Data +Create, read, update, and +delete objects from the database. +Use Android-native queries to filter data. + +### React to Changes +Live objects mean that your data is always up-to-date. +You can register a notification handler to watch for changes and perform some +logic, such as updating your UI. + +## Examples + +See the [examples/](..examples/) directory. diff --git a/docs/guides/adapters.md b/docs/guides/adapters.md new file mode 100644 index 0000000000..de56c8a274 --- /dev/null +++ b/docs/guides/adapters.md @@ -0,0 +1,460 @@ +# Display Collections - Java SDK +Android apps often populate the UI using +[RecyclerView](https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.html) +or [ListView](https://developer.android.com/reference/android/widget/ListView) components. +Realm offers **adapters** to display realm object +collections. These collections implement +the `OrderedRealmCollections` interface. RealmResults +and RealmList are examples of these adaptors. +With these adapters, UI components update when your app changes +Realm objects. + +## Install Adapters +Add these dependencies to your application level `build.gradle` file: + +```gradle +dependencies { + implementation 'io.realm:android-adapters:4.0.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' +} +``` + +Realm hosts these adapters on the +[JCenter](https://mvnrepository.com/repos/jcenter) +artifact repository. To use `jcenter` in your Android app, add it to your +project-level `build.gradle` file: + +```gradle +buildscript { + repositories { + jcenter() + } +} + +allprojects { + repositories { + jcenter() + } +} +``` + +> Seealso: +> Source code: [realm/realm-android-adapters](https://github.com/realm/realm-android-adapters) on GitHub. +> + +## Example Models +The examples on this page use a Realm object named `Item`. +This class contains a string named "name" and an identifier number named +"id": + +#### Java + +```java + +import io.realm.RealmObject; + +public class Item extends RealmObject { + int id; + String name; + + public Item() {} + + public int getId() { return id; } + public void setId(int id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } +} + +``` + +#### Kotlin + +```kotlin + +import io.realm.RealmObject + +open class Item(var id: Int = 0, + var name: String? = null): RealmObject() + +``` + +## Display Collections in a ListView +Display Realm objects in a +[ListView](https://developer.android.com/reference/android/widget/ListView) by extending +[RealmBaseAdapter](https://github.com/realm/realm-android-adapters/blob/master/adapters/src/main/java/io/realm/RealmBaseAdapter.java). +The adapter uses the `ListAdapter` interface. Implementation works +like any `ListAdapter`. This provides support for automatically-updating +Realm objects. + +Subclass `RealmBaseAdapter` to display +Item objects in a `ListView`: + +#### Java + +```java +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.TextView; +import com.mongodb.realm.examples.model.java.Item; +import io.realm.OrderedRealmCollection; +import io.realm.RealmBaseAdapter; + +class ExampleListAdapter extends RealmBaseAdapter implements ListAdapter { + String TAG = "REALM_LIST_ADAPTER"; + + ExampleListAdapter(OrderedRealmCollection realmResults) { + super(realmResults); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder viewHolder; + if (convertView == null) { + Log.i(TAG, "Creating view holder"); + // create a top-level layout for our item views + LinearLayout layout = new LinearLayout(parent.getContext()); + layout.setLayoutParams( + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + // create a text view to display item names + TextView titleView = new TextView(parent.getContext()); + titleView.setLayoutParams( + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + // attach the text view to the item view layout + layout.addView(titleView); + convertView = layout; + viewHolder = new ViewHolder(titleView); + convertView.setTag(viewHolder); + } else { + viewHolder = (ViewHolder) convertView.getTag(); + } + + // as long as we + if (adapterData != null) { + final Item item = adapterData.get(position); + viewHolder.title.setText(item.getName()); + Log.i(TAG, "Populated view holder with data: " + item.getName()); + } else { + Log.e(TAG, "No data in adapter! Failed to populate view holder."); + } + return convertView; + } + + private static class ViewHolder { + TextView title; + + public ViewHolder(TextView textView) { + title = textView; + } + } +} + +``` + +To display list data in an activity, instantiate a `ListView`. Then, +attach an `ExampleListAdapter`: + +```java +// instantiate a ListView programmatically +ListView listView = new ListView(activity.getApplicationContext()); +listView.setLayoutParams( + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + +// create an adapter with a RealmResults collection +// and attach it to the ListView +ExampleListAdapter adapter = + new ExampleListAdapter( + realm.where(Item.class).findAll()); +listView.setAdapter(adapter); +ViewGroup.LayoutParams layoutParams = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); +activity.addContentView(listView, layoutParams); + +``` + +#### Kotlin + +```kotlin +import android.util.Log +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.ListAdapter +import android.widget.TextView +import com.mongodb.realm.examples.model.kotlin.Item +import io.realm.OrderedRealmCollection +import io.realm.RealmBaseAdapter + +internal class ExampleListAdapter(realmResults: OrderedRealmCollection?) : + RealmBaseAdapter(realmResults), ListAdapter { + var TAG = "REALM_LIST_ADAPTER" + + override fun getView(position: Int, + convertView: View?, + parent: ViewGroup): View { + var convertView = convertView + val viewHolder: ViewHolder + if (convertView == null) { + Log.i(TAG, "Creating view holder") + // create a top-level layout for our item views + val layout = LinearLayout(parent.context) + layout.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + // create a text view to display item names + val titleView = TextView(parent.context) + titleView.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + + // attach the text view to the item view layout + layout.addView(titleView) + convertView = layout + viewHolder = ViewHolder(titleView) + convertView.tag = viewHolder + } else { + viewHolder = convertView.tag as ViewHolder + } + + // as long as we + if (adapterData != null) { + val item = adapterData!![position]!! + viewHolder.title.text = item.name + Log.i(TAG, "Populated view holder with data: ${item.name}") + } else { + Log.e(TAG, "No data in adapter! Failed to populate view holder.") + } + return convertView + } + + private class ViewHolder(var title: TextView) +} + +``` + +To display list data in an activity, instantiate a `ListView`. Then, +attach an `ExampleListAdapter`: + +```kotlin +// instantiate a ListView programmatically +val listView = ListView(activity!!.applicationContext) +listView.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT +) + +// create an adapter with a RealmResults collection +// and attach it to the ListView +val adapter = ExampleListAdapter(realm.where(Item::class.java).findAll()) +listView.adapter = adapter +val layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT +) +activity!!.addContentView(listView, layoutParams) + +``` + +## Display Collections in a RecyclerView +Display Realm objects in a +[RecyclerView](https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView.html) +by extending [RealmRecyclerViewAdapter](https://github.com/realm/realm-android-adapters/blob/master/adapters/src/main/java/io/realm/RealmRecyclerViewAdapter.java). +The adapter extends `RecyclerView.Adapter`. Implementation works like any +`RecyclerView` adapter. This provides support +for automatically-updating Realm objects. + +Subclass `RealmRecyclerViewAdapter` to display +Item objects in a `RecyclerView`: + +#### Java + +```java +import android.util.Log; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; +import com.mongodb.realm.examples.model.java.Item; +import io.realm.OrderedRealmCollection; +import io.realm.RealmRecyclerViewAdapter; + +/* + * ExampleRecyclerViewAdapter: extends the Realm-provided + * RealmRecyclerViewAdapter to provide data + * for a RecyclerView to display + * Realm objects on screen to a user. + */ +class ExampleRecyclerViewAdapter + extends RealmRecyclerViewAdapter { + String TAG = "REALM_RECYCLER_ADAPTER"; + + ExampleRecyclerViewAdapter(OrderedRealmCollection data) { + super(data, true); + Log.i(TAG, "Created RealmRecyclerViewAdapter for " + + getData().size() + " items."); + } + + @Override + public ExampleViewHolder onCreateViewHolder(ViewGroup parent, + int viewType) { + Log.i(TAG, "Creating view holder"); + TextView textView = new TextView(parent.getContext()); + textView.setLayoutParams( + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT)); + return new ExampleViewHolder(textView); + } + + @Override + public void onBindViewHolder(ExampleViewHolder holder, + int position) { + final Item obj = getItem(position); + Log.i(TAG, "Binding view holder: " + obj.getName()); + holder.data = obj; + holder.title.setText(obj.getName()); + } + + @Override + public long getItemId(int index) { + return getItem(index).getId(); + } + + class ExampleViewHolder extends RecyclerView.ViewHolder { + TextView title; + public Item data; + + ExampleViewHolder(TextView view) { + super(view); + title = view; + } + } +} + +``` + +To display list data in an activity, instantiate a `RecyclerView`. Then, +attach an `ExampleRecyclerViewAdapter`: + +```java +// instantiate a RecyclerView programmatically +RecyclerView recyclerView = + new RecyclerView(activity.getApplicationContext()); +recyclerView.setLayoutManager( + new LinearLayoutManager(activity.getApplicationContext())); +recyclerView.setHasFixedSize(true); +recyclerView.addItemDecoration(new DividerItemDecoration( + activity.getApplicationContext(), + DividerItemDecoration.VERTICAL)); + +// create an adapter with a RealmResults collection +// and attach it to the RecyclerView +ExampleRecyclerViewAdapter adapter = + new ExampleRecyclerViewAdapter( + realm.where(Item.class).findAll()); +recyclerView.setAdapter(adapter); +ViewGroup.LayoutParams layoutParams = + new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); +activity.addContentView(recyclerView, layoutParams); + +``` + +#### Kotlin + +```kotlin +import android.util.Log +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.mongodb.realm.examples.model.kotlin.Item +import io.realm.OrderedRealmCollection +import io.realm.RealmRecyclerViewAdapter + +/* + * ExampleRecyclerViewAdapter: extends the Realm-provided + * RealmRecyclerViewAdapter to provide data + * for a RecyclerView to display + * Realm objects on screen to a user. + */ +internal class ExampleRecyclerViewAdapter(data: OrderedRealmCollection?) : + RealmRecyclerViewAdapter(data, true) { + var TAG = "REALM_RECYCLER_ADAPTER" + + override fun onCreateViewHolder(parent: ViewGroup, + viewType: Int): ExampleViewHolder { + Log.i(TAG, "Creating view holder") + val textView = TextView(parent.context) + textView.layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + return ExampleViewHolder(textView) + } + + override fun onBindViewHolder(holder: ExampleViewHolder, position: Int) { + val obj = getItem(position) + Log.i(TAG, "Binding view holder: ${obj!!.name}") + holder.data = obj + holder.title.text = obj.name + } + + override fun getItemId(index: Int): Long { + return getItem(index)!!.id.toLong() + } + + internal inner class ExampleViewHolder(var title: TextView) + : RecyclerView.ViewHolder(title) { + var data: Item? = null + } + + init { + Log.i(TAG, + "Created RealmRecyclerViewAdapter for ${getData()!!.size} items.") + } +} + +``` + +To display list data in an activity, instantiate a `RecyclerView`. Then, +attach an `ExampleRecyclerViewAdapter`: + +```kotlin +// instantiate a RecyclerView programmatically +val recyclerView = RecyclerView(activity!!.applicationContext) +recyclerView.layoutManager = + LinearLayoutManager(activity!!.applicationContext) +recyclerView.setHasFixedSize(true) +recyclerView.addItemDecoration( + DividerItemDecoration(activity!!.applicationContext, + DividerItemDecoration.VERTICAL)) + +// create an adapter with a RealmResults collection +// and attach it to the RecyclerView +val adapter = ExampleRecyclerViewAdapter(realm.where(Item::class.java).findAll()) +recyclerView.adapter = adapter +val layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT +) +activity!!.addContentView(recyclerView, layoutParams) + +``` + diff --git a/docs/guides/async-api.md b/docs/guides/async-api.md new file mode 100644 index 0000000000..6a925de5d2 --- /dev/null +++ b/docs/guides/async-api.md @@ -0,0 +1,238 @@ +# Asynchronous API - Java SDK +The Java SDK lets you access network and disk +resources in two ways: **synchronously** and **asynchronously**. While +synchronous, or "sync", requests block execution until the request returns +success or failure, asynchronous, or "async", requests assign a +callback and proceed execution to the next line of code. When +the request returns, the callback runs to process results. +In the callback, you can check if the request executed +successfully and either access the returned results or the returned +error. + +## Asynchronous Calls +Asynchronous API requests in the SDK end with the suffix "Async". +There are several different ways an asynchronous request can behave, +depending on which part of the SDK you're using. + +### Realm.Callback +Asynchronous calls to open a realm, +use a final parameter of type `Realm.Callback`. To retrieve returned values after the +request completes, implement the `onSuccess()` method in the callback +object passed as the final parameter to these asynchronous methods. You +should also implement the `onError()` method to handle request failures, +but it is not required. + +#### Java + +```java +Realm.getInstanceAsync(config, new Realm.Callback() { + @Override + public void onSuccess(@NotNull Realm realm) { + Log.v("EXAMPLE", "Successfully fetched realm instance."); + } + public void onError(Exception e) { + Log.e("EXAMPLE", "Failed to get realm instance: " + e); + } +}); + +``` + +#### Kotlin + +```kotlin +Realm.getInstanceAsync(config, object : Realm.Callback() { + override fun onSuccess(realm: Realm) { + Log.v("EXAMPLE", "Successfully fetched realm instance.") + } + + fun onError(e: java.lang.Exception) { + Log.e("EXAMPLE", "Failed to get realm instance: $e") + } +}) + +``` + +### RealmAsyncTask +Asynchronous calls to execute transactions on a realm return +an instance of `RealmAsyncTask`. You can optionally specify an error +handler or a +success notification for `RealmAsyncTask` by +passing additional parameters to the asynchronous call. Additionally, +you use the `cancel()` +method to stop a transaction from completing. The lambda function passed +to a `RealmAsyncTask` contains the write operations to include in the +transaction. + +#### Java + +```java +// transaction logic, success notification, error handler all via lambdas +realm.executeTransactionAsync(transactionRealm -> { + Item item = transactionRealm.createObject(Item.class); +}, () -> { + Log.v("EXAMPLE", "Successfully completed the transaction"); +}, error -> { + Log.e("EXAMPLE", "Failed the transaction: " + error); +}); + +// using class instances for transaction, success, error +realm.executeTransactionAsync(new Realm.Transaction() { + @Override + public void execute(Realm transactionRealm) { + Item item = transactionRealm.createObject(Item.class); + } +}, new Realm.Transaction.OnSuccess() { + @Override + public void onSuccess() { + Log.v("EXAMPLE", "Successfully completed the transaction"); + } +}, new Realm.Transaction.OnError() { + @Override + public void onError(Throwable error) { + Log.e("EXAMPLE", "Failed the transaction: " + error); + } +}); + +``` + +#### Kotlin + +```kotlin +// using class instances for transaction, success, error +realm.executeTransactionAsync(Realm.Transaction { transactionRealm -> + val item: Item = transactionRealm.createObject() +}, Realm.Transaction.OnSuccess { + Log.v("EXAMPLE", "Successfully completed the transaction") +}, Realm.Transaction.OnError { error -> + Log.e("EXAMPLE", "Failed the transaction: $error") +}) + +// transaction logic, success notification, error handler all via lambdas +realm.executeTransactionAsync( + { transactionRealm -> + val item = transactionRealm.createObject() + }, + { Log.v("EXAMPLE", "Successfully completed the transaction") }, + { error -> + Log.e("EXAMPLE", "Failed the transaction: $error") + }) + +``` + +### RealmResults +Asynchronous reads from a realm using `findAllAsync()` immediately return an empty +`[RealmResults` instance. The SDK +executes the query on a background thread and populates the +`RealmResults` instance with the results when the query completes. You +can register a listener with `addChangeListener()` +to receive a notification when the query completes. + +#### Java + +```java +RealmResults items = realm.where(Item.class).findAllAsync(); +// length of items is zero when initially returned +items.addChangeListener(new RealmChangeListener>() { + @Override + public void onChange(RealmResults items) { + Log.v("EXAMPLE", "Completed the query."); + // items results now contains all matched objects (more than zero) + } +}); + +``` + +#### Kotlin + +```kotlin +val items = realm.where().findAllAsync() +// length of items is zero when initially returned +items.addChangeListener(RealmChangeListener { + Log.v("EXAMPLE", "Completed the query.") + // items results now contains all matched objects (more than zero) +}) + +``` + +### RealmResultTask +You can cancel `RealmResultTask` instances just like +`RealmAsyncTask`. To access the values returned by your query, you +can use: + +- `get()` to +block until the operation completes +- `getAsync()` +to handle the result via an App.Callback +instance + +#### Java + +```java +Document queryFilter = new Document("type", "perennial"); +mongoCollection.findOne(queryFilter).getAsync(task -> { + if (task.isSuccess()) { + Plant result = task.get(); + Log.v("EXAMPLE", "successfully found a document: " + result); + } else { + Log.e("EXAMPLE", "failed to find document with: ", task.getError()); + } +}); + +``` + +#### Kotlin + +```kotlin +val queryFilter = Document("type", "perennial") +mongoCollection.findOne(queryFilter) + .getAsync { task -> + if (task.isSuccess) { + val result = task.get() + Log.v("EXAMPLE", "successfully found a document: $result") + } else { + Log.e("EXAMPLE", "failed to find document with: ${task.error}") + } + } + +``` + +## Coroutines +The SDK provides a set of Kotlin extensions to request +asynchronously using coroutines and flows instead of callbacks. You can +use these extensions to execute transactions, watch for changes, read, +and write. + +```kotlin +// open a realm asynchronously +Realm.getInstanceAsync(config, object : Realm.Callback() { + override fun onSuccess(realm: Realm) { + Log.v("EXAMPLE", "Successfully fetched realm instance") + + CoroutineScope(Dispatchers.Main).launch { + // asynchronous transaction + realm.executeTransactionAwait(Dispatchers.IO) { transactionRealm: Realm -> + if (isActive) { + val item = transactionRealm.createObject() + } + } + } + // asynchronous query + val items: Flow> = realm.where().findAllAsync().toFlow() + } + + fun onError(e: Exception) { + Log.e("EXAMPLE", "Failed to get realm instance: $e") + } +}) + +``` + +> Tip: +> The `toFlow()` extension method passes frozen Realm objects to safely +communicate between threads. +> + +> Seealso: +> The SDK also includes Kotlin extensions that make specifying type +parameters for Realm reads and writes easier. +> diff --git a/docs/guides/crud.md b/docs/guides/crud.md new file mode 100644 index 0000000000..5268d53319 --- /dev/null +++ b/docs/guides/crud.md @@ -0,0 +1,120 @@ +# CRUD - Java SDK +## Write Operations +You can **create** objects in a realm, +**update** objects in a realm, and eventually **delete** +objects from a realm. Because these operations modify the +state of the realm, we call them writes. + +Realm handles writes in terms of **transactions**. A +transaction is a list of read and write operations that +Realm treats as a single indivisible operation. In other +words, a transaction is *all or nothing*: either all of the +operations in the transaction succeed or none of the +operations in the transaction take effect. + +> Note: +> All writes must happen in a transaction. +> + +A realm allows only one open write transaction at a time. Realm +blocks other writes on other threads until the open +transaction is complete. Consequently, there is no race +condition when reading values from the realm within a +transaction. + +When you are done with your transaction, Realm either +**commits** it or **cancels** it: + +- When Realm **commits** a transaction, Realm writes +all changes to disk. +- When Realm **cancels** a write transaction or an operation in +the transaction causes an error, all changes are discarded +(or "rolled back"). + +> Tip: +> Whenever you create, update, or delete a Realm object, +your changes update the representation of that object in +Realm and emit +notifications to any subscribed +listeners. As a result, you should only write to Realm +objects when necessary to persist data. +> + +> Important: +> By default, you can only read or write to a realm in your +application's UI thread using +asynchronous transactions. That is, +you can only use `Realm` methods whose name ends with the word +`Async` in the main thread of your Android application unless you +explicitly allow the use of synchronous methods. +> +> This restriction exists for the benefit of your application users: +performing read and write operations on the UI thread can lead to +unresponsive or slow UI interactions, so it's usually best to handle +these operations either asynchronously or in a background thread. + +## Managed Objects +**Managed objects** are live Realm objects that update +based on changes to underlying data in Realm. Managed +objects can only come from an open realm, and receive updates +as long as that realm remains open. Managed objects *cannot be passed +between threads*. + +## Unmanaged objects +**Unmanaged objects** are instances of Realm objects that are +not live. You can get an unmanaged object by manually constructing a +Realm object yourself, or by calling +`[Realm.copyFromRealm()`. +Unmanaged objects *can be passed between threads*. + +## Run a Transaction +Realm represents each transaction as a callback function +that contains zero or more read and write operations. To run +a transaction, define a transaction callback and pass it to +the realm's `write` method. Within this callback, you are +free to create, read, update, and delete on the realm. If +the code in the callback throws an exception when Realm runs +it, Realm cancels the transaction. Otherwise, Realm commits +the transaction immediately after the callback. + +> Example: +> The following code shows how to run a transaction with +`executeTransaction()` +or `executeTransactionAsync()`. +If the code in the callback throws an exception, Realm +cancels the transaction. Otherwise, Realm commits the +transaction. +> +> #### Java +> +> ```java +> realm.executeTransaction(r -> { +> // Create a turtle enthusiast named Ali. +> TurtleEnthusiast ali = r.createObject(TurtleEnthusiast.class, new ObjectId()); +> ali.setName("Ali"); +> // Find turtles younger than 2 years old +> RealmResults hatchlings = r.where(Turtle.class).lessThan("age", 2).findAll(); +> // Give all hatchlings to Ali. +> hatchlings.setObject("owner", ali); +> }); +> +> ``` +> +> +> #### Kotlin +> +> ```kotlin +> realm.executeTransaction { r: Realm -> +> // Create a turtle enthusiast named Ali. +> val ali = r.createObject(TurtleEnthusiast::class.java, ObjectId()) +> ali.name = "Ali" +> // Find turtles younger than 2 years old +> val hatchlings = +> r.where(Turtle::class.java).lessThan("age", 2).findAll() +> // Give all hatchlings to Ali. +> hatchlings.setObject("owner", ali) +> } +> +> ``` +> +> diff --git a/docs/guides/crud/create.md b/docs/guides/crud/create.md new file mode 100644 index 0000000000..6c101149f7 --- /dev/null +++ b/docs/guides/crud/create.md @@ -0,0 +1,200 @@ +# CRUD - Create - Java SDK +## About the Examples on this Page +The examples on this page use the data model of a project +management app that has two Realm object types: `Project` +and `Task`. A `Project` has zero or more `Tasks`. + +See the schema for these two classes, `Project` and +`Task`, below: + +#### Java + +```java + +import org.bson.types.ObjectId; + +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.RealmClass; +import io.realm.annotations.Required; + +public class ProjectTask extends RealmObject { + @PrimaryKey + public ObjectId _id; + @Required + public String name; + public String assignee; + public int progressMinutes; + public boolean isComplete; + public int priority; + @Required + public String _partition; +} + +``` + +```java + +import org.bson.types.ObjectId; + +import io.realm.RealmList; +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.RealmClass; +import io.realm.annotations.Required; + +public class Project extends RealmObject { + @PrimaryKey + public ObjectId _id; + @Required + public String name; + public RealmList tasks = new RealmList<>(); +} + +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import io.realm.annotations.Required +import org.bson.types.ObjectId + +open class ProjectTask( + @PrimaryKey + var _id: ObjectId = ObjectId(), + @Required + var name: String = "", + var assignee: String? = null, + var progressMinutes: Int = 0, + var isComplete: Boolean = false, + var priority: Int = 0, + var _partition: String = "" +): RealmObject() + +``` + +```kotlin +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import io.realm.annotations.Required +import org.bson.types.ObjectId + +open class Project( + @PrimaryKey + var _id: ObjectId = ObjectId(), + @Required + var name: String = "", + var tasks: RealmList = RealmList(), +): RealmObject() + +``` + +## Create a New Object +Use `realm.createObject()` +in a transaction to create a persistent instance of a Realm object in a +realm. You can then modify the returned object with other field values +using accessors and mutators. + +The following example demonstrates how to create an object with +`createObject()`: + +#### Java + +```java +realm.executeTransaction(r -> { + // Instantiate the class using the factory function. + Turtle turtle = r.createObject(Turtle.class, new ObjectId()); + // Configure the instance. + turtle.setName("Max"); + // Create a TurtleEnthusiast with a primary key. + ObjectId primaryKeyValue = new ObjectId(); + TurtleEnthusiast turtleEnthusiast = r.createObject(TurtleEnthusiast.class, primaryKeyValue); +}); + +``` + +#### Kotlin + +```kotlin +realm.executeTransaction { r: Realm -> + // Instantiate the class using the factory function. + val turtle = r.createObject(Turtle::class.java, ObjectId()) + // Configure the instance. + turtle.name = "Max" + // Create a TurtleEnthusiast with a primary key. + val primaryKeyValue = ObjectId() + val turtleEnthusiast = r.createObject( + TurtleEnthusiast::class.java, + primaryKeyValue + ) +} + +``` + +You can also insert objects into a realm from JSON. Realm +supports creating objects from `String`, +[JSONObject](https://developer.android.com/reference/org/json/JSONObject.html), and +[InputStream](https://developer.android.com/reference/java/io/InputStream.html) types. +Realm ignores any properties present in the JSON that are +not defined in the Realm object schema. + +The following example demonstrates how to create a single object from JSON with +`createObjectFromJson()` +or multiple objects from JSON with +`createAllFromJson()`: + +#### Java + +```java +// Insert from a string +realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + realm.createObjectFromJson(Frog.class, + "{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\" }"); + } +}); + +// Insert multiple items using an InputStream +realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(Realm realm) { + try { + InputStream inputStream = new FileInputStream( + new File("path_to_file")); + realm.createAllFromJson(Frog.class, inputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +}); + +``` + +#### Kotlin + +```kotlin +// Insert from a string +realm.executeTransaction { realm -> + realm.createObjectFromJson( + Frog::class.java, + "{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\" }" + ) +} + +// Insert multiple items using an InputStream +realm.executeTransaction { realm -> + try { + val inputStream: InputStream = + FileInputStream(File("path_to_file")) + realm.createAllFromJson(Frog::class.java, inputStream) + } catch (e: IOException) { + throw RuntimeException(e) + } +} + +``` + diff --git a/docs/guides/crud/delete.md b/docs/guides/crud/delete.md new file mode 100644 index 0000000000..9d37cac278 --- /dev/null +++ b/docs/guides/crud/delete.md @@ -0,0 +1,269 @@ +# CRUD - Delete - Java SDK +## About the Examples on this Page +The examples on this page use the data model of a project +management app that has two Realm object types: `Project` +and `Task`. A `Project` has zero or more `Tasks`. + +See the schema for these two classes, `Project` and +`Task`, below: + +#### Java + +```java + +import org.bson.types.ObjectId; + +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.RealmClass; +import io.realm.annotations.Required; + +public class ProjectTask extends RealmObject { + @PrimaryKey + public ObjectId _id; + @Required + public String name; + public String assignee; + public int progressMinutes; + public boolean isComplete; + public int priority; + @Required + public String _partition; +} + +``` + +```java + +import org.bson.types.ObjectId; + +import io.realm.RealmList; +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.RealmClass; +import io.realm.annotations.Required; + +public class Project extends RealmObject { + @PrimaryKey + public ObjectId _id; + @Required + public String name; + public RealmList tasks = new RealmList<>(); +} + +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import io.realm.annotations.Required +import org.bson.types.ObjectId + +open class ProjectTask( + @PrimaryKey + var _id: ObjectId = ObjectId(), + @Required + var name: String = "", + var assignee: String? = null, + var progressMinutes: Int = 0, + var isComplete: Boolean = false, + var priority: Int = 0, + var _partition: String = "" +): RealmObject() + +``` + +```kotlin +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import io.realm.annotations.Required +import org.bson.types.ObjectId + +open class Project( + @PrimaryKey + var _id: ObjectId = ObjectId(), + @Required + var name: String = "", + var tasks: RealmList = RealmList(), +): RealmObject() + +``` + +## Delete an Object +To delete an object from a realm, use either the dynamic or static +versions of the `deleteFromRealm()` method of a `RealmObject` subclass. + +The following example shows how to delete one object from +its realm with `deleteFromRealm()`: + +#### Java + +```java +realm.executeTransaction(r -> { + // Get a turtle named "Tony". + Turtle tony = r.where(Turtle.class).equalTo("name", "Tony").findFirst(); + tony.deleteFromRealm(); + // discard the reference + tony = null; +}); + +``` + +#### Kotlin + +```kotlin +realm.executeTransaction { r: Realm -> + // Get a turtle named "Tony". + var tony = r.where(Turtle::class.java) + .equalTo("name", "Tony") + .findFirst() + tony!!.deleteFromRealm() + // discard the reference + tony = null +} + +``` + +> Tip: +> The SDK throws an error if you try to use an object after +it has been deleted. +> + +## Delete Multiple Objects +To delete an object from a realm, use the `deleteAllFromRealm()` +method of the `RealmResults` +instance that contains the objects you would like to delete. You can +filter the `RealmResults` down to a subset of objects using the +`where()` method. + +The following example demonstrates how to delete a +collection from a realm with `deleteAllFromRealm()`: + +#### Java + +```java +realm.executeTransaction(r -> { + // Find turtles older than 2 years old. + RealmResults oldTurtles = r.where(Turtle.class).greaterThan("age", 2).findAll(); + oldTurtles.deleteAllFromRealm(); +}); + +``` + +#### Kotlin + +```kotlin +realm.executeTransaction { r: Realm -> + // Find turtles older than 2 years old. + val oldTurtles = r.where(Turtle::class.java) + .greaterThan("age", 2) + .findAll() + oldTurtles.deleteAllFromRealm() +} + +``` + +## Delete an Object and its Dependent Objects +Sometimes, you have dependent objects that you want to delete when +you delete the parent object. We call this a **chaining +delete**. Realm does not delete the dependent +objects for you. If you do not delete the objects yourself, +they will remain orphaned in your realm. Whether or not +this is a problem depends on your application's needs. + +Currently, the best way to delete dependent objects is to +iterate through the dependencies and delete them before +deleting the parent object. + +The following example demonstrates how to perform a +chaining delete by first deleting all of Ali's turtles, +then deleting Ali: + +#### Java + +```java +realm.executeTransaction(r -> { + // Find a turtle enthusiast named "Ali" + TurtleEnthusiast ali = r.where(TurtleEnthusiast.class).equalTo("name", "Ali").findFirst(); + // Delete all of ali's turtles + ali.getTurtles().deleteAllFromRealm(); + ali.deleteFromRealm(); +}); + +``` + +#### Kotlin + +```kotlin +realm.executeTransaction { r: Realm -> + // Find a turtle enthusiast named "Ali" + val ali = r.where(TurtleEnthusiast::class.java) + .equalTo("name", "Ali").findFirst() + // Delete all of ali's turtles + ali!!.turtles!!.deleteAllFromRealm() + ali.deleteFromRealm() +} + +``` + +## Delete All Objects of a Specific Type +Realm supports deleting all instances of a Realm type from a realm. + +The following example demonstrates how to delete all +Turtle instances from a realm with `delete()`: + +#### Java + +```java +realm.executeTransaction(r -> { + r.delete(Turtle.class); +}); + +``` + +#### Kotlin + +```kotlin +realm.executeTransaction { r: Realm -> + r.delete(Turtle::class.java) +} + +``` + +## Delete All Objects in a Realm +It is possible to delete all objects from the realm. This +does not affect the schema of the realm. This is useful for +quickly clearing out your realm while prototyping. + +The following example demonstrates how to delete everything +from a realm with `deleteAll()`: + +#### Java + +```java +realm.executeTransaction(r -> { + r.deleteAll(); +}); + +``` + +#### Kotlin + +```kotlin +realm.executeTransaction { r: Realm -> + r.deleteAll() +} + +``` + +## Delete an Object Using an Iterator +Because realm collections always reflect the latest state, they +can appear, disappear, or change while you iterate over a collection. +To get a stable collection you can iterate over, you can create a +**snapshot** of a collection's data. A snapshot guarantees the order of +elements will not change, even if an element is deleted. + +For an example, refer to Iteration. diff --git a/docs/guides/crud/filter-data.md b/docs/guides/crud/filter-data.md new file mode 100644 index 0000000000..fd883d226f --- /dev/null +++ b/docs/guides/crud/filter-data.md @@ -0,0 +1,803 @@ +# Filter Data - Java SDK +## Query Engine +To filter data in your realm, use the Realm query engine. + +There are two ways to access the query engine with the Java SDK: + +- Fluent interface +- Realm Query Language + +## Fluent Interface +The Java SDK uses a [Fluent interface](https://en.wikipedia.org/wiki/Fluent_interface) +to construct multi-clause queries that are passed to the query engine. + +See RealmQuery API +for a complete list of available methods. + +There are several types of operators available to filter a +Realm collection. +Filters work by **evaluating** an operator expression for +every object in the collection being +filtered. If the expression resolves to `true`, Realm +Database includes the object in the results collection. + +An **expression** consists of one of the following: + +- The name of a property of the object currently being evaluated. +- An operator and up to two argument expression(s). +- A literal string, number, or date. + +### About the Examples In This Section +The examples in this section use a simple data set for a +task list app. The two Realm object types are `Project` +and `Task`. A `Task` has a name, assignee's name, and +completed flag. There is also an arbitrary number for +priority (higher is more important) and a count of +minutes spent working on it. A `Project` has zero or more +`Tasks`. + +See the schema for these two classes, `Project` and +`Task`, below: + +#### Java + +```java + +import org.bson.types.ObjectId; + +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.RealmClass; +import io.realm.annotations.Required; + +public class ProjectTask extends RealmObject { + @PrimaryKey + public ObjectId _id; + @Required + public String name; + public String assignee; + public int progressMinutes; + public boolean isComplete; + public int priority; + @Required + public String _partition; +} + +``` + +```java + +import org.bson.types.ObjectId; + +import io.realm.RealmList; +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.RealmClass; +import io.realm.annotations.Required; + +public class Project extends RealmObject { + @PrimaryKey + public ObjectId _id; + @Required + public String name; + public RealmList tasks = new RealmList<>(); +} + +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import io.realm.annotations.Required +import org.bson.types.ObjectId + +open class ProjectTask( + @PrimaryKey + var _id: ObjectId = ObjectId(), + @Required + var name: String = "", + var assignee: String? = null, + var progressMinutes: Int = 0, + var isComplete: Boolean = false, + var priority: Int = 0, + var _partition: String = "" +): RealmObject() + +``` + +```kotlin +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import io.realm.annotations.Required +import org.bson.types.ObjectId + +open class Project( + @PrimaryKey + var _id: ObjectId = ObjectId(), + @Required + var name: String = "", + var tasks: RealmList = RealmList(), +): RealmObject() + +``` + +### Comparison Operators +The most straightforward operation in a search is to compare +values. + +|Operator|Description| +| --- | --- | +|`between`|Evaluates to `true` if the left-hand numerical or date expression is between or equal to the right-hand range. For dates, this evaluates to `true` if the left-hand date is within the right-hand date range.| +|equalTo|Evaluates to `true` if the left-hand expression is equal to the right-hand expression.| +|greaterThan|Evaluates to `true` if the left-hand numerical or date expression is greater than the right-hand numerical or date expression. For dates, this evaluates to `true` if the left-hand date is later than the right-hand date.| +|greaterThanOrEqualTo|Evaluates to `true` if the left-hand numerical or date expression is greater than or equal to the right-hand numerical or date expression. For dates, this evaluates to `true` if the left-hand date is later than or the same as the right-hand date.| +|`in`|Evaluates to `true` if the left-hand expression is in the right-hand list.| +|lessThan|Evaluates to `true` if the left-hand numerical or date expression is less than the right-hand numerical or date expression. For dates, this evaluates to `true` if the left-hand date is earlier than the right-hand date.| +|lessThanOrEqualTo|Evaluates to `true` if the left-hand numeric expression is less than or equal to the right-hand numeric expression. For dates, this evaluates to `true` if the left-hand date is earlier than or the same as the right-hand date.| +|notEqualTo|Evaluates to `true` if the left-hand expression is not equal to the right-hand expression.| + +> Example: +> The following example uses the query engine's +comparison operators to: +> +> - Find high priority tasks by comparing the value of the `priority` property value with a threshold number, above which priority can be considered high. +> - Find just-started or short-running tasks by seeing if the `progressMinutes` property falls within a certain range. +> - Find unassigned tasks by finding tasks where the `assignee` property is equal to `null`. +> - Find tasks assigned to specific teammates Ali or Jamie by seeing if the `assignee` property is in a list of names. +> +> #### Java +> +> ```java +> RealmQuery tasksQuery = realm.where(ProjectTask.class); +> Log.i("EXAMPLE", "High priority tasks: " + tasksQuery.greaterThan("priority", 5).count()); +> Log.i("EXAMPLE", "Just-started or short tasks: " + tasksQuery.between("progressMinutes", 1, 10).count()); +> Log.i("EXAMPLE", "Unassigned tasks: " + tasksQuery.isNull("assignee").count()); +> Log.i("EXAMPLE", "Ali or Jamie's tasks: " + tasksQuery.in("assignee", new String[]{"Ali", "Jamie"}).count()); +> +> ``` +> +> +> #### Kotlin +> +> ```kotlin +> val tasksQuery = realm.where(ProjectTask::class.java) +> Log.i("EXAMPLE", "High priority tasks: " + tasksQuery.greaterThan("priority", 5).count()) +> Log.i("EXAMPLE", "Just-started or short tasks: " + tasksQuery.between("progressMinutes", 1, 10).count()) +> Log.i("EXAMPLE", "Unassigned tasks: " + tasksQuery.isNull("assignee").count()) +> Log.i("EXAMPLE", "Ali or Jamie's tasks: " + tasksQuery.`in`("assignee", arrayOf("Ali", "Jamie")).count()) +> +> ``` +> +> + +### Logical Operators +You can make compound predicates using logical operators. + +|Operator|Description| +| --- | --- | +|and|Evaluates to `true` if both left-hand and right-hand expressions are `true`.| +|not|Negates the result of the given expression.| +|or|Evaluates to `true` if either expression returns `true`.| + +> Example: +> We can use the query language's logical operators to find +all of Ali's completed tasks. That is, we find all tasks +where the `assignee` property value is equal to 'Ali' AND +the `isComplete` property value is `true`: +> +> #### Java +> +> ```java +> RealmQuery tasksQuery = realm.where(ProjectTask.class); +> Log.i("EXAMPLE", "Ali has completed " + +> tasksQuery.equalTo("assignee", "Ali").and().equalTo("isComplete", true).findAll().size() + +> " tasks."); +> +> ``` +> +> +> #### Kotlin +> +> ```kotlin +> val tasksQuery = realm.where(ProjectTask::class.java) +> Log.i("EXAMPLE", "Ali has completed " + +> tasksQuery.equalTo("assignee", "Ali").and() +> .equalTo("isComplete", true).findAll().size + " tasks.") +> +> ``` +> +> + +### String Operators +You can compare string values using these string operators. +Regex-like wildcards allow more flexibility in search. + +|Operator|Description| +| --- | --- | +|beginsWith|Evaluates to `true` if the left-hand string expression begins with the right-hand string expression. This is similar to `contains`, but only matches if the left-hand string expression is found at the beginning of the right-hand string expression.| +|`contains`|Evaluates to `true` if the left-hand string expression is found anywhere in the right-hand string expression.| +|endsWith|Evaluates to `true` if the left-hand string expression ends with the right-hand string expression. This is similar to `contains`, but only matches if the left-hand string expression is found at the very end of the right-hand string expression.| +|like|Evaluates to `true` if the left-hand string expression matches the right-hand string wildcard string expression. A wildcard string expression is a string that uses normal characters with two special wildcard characters: The `*` wildcard matches zero or more of any character The `?` wildcard matches any character. For example, the wildcard string "d?g" matches "dog", "dig", and "dug", but not "ding", "dg", or "a dog".| +|equalTo|Evaluates to `true` if the left-hand string is lexicographically equal to the right-hand string.| + +> Example: +> We use the query engine's string operators to find +projects with a name starting with the letter 'e' and +projects with names that contain 'ie': +> +> #### Java +> +> ```java +> RealmQuery projectsQuery = realm.where(Project.class); +> // Pass Case.INSENSITIVE as the third argument for case insensitivity. +> Log.i("EXAMPLE", "Projects that start with 'e': " +> + projectsQuery.beginsWith("name", "e", Case.INSENSITIVE).count()); +> Log.i("EXAMPLE", "Projects that contain 'ie': " +> + projectsQuery.contains("name", "ie").count()); +> +> ``` +> +> +> #### Kotlin +> +> ```kotlin +> val projectsQuery = realm.where(Project::class.java) +> // Pass Case.INSENSITIVE as the third argument for case insensitivity. +> Log.i("EXAMPLE", "Projects that start with 'e': " +> + projectsQuery.beginsWith("name", "e", Case.INSENSITIVE).count()) +> Log.i("EXAMPLE", "Projects that contain 'ie': " +> + projectsQuery.contains("name", "ie").count()) +> +> ``` +> +> + +> Note: +> Case-insensitive string operators only support the +`Latin Basic`, `Latin Supplement`, `Latin Extended A`, and +`Latin Extended B (UTF-8 range 0-591)` character sets. Setting +the case insensitive flag in queries when using `equalTo`, +`notEqualTo`, `contains`, `endsWith`, `beginsWith`, or +`like` only works on English locale characters. +> + +### Aggregate Operators +You can apply an aggregate operator to a collection property +of a Realm object. Aggregate operators traverse a +collection and reduce it +to a single value. + +|Operator|Description| +| --- | --- | +|average|Evaluates to the average value of a given numerical property across a collection.| +|count|Evaluates to the number of objects in the given collection.| +|max|Evaluates to the highest value of a given numerical property across a collection.| +|min|Evaluates to the lowest value of a given numerical property across a collection.| +|sum|Evaluates to the sum of a given numerical property across a collection.| + +> Example: +> We create a couple of filters to show different facets of +the data: +> +> - Projects with average tasks priority above 5. +> - Long running projects. +> +> #### Java +> +> ```java +> RealmQuery tasksQuery = realm.where(ProjectTask.class); +> /* +> Aggregate operators do not support dot-notation, so you +> cannot directly operate on a property of all of the objects +> in a collection property. +> +> You can operate on a numeric property of the top-level +> object, however: +> */ +> Log.i("EXAMPLE", "Tasks average priority: " + tasksQuery.average("priority")); +> +> ``` +> +> +> #### Kotlin +> +> ```kotlin +> val tasksQuery = realm.where(ProjectTask::class.java) +> /* +> Aggregate operators do not support dot-notation, so you +> cannot directly operate on a property of all of the objects +> in a collection property. +> +> You can operate on a numeric property of the top-level +> object, however: +> */Log.i("EXAMPLE", "Tasks average priority: " + tasksQuery.average("priority")) +> +> ``` +> +> + +## Filter, Sort, Limit, Unique, and Chain Queries +### About the Examples in This Section +The examples in this section use two Realm object types: `Teacher` +and `Student`. + +See the schema for these two classes below: + +#### Java + +```java +import io.realm.RealmList; +import io.realm.RealmObject; + +public class Teacher extends RealmObject { + private String name; + private Integer numYearsTeaching; + private String subject; + private RealmList students; + public Teacher() {} + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public Integer getNumYearsTeaching() { return numYearsTeaching; } + public void setNumYearsTeaching(Integer numYearsTeaching) { this.numYearsTeaching = numYearsTeaching; } + public String getSubject() { return subject; } + public void setSubject(String subject) { this.subject = subject; } + public RealmList getStudents() { return students; } + public void setStudents(RealmList students) { this.students = students; } +} + +``` + +```java +import io.realm.RealmObject; +import io.realm.RealmResults; +import io.realm.annotations.LinkingObjects; + +public class Student extends RealmObject { + private String name; + private Integer year; + @LinkingObjects("students") + private final RealmResults teacher = null; + public Student() {} + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public Integer getYear() { return year; } + public void setYear(Integer year) { this.year = year; } + public RealmResults getTeacher() { return teacher; } +} + +``` + +#### Kotlin + +```kotlin +import io.realm.RealmList +import io.realm.RealmObject + +open class Teacher : RealmObject() { + var name: String? = null + var numYearsTeaching: Int? = null + var subject: String? = null + var students: RealmList? = null +} + +``` + +```kotlin +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects + +open class Student : RealmObject() { + var name: String? = null + var year: Int? = null + + @LinkingObjects("students") + val teacher: RealmResults? = null +} + +``` + +### Filters +You can build filters using the operator methods of the +[fluent interface](https://en.wikipedia.org/wiki/Fluent_interface) exposed by the +`RealmQuery` class: + +#### Java + +```java +// Build the query looking at all teachers: +RealmQuery query = realm.where(Teacher.class); + +// Add query conditions: +query.equalTo("name", "Ms. Langtree"); +query.or().equalTo("name", "Mrs. Jacobs"); + +// Execute the query: +RealmResults result1 = query.findAll(); + +// Or alternatively do the same all at once (the "Fluent interface"): +RealmResults result2 = realm.where(Teacher.class) + .equalTo("name", "Ms. Langtree") + .or() + .equalTo("name", "Mrs. Jacobs") + .findAll(); + +``` + +#### Kotlin + +```kotlin +// Build the query looking at all teachers: +val query = realm.where(Teacher::class.java) + +// Add query conditions: +query.equalTo("name", "Ms. Langtree") +query.or().equalTo("name", "Mrs. Jacobs") + +// Execute the query: +val result1 = query.findAll() + +// Or alternatively do the same all at once (the "Fluent interface"): +val result2 = realm.where(Teacher::class.java) + .equalTo("name", "Ms. Langtree") + .or() + .equalTo("name", "Mrs. Jacobs") + .findAll() + +``` + +This gives you a new instance of the class `RealmResults`, +containing teachers with the name "Ms. Langtree" or "Mrs. Jacobs". + +`RealmQuery` includes several methods that can execute queries: + +- `findAll()` blocks until +it finds all objects that meet the query conditions +- `findAllAsync()` +returns immediately and finds all objects that meet the query +conditions asynchronously on a background thread +- `findFirst()` blocks +until it finds the first object that meets the query conditions +- `findFirstAsync()` +returns immediately and finds the first object that meets the query +conditions asynchronously on a background thread + +Queries return a list of references to the matching Realm +objects using the RealmResults type. + +#### Link Queries +When referring to an object property, you can use **dot notation** to refer +to child properties of that object. You can refer to the properties of +embedded objects and relationships with dot notation. + +For example, consider a query for all teachers with a student named +"Wirt" or "Greg": + +#### Java + +```java +// Find all teachers who have students with the names "Wirt" or "Greg" +RealmResults result = realm.where(Teacher.class) + .equalTo("students.name", "Wirt") + .or() + .equalTo("students.name", "Greg") + .findAll(); + +``` + +#### Kotlin + +```kotlin +// Find all teachers who have students with the names "Wirt" or "Greg" +val result = realm.where(Teacher::class.java) + .equalTo("students.name", "Wirt") + .or() + .equalTo("students.name", "Greg") + .findAll() + +``` + +You can even use dot notation to query inverse relationships: + +#### Java + +```java +// Find all students who have teachers with the names "Ms. Langtree" or "Mrs. Jacobs" +RealmResults result = realm.where(Student.class) + .equalTo("teacher.name", "Ms. Langtree") + .or() + .equalTo("teacher.name", "Mrs. Jacobs") + .findAll(); + +``` + +#### Kotlin + +```kotlin +// Find all students who have teachers with the names "Ms. Langtree" or "Mrs. Jacobs" +val result = realm.where(Student::class.java) + .equalTo("teacher.name", "Ms. Langtree") + .or() + .equalTo("teacher.name", "Mrs. Jacobs") + .findAll() + +``` + +### Sort Results +> Important: +> Realm applies the `distinct()`, `sort()` and +`limit()` methods in the order you specify. Depending on the +data set this can alter the query result. Generally, you should +apply `limit()` last to avoid unintended result sets. +> + +You can define the order of query results using the +`sort()` +method: + +#### Java + +```java +// Find all students in year 7, and sort them by name +RealmResults result = realm.where(Student.class) + .equalTo("year", 7) + .sort("name") + .findAll(); + +// Alternatively, find all students in year 7 +RealmResults unsortedResult = realm.where(Student.class) + .equalTo("year", 7) + .findAll(); +// then sort the results set by name +RealmResults sortedResult = unsortedResult.sort("name"); + +``` + +#### Kotlin + +```kotlin +// Find all students in year 7, and sort them by name +val result: RealmResults = realm.where(Student::class.java) + .equalTo("year", 7L) + .sort("name") + .findAll() + +// Alternatively, find all students in year 7 +val unsortedResult: RealmResults = realm.where(Student::class.java) + .equalTo("year", 7L) + .findAll() +// then sort the results set by name +val sortedResult = unsortedResult.sort("name") + +``` + +Sorts organize results in ascending order by default. To organize results +in descending order, pass `Sort.DESCENDING` as a second argument. +You can resolve sort order ties between identical property values +by passing an array of properties instead of a single property: in the +event of a tie, Realm sorts the tied objects by subsequent +properties in order. + +> Note: +> Realm uses non-standard sorting for upper and lowercase +letters, sorting them together rather than sorting uppercase first. +As a result, `'- !"#0&()*,./:;?_+<=>123aAbBcC...xXyYzZ` is the +actual sorting order in Realm. Additionally, sorting +strings only supports the `Latin Basic`, `Latin Supplement`, +`Latin Extended A`, and `Latin Extended B (UTF-8 range 0–591)` +character sets. +> + +### Limit Results +You can cap the number of query results to a specific maximum number +using the `limit()` +method: + +#### Java + +```java +// Find all students in year 8, and limit the results collection to 10 items +RealmResults result = realm.where(Student.class) + .equalTo("year", 8) + .limit(10) + .findAll(); + +``` + +#### Kotlin + +```kotlin +// Find all students in year 8, and limit the results collection to 10 items +val result: RealmResults = realm.where(Student::class.java) + .equalTo("year", 8L) + .limit(10) + .findAll() + +``` + +Limited result collections automatically update like any other query +result. Consequently, objects might drop out of the collection as +underlying data changes. + +> Tip: +> Some databases encourage paginating results with limits to avoid +reading unnecessary data from disk or using too much memory. +> +> Since Realm queries are lazy, there is no need to +take such measures. Realm only loads objects from query +results when they are explicitly accessed. +> + +> Tip: +> Collection notifications +report objects as deleted when they drop out of the result set. +This does not necessarily mean that they have been deleted from the +underlying realm, just that they are no longer part of the +query result. +> + +### Unique Results +You can reduce query results to unique values for a given field or fields +using the `distinct()` method: + +#### Java + +```java +// Find all students in year 9, and cap the result collection at 10 items +RealmResults result = realm.where(Student.class) + .equalTo("year", 9) + .distinct("name") + .findAll(); + +``` + +#### Kotlin + +```kotlin +// Find all students in year 9, and cap the result collection at 10 items +val result: RealmResults = realm.where(Student::class.java) + .equalTo("year", 9L) + .distinct("name") + .findAll() + +``` + +You can only call `distinct()` on integer, long, short, and `String` +fields; other field types will throw an exception. As with sorting, +you can specify multiple fields to resolve ties. + +### Chain Queries +You can apply additional filters to a results collection by calling the +`where()` method: + +#### Java + +```java +// Find all students in year 9 and resolve the query into a results collection +RealmResults result = realm.where(Student.class) + .equalTo("year", 9) + .findAll(); + +// filter the students results again by teacher name +RealmResults filteredResults = result.where().equalTo("teacher.name", "Ms. Langtree").findAll(); + +``` + +#### Kotlin + +```kotlin +// Find all students in year 9 and resolve the query into a results collection +val result: RealmResults = realm.where(Student::class.java) + .equalTo("year", 9L) + .findAll() + +// filter the students results again by teacher name +val filteredResults = + result.where().equalTo("teacher.name", "Ms. Langtree").findAll() + +``` + +The `where()` method returns a `RealmQuery` that you can resolve into +a `RealmResults` using a `find` method. Filtered results can only +return objects of the same type as the original results set, but are +otherwise able to use any filters. + +## Query with Realm Query Language +> Version added: 10.4.0 + +You can also query realms using Realm Query Language, a string-based +query language to constrain searches when retrieving objects from a realm. + +You can use `RealmQuery.rawPredicate()`. +For more information about syntax, usage and limitations, +refer to the Realm Query Language reference. + +Realm Query Language can use either the class and property names defined +in your Realm Model classes or the internal names defined with `@RealmField`. +You can combine raw predicates with other raw predicates or type-safe +predicates created with `RealmQuery`: + +#### Java + +```java +// Build a RealmQuery based on the Student type +RealmQuery query = realm.where(Student.class); + +// Simple query +RealmResults studentsNamedJane = + query.rawPredicate("name = 'Jane'").findAll(); + +// Multiple predicates +RealmResults studentsNamedJaneOrJohn = + query.rawPredicate("name = 'Jane' OR name = 'John'").findAll(); + +// Collection queries +RealmResults studentsWithTeachers = + query.rawPredicate("teacher.@count > 0").findAll(); +RealmResults studentsWithSeniorTeachers = + query.rawPredicate("ALL teacher.numYearsTeaching > 5").findAll(); + +// Sub queries +RealmResults studentsWithMathTeachersNamedSteven = + query.rawPredicate("SUBQUERY(teacher, $teacher, $teacher.subject = 'Mathematics' AND $teacher.name = 'Mr. Stevens').@count > 0").findAll(); + +// Sort, Distinct, Limit +RealmResults students = + query.rawPredicate("teacher.@count > 0 SORT(year ASCENDING) DISTINCT(name) LIMIT(5)").findAll(); + +// Combine two raw predicates +RealmResults studentsNamedJaneOrHenry = + query.rawPredicate("name = 'Jane'") + .rawPredicate("name = 'Henry'").findAll(); + +// Combine raw predicate with type-safe predicate +RealmResults studentsNamedJaneOrHenryAgain = + query.rawPredicate("name = 'Jane'") + .equalTo("name", "Henry").findAll(); + +``` + +#### Kotlin + +```kotlin +// Build a RealmQuery based on the Student type +val query = realm.where(Student::class.java) + +// Simple query +val studentsNamedJane = query.rawPredicate("name = 'Jane'").findAll() + +// Multiple predicates +val studentsNamedJaneOrJohn = + query.rawPredicate("name = 'Jane' OR name = 'John'").findAll() + +// Collection queries +val studentsWithTeachers = + query.rawPredicate("teacher.@count > 0").findAll() +val studentsWithSeniorTeachers = + query.rawPredicate("ALL teacher.numYearsTeaching > 5").findAll() + +// Sub queries +val studentsWithMathTeachersNamedSteven = + query.rawPredicate("SUBQUERY(teacher, \$teacher, \$teacher.subject = 'Mathematics' AND \$teacher.name = 'Mr. Stevens').@count > 0") + .findAll() + +// Sort, Distinct, Limit +val students = + query.rawPredicate("teacher.@count > 0 SORT(year ASCENDING) DISTINCT(name) LIMIT(5)") + .findAll() + +// Combine two raw predicates +val studentsNamedJaneOrHenry = query.rawPredicate("name = 'Jane'") + .rawPredicate("name = 'Henry'").findAll() + +// Combine raw predicate with type-safe predicate +val studentsNamedJaneOrHenryAgain = + query.rawPredicate("name = 'Jane'") + .equalTo("name", "Henry").findAll() + +``` diff --git a/docs/guides/crud/read.md b/docs/guides/crud/read.md new file mode 100644 index 0000000000..1abd26b178 --- /dev/null +++ b/docs/guides/crud/read.md @@ -0,0 +1,608 @@ +# CRUD - Read - Java SDK +## Read Operations +You can read back the data that you have +stored in Realm. +The standard data access pattern across Realm +SDKs is to find, filter, and sort objects, in that order. To +get the best performance from Realm as your app grows and +your queries become more complex, design your app's data +access patterns around a solid understanding of Realm +read characteristics. + +### Read Characteristics +When you design your app's data access patterns around the +following three key characteristics of reads in Realm, +you can be confident you are reading data as +efficiently as possible. + +### Results Are Not Copies +Results to a query are not copies of your data: modifying +the results of a query will modify the data on disk +directly. This memory mapping also means that results are +**live**: that is, they always reflect the current state on +disk. + +### Results Are Lazy +Realm defers execution of a query until you access the +results. You can chain several filter and sort operations +without requiring extra work to process the intermediate +state. + +### References Are Retained +One benefit of Realm's object model is that +Realm automatically retains all of an object's +relationships as +direct references, so you can traverse your graph of +relationships directly through the results of a query. + +A **direct reference**, or pointer, allows you to access a +related object's properties directly through the reference. + +Other databases typically copy objects from database storage +into application memory when you need to work with them +directly. Because application objects contain direct +references, you are left with a choice: copy the object +referred to by each direct reference out of the database in +case it's needed, or just copy the foreign key for each +object and query for the object with that key if it's +accessed. If you choose to copy referenced objects into +application memory, you can use up a lot of resources for +objects that are never accessed, but if you choose to only +copy the foreign key, referenced object lookups can cause +your application to slow down. + +Realm bypasses all of this using zero-copy +live objects. Realm object accessors point directly into +database storage using memory mapping, so there is no distinction +between the objects in Realm and the results of your query in +application memory. Because of this, you can traverse direct references +across an entire realm from any query result. + +## About the Examples on this Page +The examples on this page use the data model of a project +management app that has two Realm object types: `Project` +and `Task`. A `Project` has zero or more `Tasks`. + +See the schema for these two classes, `Project` and +`Task`, below: + +#### Java + +```java + +import org.bson.types.ObjectId; + +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.RealmClass; +import io.realm.annotations.Required; + +public class ProjectTask extends RealmObject { + @PrimaryKey + public ObjectId _id; + @Required + public String name; + public String assignee; + public int progressMinutes; + public boolean isComplete; + public int priority; + @Required + public String _partition; +} + +``` + +```java + +import org.bson.types.ObjectId; + +import io.realm.RealmList; +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.RealmClass; +import io.realm.annotations.Required; + +public class Project extends RealmObject { + @PrimaryKey + public ObjectId _id; + @Required + public String name; + public RealmList tasks = new RealmList<>(); +} + +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import io.realm.annotations.Required +import org.bson.types.ObjectId + +open class ProjectTask( + @PrimaryKey + var _id: ObjectId = ObjectId(), + @Required + var name: String = "", + var assignee: String? = null, + var progressMinutes: Int = 0, + var isComplete: Boolean = false, + var priority: Int = 0, + var _partition: String = "" +): RealmObject() + +``` + +```kotlin +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import io.realm.annotations.Required +import org.bson.types.ObjectId + +open class Project( + @PrimaryKey + var _id: ObjectId = ObjectId(), + @Required + var name: String = "", + var tasks: RealmList = RealmList(), +): RealmObject() + +``` + +## Read from Realm +A read from a realm generally consists of the following +steps: + +- Get all objects of a certain type from the realm. +- Optionally, filter the results using the query engine. +- Optionally, sort the results. + +All query, filter, and sort operations return a +results collection. The results +collections are live, meaning they always contain the latest +results of the associated query. + +> Important: +> By default, you can only read or write to a realm in your +application's UI thread using +asynchronous transactions. That is, +you can only use `Realm` methods whose name ends with the word +`Async` in the main thread of your Android application unless you +explicitly allow the use of synchronous methods. +> +> This restriction exists for the benefit of your application users: +performing read and write operations on the UI thread can lead to +unresponsive or slow UI interactions, so it's usually best to handle +these operations either asynchronously or in a background thread. + +### Find a Specific Object by Primary Key +To find an object with a specific primary key value, open a realm +and query the primary key field for the desired primary key value +using the `RealmQuery.equalTo()` method: + +#### Java + +```java +ProjectTask task = realm.where(ProjectTask.class).equalTo("_id", PRIMARY_KEY_VALUE.get()).findFirst(); +Log.v("EXAMPLE", "Fetched object by primary key: " + task); + +``` + +#### Kotlin + +```kotlin +val task = realm.where(ProjectTask::class.java) + .equalTo("_id", ObjectId.get()).findFirst() +Log.v("EXAMPLE", "Fetched object by primary key: $task") + +``` + +### Query All Objects of a Given Type +The first step of any read is to **get all objects** of a +certain type in a realm. With this results collection, you +can operate on all instances on a type or filter and sort to +refine the results. + +In order to access all instances of `ProjectTask` and `Project`, use +the `where()` method +to specify a class: + +#### Java + +```java +RealmQuery tasksQuery = realm.where(ProjectTask.class); +RealmQuery projectsQuery = realm.where(Project.class); + +``` + +#### Kotlin + +```kotlin +val tasksQuery = realm.where(ProjectTask::class.java) +val projectsQuery = realm.where(Project::class.java) + +``` + +### Filter Queries Based on Object Properties +A **filter** selects a subset of results based on the +value(s) of one or more object properties. Realm provides a +full-featured query engine you +can use to define filters. The most common use case is to +find objects where a certain property matches a certain +value. Additionally, you can compare strings, aggregate over +collections of numbers, and use logical operators to build +up complex queries. + +In the following example, we use the query +engine's comparison operators to: + +- Find high priority tasks by comparing the value of the `priority` property value with a threshold number, above which priority can be considered high. +- Find just-started or short-running tasks by seeing if the `progressMinutes` property falls within a certain range. +- Find unassigned tasks by finding tasks where the `assignee` property is equal to null. +- Find tasks assigned to specific teammates Ali or Jamie by seeing if the `assignee` property is in a list of names. + +#### Java + +```java +RealmQuery tasksQuery = realm.where(ProjectTask.class); +Log.i("EXAMPLE", "High priority tasks: " + tasksQuery.greaterThan("priority", 5).count()); +Log.i("EXAMPLE", "Just-started or short tasks: " + tasksQuery.between("progressMinutes", 1, 10).count()); +Log.i("EXAMPLE", "Unassigned tasks: " + tasksQuery.isNull("assignee").count()); +Log.i("EXAMPLE", "Ali or Jamie's tasks: " + tasksQuery.in("assignee", new String[]{"Ali", "Jamie"}).count()); + +``` + +#### Kotlin + +```kotlin +val tasksQuery = realm.where(ProjectTask::class.java) +Log.i( + "EXAMPLE", "High priority tasks: " + tasksQuery.greaterThan( + "priority", + 5 + ).count() +) +Log.i( + "EXAMPLE", "Just-started or short tasks: " + tasksQuery.between( + "progressMinutes", + 1, + 10 + ).count() +) +Log.i( + "EXAMPLE", + "Unassigned tasks: " + tasksQuery.isNull("assignee").count() +) +Log.i( + "EXAMPLE", "Ali or Jamie's tasks: " + tasksQuery.`in`( + "assignee", arrayOf( + "Ali", + "Jamie" + ) + ).count() +) + +``` + +### Sort Query Results +A **sort** operation allows you to configure the order in +which Realm returns queried objects. You can sort based on +one or more properties of the objects in the results +collection. + +Realm only guarantees a consistent order of results when the +results are sorted. + +The following code sorts the projects by name in reverse +alphabetical order (i.e. "descending" order). + +#### Java + +```java +RealmQuery projectsQuery = realm.where(Project.class); +RealmResults results = projectsQuery.sort("name", Sort.DESCENDING).findAll(); + +``` + +#### Kotlin + +```kotlin +val projectsQuery = realm.where(Project::class.java) +val results = projectsQuery.sort("name", Sort.DESCENDING).findAll() + +``` + +### Query a Relationship +#### Java + +Consider the following relationship between classes `Human` and +`Cat`. This arrangement allows each human to own a single cat: + +```java +import org.bson.types.ObjectId; + +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; + +public class Human extends RealmObject { + @PrimaryKey + private ObjectId _id = new ObjectId(); + private String name; + private Cat cat; + + public Human(String name) { + this.name = name; + } + + public Human() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Cat getCat() { + return cat; + } + + public void setCat(Cat cat) { + this.cat = cat; + } + + public ObjectId get_id() { + return _id; + } +} + +``` + +```java +import org.bson.types.ObjectId; + +import io.realm.RealmObject; +import io.realm.RealmResults; +import io.realm.annotations.LinkingObjects; +import io.realm.annotations.PrimaryKey; + +public class Cat extends RealmObject { + @PrimaryKey + private ObjectId _id = new ObjectId(); + private String name = null; + @LinkingObjects("cat") + private final RealmResults owner = null; + public Cat(String name) { + this.name = name; + } + public Cat() { + } + + public ObjectId get_id() { + return _id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public RealmResults getOwner() { + return owner; + } +} + +``` + +To query this relationship, use dot notation in a +query to access any property +of the linked object. + +#### Kotlin + +Consider the following relationship between classes `Person` and +`Dog`. This arrangement allows each person to own a single dog: + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import org.bson.types.ObjectId + +open class Person(var name : String? = null) : RealmObject() { + @PrimaryKey + var _id : ObjectId = ObjectId() + var dog: Dog? = null +} + +``` + +```kotlin +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey +import org.bson.types.ObjectId + +open class Dog(var name : String? = null): RealmObject() { + @PrimaryKey + var _id : ObjectId = ObjectId() + @LinkingObjects("dog") + val owner: RealmResults? = null +} + +``` + +To query this relationship, use dot notation in a +query to access any property +of the linked object. + +### Query an Inverse Relationship +#### Java + +Consider the following relationship between classes `Cat` and +`Human`. In this example, all cats link to their human (or +multiple humans, if multiple human objects refer to the same cat). +Realm calculates the owners of each cat for you based on the field +name you provide to the `@LinkingObjects` annotation: + +```java +import org.bson.types.ObjectId; + +import io.realm.RealmObject; +import io.realm.RealmResults; +import io.realm.annotations.LinkingObjects; +import io.realm.annotations.PrimaryKey; + +public class Cat extends RealmObject { + @PrimaryKey + private ObjectId _id = new ObjectId(); + private String name = null; + @LinkingObjects("cat") + private final RealmResults owner = null; + public Cat(String name) { + this.name = name; + } + public Cat() { + } + + public ObjectId get_id() { + return _id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public RealmResults getOwner() { + return owner; + } +} +``` + +```java +import org.bson.types.ObjectId; + +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; + +public class Human extends RealmObject { + @PrimaryKey + private ObjectId _id = new ObjectId(); + private String name; + private Cat cat; + + public Human(String name) { + this.name = name; + } + + public Human() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Cat getCat() { + return cat; + } + + public void setCat(Cat cat) { + this.cat = cat; + } + + public ObjectId get_id() { + return _id; + } +} +``` + +To query this relationship, use dot notation in a +query to access any property +of the linked object. + +#### Kotlin + +Consider the following relationship between classes `Dog` and +`Person`. In this example, all dogs link to their owner (or +multiple owners, if multiple person objects refer to the same dog). +Realm calculates the owners of each dog for you based on the field +name you provide to the `@LinkingObjects` annotation: + +```kotlin +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey +import org.bson.types.ObjectId + +open class Dog(var name : String? = null): RealmObject() { + @PrimaryKey + var _id : ObjectId = ObjectId() + @LinkingObjects("dog") + val owner: RealmResults? = null +} +``` + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import org.bson.types.ObjectId + +open class Person(var name : String? = null) : RealmObject() { + @PrimaryKey + var _id : ObjectId = ObjectId() + var dog: Dog? = null +} +``` + +To query this relationship, use dot notation in a +query to access any property +of the linked object. + +### Aggregate Data +#### Java + +```java +RealmQuery tasksQuery = realm.where(ProjectTask.class); +/* +Aggregate operators do not support dot-notation, so you +cannot directly operate on a property of all of the objects +in a collection property. + +You can operate on a numeric property of the top-level +object, however: +*/ +Log.i("EXAMPLE", "Tasks average priority: " + tasksQuery.average("priority")); + +``` + +#### Kotlin + +```kotlin +val tasksQuery = realm.where(ProjectTask::class.java) +/* +Aggregate operators do not support dot-notation, so you +cannot directly operate on a property of all of the objects +in a collection property. + +You can operate on a numeric property of the top-level +object, however: +*/Log.i("EXAMPLE", "Tasks average priority: " + tasksQuery.average("priority")) + +``` + diff --git a/docs/guides/crud/threading.md b/docs/guides/crud/threading.md new file mode 100644 index 0000000000..04a97ea9fe --- /dev/null +++ b/docs/guides/crud/threading.md @@ -0,0 +1,290 @@ +# Threading - Java SDK +To make your Android apps fast and responsive, you must +balance the computing time needed to lay out the visuals and +handle user interactions with the time needed to process +your data and run your business logic. Typically, app +developers spread this work across multiple threads: the +main or UI thread for all of the user interface-related +work, and one or more background threads to compute heavier +workloads before sending it to the UI thread for +presentation. By offloading heavy work to background +threads, the UI thread can remain highly responsive +regardless of the size of the workload. + +## Three Rules to Keep in Mind +Realm enables simple and safe multithreaded code when you +follow these three rules: + +You can write to a +realm from any thread, but there can be only one +writer at a time. Consequently, write transactions block +each other. A write on the UI thread may result in your +app appearing unresponsive while it waits for a write on a +background thread to complete. + +Live objects, collections, and realm instances are +**thread-confined**: that is, they are only valid on the +thread on which they were created. Practically speaking, +this means you cannot pass live instances to other +threads. However, Realm offers several mechanisms for +sharing objects across threads. + +Realm's Multiversion Concurrency Control (MVCC) architecture eliminates the need to lock for read operations. The +values you read will never be corrupted or in a +partially-modified state. You can freely read from realms +on any thread without the need for locks or mutexes. +Unnecessarily locking would be a performance bottleneck +since each thread might need to wait its turn before +reading. + +## Communication Across Threads +Live objects, collections, and realms are **thread-confined**. If +you need to work with the same data across multiple threads, you should +open the same realm on multiple threads as separate realm +instances. The Java SDK consolidates underlying connections across +threads where possible to make this pattern +more efficient. + +When you need to communicate across threads, you have +several options depending on your use case: + +- To modify the data on two threads, query +for the object on both threads using a +primary key. +- To send a fast, read-only view of an object to other threads, +freeze the object. +- To keep and share many read-only views of the object in your app, copy +the object from the realm. +- To react to changes made on any thread, use +notifications. +- To see changes from other threads in the realm on the current +thread, refresh your realm +instance (event loop threads refresh automatically). + +### Intents +Managed `RealmObject` instances are +not thread-safe or `Parcelable`, so you cannot pass them between +activities or threads via an `Intent`. Instead, you can pass an object +identifier, like a primary key, +in the `Intent` extras bundle, and then open a new realm instance +in the separate thread to query for that identifier. Alternatively, you +can freeze Realm objects. + +> Seealso: +> You can find working examples in the [Passing Objects](https://github.com/realm/realm-java/blob/master/examples/threadExample/src/main/java/io/realm/examples/threads/PassingObjectsFragment.java) +portion of the [Java SDK Threading Example](https://github.com/realm/realm-java/tree/master/examples/threadExample). +The example shows you how to pass IDs and retrieve a `RealmObject` +in common Android use cases. +> + +### Frozen Objects +Live, thread-confined objects work fine in most cases. +However, some apps -- those based on reactive, event +stream-based architectures, for example -- need to send +immutable copies across threads. In this case, +you can **freeze** objects, collections, and realms. + +Freezing creates an immutable view of a specific object, +collection, or realm that still exists on disk and does not +need to be deeply copied when passed around to other +threads. You can freely share a frozen object across threads +without concern for thread issues. + +Frozen objects are not live and do not automatically update. They are +effectively snapshots of the object state at the time of freezing. When +you freeze a realm all child objects and collections also become +frozen. You can't modify frozen objects, but you can read the primary +key from a frozen object, query a live realm for the underlying +object, and then update that live object instance. + +Frozen objects remain valid for as long as the realm that +spawned them stays open. Avoid closing realms that contain frozen +objects until all threads are done working with those frozen objects. + +> Warning: +> When working with frozen objects, an attempt to do any of +the following throws an exception: +> +> - Opening a write transaction on a frozen realm. +> - Modifying a frozen object. +> - Adding a change listener to a frozen realm, collection, or object. +> + +Once frozen, you cannot unfreeze an object. You +can use `isFrozen()` to check if an object is frozen. +This method is always thread-safe. + +To freeze an object, collection, or realm, use the +`freeze()` method: + +#### Java + +```java +Realm realm = Realm.getInstance(config); + +// Get an immutable copy of the realm that can be passed across threads +Realm frozenRealm = realm.freeze(); +Assert.assertTrue(frozenRealm.isFrozen()); + +RealmResults frogs = realm.where(Frog.class).findAll(); +// You can freeze collections +RealmResults frozenFrogs = frogs.freeze(); +Assert.assertTrue(frozenFrogs.isFrozen()); + +// You can still read from frozen realms +RealmResults frozenFrogs2 = frozenRealm.where(Frog.class).findAll(); +Assert.assertTrue(frozenFrogs2.isFrozen()); + +Frog frog = frogs.first(); +Assert.assertTrue(!frog.getRealm().isFrozen()); + +// You can freeze objects +Frog frozenFrog = frog.freeze(); +Assert.assertTrue(frozenFrog.isFrozen()); +// Frozen objects have a reference to a frozen realm +Assert.assertTrue(frozenFrog.getRealm().isFrozen()); + +``` + +#### Kotlin + +```kotlin +val realm = Realm.getInstance(config) + +// Get an immutable copy of the realm that can be passed across threads +val frozenRealm = realm.freeze() +Assert.assertTrue(frozenRealm.isFrozen) +val frogs = realm.where(Frog::class.java).findAll() +// You can freeze collections +val frozenFrogs = frogs.freeze() +Assert.assertTrue(frozenFrogs.isFrozen) + +// You can still read from frozen realms +val frozenFrogs2 = + frozenRealm.where(Frog::class.java).findAll() +Assert.assertTrue(frozenFrogs2.isFrozen) +val frog: Frog = frogs.first()!! +Assert.assertTrue(!frog.realm.isFrozen) + +// You can freeze objects +val frozenFrog: Frog = frog.freeze() +Assert.assertTrue(frozenFrog.isFrozen) +Assert.assertTrue(frozenFrog.realm.isFrozen) + +``` + +> Important: +> Frozen objects preserve an entire copy of the realm that contains +them at the moment they were frozen. As a result, freezing a large +number of objects can cause a realm to consume more memory and +storage than it might have without frozen objects. If you need to +separately freeze a large number of objects for long periods of time, +consider copying what you need out of the realm instead. +> + +## Refreshing Realms +When you open a realm, it reflects the most recent successful write +commit and remains on that version until it is **refreshed**. This means +that the realm will not see changes that happened on another thread +until the next refresh. Realms on any event loop thread +(including the UI thread) automatically refresh themselves at the +beginning of that thread's loop. However, you must manually refresh +realm instances that are tied to non-looping threads or that have +auto-refresh disabled. To refresh a realm, call +`Realm.refresh()`: + +#### Java + +```java +if (!realm.isAutoRefresh()) { + // manually refresh + realm.refresh(); +} + +``` + +#### Kotlin + +```kotlin +if (!realm.isAutoRefresh) { + // manually refresh + realm.refresh() +} + +``` + +> Tip: +> Realms also automatically refresh after completing a write transaction. +> + +## Realm's Threading Model in Depth +Realm provides safe, fast, lock-free, and concurrent access +across threads with its [Multiversion Concurrency +Control (MVCC)](https://en.wikipedia.org/wiki/Multiversion_concurrency_control) +architecture. + +### Compared and Contrasted with Git +If you are familiar with a distributed version control +system like [Git](https://git-scm.com/), you may already +have an intuitive understanding of MVCC. Two fundamental +elements of Git are: + +- Commits, which are atomic writes. +- Branches, which are different versions of the commit history. + +Similarly, Realm has atomically-committed writes in the form +of transactions. Realm also has many +different versions of the history at any given time, like +branches. + +Unlike Git, which actively supports distribution and +divergence through forking, a realm only has one true latest +version at any given time and always writes to the head of +that latest version. Realm cannot write to a previous +version. This makes sense: your data should converge on one +latest version of the truth. + +### Internal Structure +A realm is implemented using a [B+ tree](https://en.wikipedia.org/wiki/B%2B_tree) data structure. The top-level node represents a +version of the realm; child nodes are objects in that +version of the realm. The realm has a pointer to its latest +version, much like how Git has a pointer to its HEAD commit. + +Realm uses a copy-on-write technique to ensure +[isolation](https://en.wikipedia.org/wiki/Isolation_(database_systems)) and +[durability](https://en.wikipedia.org/wiki/Durability_(database_systems)). +When you make changes, Realm copies the relevant part of the +tree for writing, then commits the changes in two phases: + +- Write changes to disk and verify success. +- Set the latest version pointer to point to the newly-written version. + +This two-step commit process guarantees that even if the +write failed partway, the original version is not corrupted +in any way because the changes were made to a copy of the +relevant part of the tree. Likewise, the realm's root +pointer will point to the original version until the new +version is guaranteed to be valid. + +Realm uses zero-copy techniques +like memory mapping to handle data. When you read a value +from the realm, you are virtually looking at the value on +the actual disk, not a copy of it. This is the basis for +live objects. This is also why a realm +head pointer can be set to point to the new version after +the write to disk has been validated. + +## Summary +- Realm enables simple and safe multithreaded code when you follow +these rules: Don't pass live objects to other threads, and don't lock to read. +- In order to see changes made on other threads in your realm +instance, you must manually **refresh** realm instances that do +not exist on "loop" threads or that have auto-refresh disabled. +- For apps based on reactive, event-stream-based architectures, you can +**freeze** objects, collections, and realms in order to pass +copies around efficiently to different threads for processing. +- Realm's multiversion concurrency control (MVCC) +architecture is similar to Git's. Unlike Git, Realm has +only one true latest version for each realm. +- Realm commits in two stages to guarantee isolation and +durability. diff --git a/docs/guides/crud/update.md b/docs/guides/crud/update.md new file mode 100644 index 0000000000..d014440d26 --- /dev/null +++ b/docs/guides/crud/update.md @@ -0,0 +1,365 @@ +# CRUD - Update - Java SDK +## About the Examples on this Page +The examples on this page use the data model of a project +management app that has two Realm object types: `Project` +and `Task`. A `Project` has zero or more `Tasks`. + +See the schema for these two classes, `Project` and +`Task`, below: + +#### Java + +```java + +import org.bson.types.ObjectId; + +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.RealmClass; +import io.realm.annotations.Required; + +public class ProjectTask extends RealmObject { + @PrimaryKey + public ObjectId _id; + @Required + public String name; + public String assignee; + public int progressMinutes; + public boolean isComplete; + public int priority; + @Required + public String _partition; +} + +``` + +```java + +import org.bson.types.ObjectId; + +import io.realm.RealmList; +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.RealmClass; +import io.realm.annotations.Required; + +public class Project extends RealmObject { + @PrimaryKey + public ObjectId _id; + @Required + public String name; + public RealmList tasks = new RealmList<>(); +} + +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import io.realm.annotations.Required +import org.bson.types.ObjectId + +open class ProjectTask( + @PrimaryKey + var _id: ObjectId = ObjectId(), + @Required + var name: String = "", + var assignee: String? = null, + var progressMinutes: Int = 0, + var isComplete: Boolean = false, + var priority: Int = 0, + var _partition: String = "" +): RealmObject() + +``` + +```kotlin +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey +import io.realm.annotations.Required +import org.bson.types.ObjectId + +open class Project( + @PrimaryKey + var _id: ObjectId = ObjectId(), + @Required + var name: String = "", + var tasks: RealmList = RealmList(), +): RealmObject() + +``` + +## Modify an Object +Within a transaction, you can update a Realm object the same +way you would update any other object in your language of +choice. Just assign a new value to the property or update +the property. + +The following example changes the turtle's name to "Archibald" and +sets Archibald's age to 101 by assigning new values to properties: + +#### Java + +```java +realm.executeTransaction(r -> { + // Get a turtle to update. + Turtle turtle = r.where(Turtle.class).findFirst(); + // Update properties on the instance. + // This change is saved to the realm. + turtle.setName("Archibald"); + turtle.setAge(101); +}); + +``` + +#### Kotlin + +```kotlin +realm.executeTransaction { r: Realm -> + // Get a turtle to update. + val turtle = r.where(Turtle::class.java).findFirst() + // Update properties on the instance. + // This change is saved to the realm. + turtle!!.name = "Archibald" + turtle.age = 101 +} + +``` + +## Upsert an Object +An **upsert** is a write operation that either inserts a new object +with a given primary key or updates an existing object that already has +that primary key. We call this an upsert because it is an "**update** or +**insert**" operation. This is useful when an object may or may not +already exist, such as when bulk importing a dataset into an existing +realm. Upserting is an elegant way to update existing entries while +adding any new entries. + +The following example demonstrates how to upsert an object with +realm. We create a new turtle enthusiast named "Drew" and then +update their name to "Andy" using `insertOrUpdate()`: + +#### Java + +```java +realm.executeTransaction(r -> { + ObjectId id = new ObjectId(); + TurtleEnthusiast drew = new TurtleEnthusiast(); + drew.set_id(id); + drew.setName("Drew"); + drew.setAge(25); + // Add a new turtle enthusiast to the realm. Since nobody with this id + // has been added yet, this adds the instance to the realm. + r.insertOrUpdate(drew); + TurtleEnthusiast andy = new TurtleEnthusiast(); + andy.set_id(id); + andy.setName("Andy"); + andy.setAge(56); + // Judging by the ID, it's the same turtle enthusiast, just with a different name. + // As a result, you overwrite the original entry, renaming "Drew" to "Andy". + r.insertOrUpdate(andy); +}); + +``` + +#### Kotlin + +```kotlin +realm.executeTransaction { r: Realm -> + val id = ObjectId() + val drew = TurtleEnthusiast() + drew._id = id + drew.name = "Drew" + drew.age = 25 + // Add a new turtle enthusiast to the realm. Since nobody with this id + // has been added yet, this adds the instance to the realm. + r.insertOrUpdate(drew) + val andy = TurtleEnthusiast() + andy._id = id + andy.name = "Andy" + andy.age = 56 + // Judging by the ID, it's the same turtle enthusiast, just with a different name. + // As a result, you overwrite the original entry, renaming "Drew" to "Andy". + r.insertOrUpdate(andy) +} + +``` + +You can also use `copyToRealmOrUpdate()` to +either create a new object based on a supplied object or update an +existing object with the same primary key value. Use the +`CHECK_SAME_VALUES_BEFORE_SET` +`ImportFlag` to only update fields +that are different in the supplied object: + +The following example demonstrates how to insert an object or, if an object already +exists with the same primary key, update only those fields that differ: + +#### Java + +```java +realm.executeTransaction(r -> { + ObjectId id = new ObjectId(); + TurtleEnthusiast drew = new TurtleEnthusiast(); + drew.set_id(id); + drew.setName("Drew"); + drew.setAge(25); + // Add a new turtle enthusiast to the realm. Since nobody with this id + // has been added yet, this adds the instance to the realm. + r.insertOrUpdate(drew); + TurtleEnthusiast andy = new TurtleEnthusiast(); + andy.set_id(id); + andy.setName("Andy"); + // Judging by the ID, it's the same turtle enthusiast, just with a different name. + // As a result, you overwrite the original entry, renaming "Drew" to "Andy". + // the flag passed ensures that we only write the updated name field to the db + r.copyToRealmOrUpdate(andy, ImportFlag.CHECK_SAME_VALUES_BEFORE_SET); +}); + +``` + +#### Kotlin + +```kotlin +realm.executeTransaction { r: Realm -> + val id = ObjectId() + val drew = TurtleEnthusiast() + drew._id = id + drew.name = "Drew" + drew.age = 25 + // Add a new turtle enthusiast to the realm. Since nobody with this id + // has been added yet, this adds the instance to the realm. + r.insertOrUpdate(drew) + val andy = TurtleEnthusiast() + andy._id = id + andy.name = "Andy" + // Judging by the ID, it's the same turtle enthusiast, just with a different name. + // As a result, you overwrite the original entry, renaming "Drew" to "Andy". + r.copyToRealmOrUpdate(andy, + ImportFlag.CHECK_SAME_VALUES_BEFORE_SET) +} + +``` + +## Update a Collection +Realm supports collection-wide updates. A collection update +applies the same update to specific properties of several +objects in a collection at once. + +The following example demonstrates how to update a +collection. Thanks to the implicit inverse +relationship between the Turtle's +`owner` property and the TurtleEnthusiast's `turtles` property, +Realm automatically updates Josephine's list of turtles +when you use `setObject()` +to update the "owner" property for all turtles in the collection. + +#### Java + +```java +realm.executeTransaction(r -> { + // Create a turtle enthusiast named Josephine. + TurtleEnthusiast josephine = r.createObject(TurtleEnthusiast.class, new ObjectId()); + josephine.setName("Josephine"); + + // Get all turtles named "Pierogi". + RealmResults turtles = r.where(Turtle.class).equalTo("name", "Pierogi").findAll(); + + // Give all turtles named "Pierogi" to Josephine + turtles.setObject("owner", josephine); +}); + +``` + +#### Kotlin + +```kotlin +realm.executeTransaction { r: Realm -> + // Create a turtle enthusiast named Josephine. + val josephine = realm.createObject( + TurtleEnthusiast::class.java, + ObjectId() + ) + josephine.name = "Josephine" + + // Get all turtles named "Pierogi". + val turtles = r.where(Turtle::class.java) + .equalTo("name", "Pierogi") + .findAll() + + // Give all turtles named "Pierogi" to Josephine + turtles.setObject("owner", josephine) +} + +``` + +## Iteration +Because realm collections always reflect the latest state, they +can appear, disappear, or change while you iterate over a collection. +To get a stable collection you can iterate over, you can create a +**snapshot** of a collection's data. A snapshot guarantees the order of +elements will not change, even if an element is deleted or modified. + +`Iterator` objects created from `RealmResults` use snapshots +automatically. `Iterator` objects created from `RealmList` +instances do *not* use snapshots. Use +`RealmList.createSnapshot()` +or +`RealmResults.createSnapshot()` +to manually generate a snapshot you can iterate over manually: + +The following example demonstrates how to iterate over a collection +safely using either an implicit snapshot created from a `RealmResults` +`Iterator` or a manual snapshot created from a `RealmList`: + +#### Java + +```java +RealmResults frogs = realm.where(Frog.class) + .equalTo("species", "bullfrog") + .findAll(); + +// Use an iterator to rename the species of all bullfrogs +realm.executeTransaction(r -> { + for (Frog frog : frogs) { + frog.setSpecies("Lithobates catesbeiana"); + } +}); + +// Use a snapshot to rename the species of all bullfrogs +realm.executeTransaction(r -> { + OrderedRealmCollectionSnapshot frogsSnapshot = frogs.createSnapshot(); + for (int i = 0; i < frogsSnapshot.size(); i++) { + frogsSnapshot.get(i).setSpecies("Lithobates catesbeiana"); + } +}); + +``` + +#### Kotlin + +```kotlin +val frogs = realm.where(Frog::class.java) + .equalTo("species", "bullfrog") + .findAll() + +// Use an iterator to rename the species of all bullfrogs +realm.executeTransaction { + for (frog in frogs) { + frog.species = "Lithobates catesbeiana" + } +} + +// Use a snapshot to rename the species of all bullfrogs +realm.executeTransaction { + val frogsSnapshot = frogs.createSnapshot() + for (i in frogsSnapshot.indices) { + frogsSnapshot[i]!!.species = "Lithobates catesbeiana" + } +} + +``` + diff --git a/docs/guides/install.md b/docs/guides/install.md new file mode 100644 index 0000000000..376e9b6608 --- /dev/null +++ b/docs/guides/install.md @@ -0,0 +1,205 @@ +# Install Realm - Java SDK +> Note: +> The Java SDK is in best-effort maintenance mode and **no longer receives +new development or non-critical bug fixes. To develop your app with new +features, use the Kotlin SDK. You can use the Java SDK +with the Kotlin SDK in the same project. +> +> Learn more about how to Migrate from the Java SDK to the Kotlin SDK. +> + +## Overview +This page details how to install Realm using the Java SDK in your project +and get started. + +You can use multiple SDKs in your project. Because the Java SDK is no longer +receiving new development, this is useful if you want to +use new features in your app. + +## Prerequisites +- [Android Studio](https://developer.android.com/studio/index.html) version 1.5.1 or higher. +- Java Development Kit (JDK) 11 or higher. +- An emulated or hardware Android device for testing. +- Android API Level 16 or higher (Android 4.1 and above). + +## Installation +Realm only supports the Gradle build system. Follow these steps +to add the Realm Java SDK to your project. + +> Note: +> Because Realm provides a ProGuard configuration as part +of the Realm library, you do not need to add any +Realm-specific rules to your ProGuard configuration. +> + +### Project Gradle Configuration +To add local realm to your application, make +the following changes to your project-level Gradle build +file, typically found at /build.gradle: + +#### Gradle Plugin + +> Tip: +> The Java SDK does not yet support the Gradle Plugin syntax. Fortunately, +you can still add the SDK to projects that use this syntax. +> + +- Add a `buildscript` block that contains a `repositories` block and a `dependencies` block. +- Add the `mavenCentral()` repository to the `buildscript.repositories` block. +- Add the `io.realm:realm-gradle-plugin` dependency to the `buildscript.dependencies` block. + +```groovy +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "io.realm:realm-gradle-plugin:10.18.0" + } +} + +plugins { + id 'com.android.application' version '7.1.2' apply false + id 'com.android.library' version '7.1.2' apply false + id 'org.jetbrains.kotlin.android' version '1.6.10' apply false + id "org.jetbrains.kotlin.kapt" version "1.6.20" apply false +} + +task clean(type: Delete) { + delete rootProject.buildDir +} +``` + +#### Gradle Legacy + +- Add the `io.realm:realm-gradle-plugin` dependency to the `buildscript.dependencies` block. +- Add the `mavenCentral()` repository to the `allprojects.repositories` block. + +```groovy +buildscript { + repositories { + google() + } + dependencies { + classpath "com.android.tools.build:gradle:3.5.1" + classpath "io.realm:realm-gradle-plugin:10.18.0" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } + dependencies { + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} +``` + +### Application Module Gradle Configuration +Then, make the following changes to your application-level +Gradle build file, typically found at /app/build.gradle: + +#### Gradle Plugin + +- Apply the `kotlin-kapt` plugin if your application uses Kotlin +- Beneath the `plugins` block, apply the `realm-android` plugin. + +```groovy +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.kotlin.kapt' +} + +apply plugin: "realm-android" + +android { + compileSdk 31 + defaultConfig { + applicationId "com.mongodb.example-realm-application" + minSdk 28 + targetSdk 31 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_11 + targetCompatibility JavaVersion.VERSION_11 + } + kotlinOptions { + jvmTarget = '11' + } +} + +dependencies { + implementation 'io.realm:realm-gradle-plugin:10.10.1' + implementation 'androidx.core:core-ktx:1.7.0' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.android.material:material:1.5.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.3' + testImplementation 'junit:junit:4.13.2' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} +``` + +#### Gradle Legacy + +- Apply the `kotlin-kapt` plugin if your application uses Kotlin +- Apply the `realm-android` plugin + +```groovy +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'realm-android' + +android { + compileSdkVersion 31 + buildToolsVersion "29.0.2" + defaultConfig { + applicationId "com.mongodb.example-realm-application" + minSdkVersion 28 + targetSdkVersion 31 + } + compileOptions { + sourceCompatibility 1.11 + targetCompatibility 1.11 + } + kotlinOptions { + jvmTarget = '11' + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation "androidx.appcompat:appcompat:1.1.0" + implementation "androidx.core:core-ktx:1.2.0" +} +``` + +After updating the `build.gradle` files, resolve the dependencies by +clicking File > Sync Project with Gradle Files. + +## Supported Platforms +Realm's Java SDK enables you to build apps for the +following platforms: + +- Android +- Wear OS +- Android Automotive OS +- Android TV +- Android Things diff --git a/docs/guides/model-data.md b/docs/guides/model-data.md new file mode 100644 index 0000000000..58e51b3608 --- /dev/null +++ b/docs/guides/model-data.md @@ -0,0 +1,152 @@ +# Model Data - Java SDK + +An **object schema** is a configuration object that defines the fields and +relationships of a Realm object type. Android +Realm applications define object schemas with Java or Kotlin +classes using Realm Schemas. + +Object schemas specify constraints on object fields such as the data +type of each field, whether a +field is required, and default field values. Schemas can also define +relationships between object types in +a realm. + +Modifying your application's Realm Schema requires you to +migrate data from older +versions of your Realm Schema to the new version. + +## Realm Apps +Every App has a Realm Schema +composed of a list of object schemas for each type of object that the +realms in that application may contain. + +Realm guarantees that all objects in a realm conform to the +schema for their object type and validates objects whenever they're +created, modified, or deleted. + +## Relationships +You can model **one-to-one** relationships in realm with +`RealmObject` fields. +You can model **one-to-many** and **many-to-one** relationships +`RealmList` fields. +Inverse relationships are the opposite end of a **one-to-many** or +**many-to-one** relationship. +You can make **inverse** relationships traversable with the +`@LinkingObjects` +annotation on a `RealmResults` +field. In an instance of a `RealmObject`, inverse relationship fields +contain the set of Realm objects that point to that object +instance through the described relationship. You can find the same set +of Realm objects with a manual query, but the inverse +relationship field reduces boilerplate query code and capacity for error. + +## Realm Objects +Unlike normal Java objects, which contain their own data, a +Realm object doesn't contain data. Instead, +Realm objects read and write properties directly to +Realm. + +Instances of Realm objects can be either **managed** or **unmanaged**. + +- **Managed** objects are: persisted in Realmalways up to datethread-confinedgenerally more lightweight than the unmanaged version, as they take +up less space on the Java heap. +- **Unmanaged** objects are just like ordinary Java objects, since +they are not persisted and never update automatically. +You can move unmanaged objects freely across threads. + +You can convert between the two states using +`realm.copyToRealm()` +and `realm.copyFromRealm()`. + +### RealmProxy +The `RealmProxy` classes are the Realm SDK's way of +ensuring that Realm objects don't contain any data +themselves. Instead, each class's `RealmProxy` accesses data directly +in the database. + +For every model class in your project, the Realm annotation +processor generates a corresponding `RealmProxy` class. This class +extends your model class and is returned when you call +`Realm.createObject()`. In your code, this object works just like your +model class. + +### Realm Object Limitations +Realm objects: + +- cannot contain fields that use the `final` or `volatile` modifiers +(except for inverse relationship +fields). +- cannot extend any object other than `RealmObject`. +- must contain an empty constructor (if your class does not include any +constructor, the automatically generated empty constructor will suffice) + +Naming limitations: + +- Class names cannot exceed 57 characters. +- Class names must be unique within realm modules +- Field names cannot exceed 63 characters. + +Size limitations: + +- `String` or `byte[]` fields cannot exceed 16 MB. + +Usage limitations: + +- Because Realm objects are live and can change at any time, +their `hashCode()` value can change over time. As a result, you +should not use `RealmObject` instances as a key in any map or set. + +## Incremental Builds +The bytecode transformer used by Realm supports incremental +builds, but your application requires a full rebuild when adding or +removing the following from a Realm object field: + +- an `@Ignore` annotation +- the `static` keyword +- the `transient` keyword + +You can perform a full rebuild with Build > Clean Project +and Build > Rebuild Project in these cases. + +## Schema Version +A **schema version** identifies the state of a Realm Schema at some point in time. Realm tracks the schema +version of each realm and uses it to map the objects in each realm +to the correct schema. + +Schema versions are integers that you may include +in the realm configuration when you open a realm. If a client +application does not specify a version number when it opens a realm then +the realm defaults to version `0`. + +> Important: +> Migrations must update a realm to a +higher schema version. Realm throws an error if a client +application opens a realm with a schema version that is lower than +the realm's current version or if the specified schema version is the +same as the realm's current version but includes different +object schemas. +> + +## Migrations +A **local migration** is a migration for a realm with +another realm. Local migrations have access to the existing +Realm Schema, version, and objects and define logic that +incrementally updates the realm to its new schema version. +To perform a local migration you must specify a new schema +version that is higher than the current version and provide +a migration function when you open the out-of-date realm. + +With the SDK, you can update underlying data to reflect schema changes +using manual migrations. During such a manual migration, you can +define new and deleted properties when they are added or removed from +your schema. The editable schema exposed via a +`DynamicRealm` provides +convenience functions for renaming fields. This gives you full control +over the behavior of your data during complex schema migrations. + +> Tip: +> During development of an application, `RealmObject` classes can +change frequently. You can use `Realm.deleteRealm()`to +delete the database file and eliminate the need to write a full +migration for testing data. +> diff --git a/docs/guides/model-data/data-types.md b/docs/guides/model-data/data-types.md new file mode 100644 index 0000000000..e5389af2c6 --- /dev/null +++ b/docs/guides/model-data/data-types.md @@ -0,0 +1,14 @@ +# Realm Data Types - Java SDK + +Explore detailed guidance for each Realm Java SDK data type and related concepts: + +- [Field Types](./data-types/field-types.md) +- [Collections](./data-types/collections.md) +- [Counters](./data-types/counters.md) +- [Dictionaries](./data-types/realmdictionary.md) +- [Sets](./data-types/realmset.md) +- [Mixed (RealmAny)](./data-types/realmany.md) +- [Enums](./data-types/enums.md) +- [Embedded Objects](./data-types/embedded-objects.md) + +Each linked page covers usage details, constraints, and example code where applicable. diff --git a/docs/guides/model-data/data-types/collections.md b/docs/guides/model-data/data-types/collections.md new file mode 100644 index 0000000000..6e24a9277d --- /dev/null +++ b/docs/guides/model-data/data-types/collections.md @@ -0,0 +1,171 @@ +# Collections - Java SDK +A Realm collection is an +object that contains zero or more instances of one +type. Realm collections +are homogenous, i.e. all objects in a collection are of the +same type. + +You can filter and sort any collection using Realm's +query engine. Collections are +live, so they always reflect the +current state of the realm instance on the current +thread. You can also listen for changes in the collection by subscribing +to collection notifications. + +Realm has two kinds of collections: **lists** and **results**. + +## Lists +Realm objects can contain lists of non-Realm-object data +types. You can model these collections with the type `RealmList`, +where `T` can be the following types: + +- `String` +- `Integer` +- `UUID` +- `ObjectId` +- `Boolean` +- `Float` +- `Double` +- `Short` +- `Long` +- `Byte` +- `byte[]` +- `Date` + +> Seealso: +> Lists +> + +### List Collections +A **list collection** represents a to-many +relationship between two Realm +types. Lists are mutable: within a write transaction, you +can add and remove elements on a list. Lists are not +associated with a query. + +### Results Collections +A **results collection** represents the lazily-evaluated +results of a query operation. Results are immutable: you +cannot add or remove elements on the results collection. +Results have an associated query that determines their +contents. + +The `RealmResults` class inherits from +[AbstractList](https://developer.android.com/reference/java/util/AbstractList) and behaves +in similar ways. For example, `RealmResults` are ordered, and you can +access the individual objects through an index. If a query has no +matches, the returned `RealmResults` object will be a list of length +0, not a `null` object reference. + +You can only modify or delete objects in a `RealmResults` set +in a write transaction. + +## Iteration +Because Realm collections are live, objects may move as you +iterate over a collection. You can use +snapshots to iterate over collections safely. + +## Adapters +Realm offers adapters to help bind data +to standard UI widgets. These classes work with any class that +implements the `OrderedRealmCollection` interface, which includes +the built-in `RealmResults` and `RealmList` classes. For more +information on adapters, see the documentation on +Displaying Collections. + +> Important: +> The Realm adapters only accept *managed* +Realm object instances tied to an instance of a realm. +To display non-managed objects, use the general-use Android +`RecyclerView.Adapter` for recycler views or `ArrayAdapter` for +list views. +> + +## Collections are Live +Like live objects, Realm collections +are usually **live**: + +- Live results collections always reflect the current results of the associated query. +- Live lists always reflect the current state of the relationship on the realm instance. + +There are three cases when a collection is **not** live: + +- The collection is unmanaged, e.g. a List property of a Realm object that has not been added to a realm yet or that has been copied from a realm. +- The collection is frozen. +- The collection is part of a snapshot. + +Combined with collection notifications, live collections enable clean, +reactive code. For example, suppose your view displays the +results of a query. You can keep a reference to the results +collection in your view class, then read the results +collection as needed without having to refresh it or +validate that it is up-to-date. + +> Warning: +> Results update themselves automatically. If you +store the positional index of an object in a collection +or the count of objects in a collection, the stored index +or count value could be outdated by the time you use it. +> + +## Results are Lazily Evaluated +Realm only runs a query when you actually request the +results of that query, e.g. by accessing elements of the +results collection. This lazy evaluation enables you to +write elegant, highly performant code for handling large +data sets and complex queries. + +### Limiting Query Results +As a result of lazy evaluation, you do not need any special +mechanism to limit query results with Realm. For example, if +your query matches thousands of objects, but you only want +to load the first ten, simply access only the first ten +elements of the results collection. + +### Pagination +Thanks to lazy evaluation, the common task of pagination +becomes quite simple. For example, suppose you have a +results collection associated with a query that matches +thousands of objects in your realm. You display one hundred +objects per page. To advance to any page, simply access the +elements of the results collection starting at the index +that corresponds to the target page. + +## List vs. Results +When you need a collection, you can use the following rule +of thumb to determine whether a list or a results collection +is appropriate: + +- When you define the properties of your Realm objects, use lists to define to-many relationships except implicit inverse relationships. +- Use results everywhere else. + +To understand these different use cases, consider whether +you should be able to add or remove objects directly. Lists +allow you to add and remove objects directly, because you +control the relationships. Results collections do not allow +you to add or remove objects directly, because their contents +are determined by a query. + +> Example: +> Consider a Realm type called Person with a field called +`emails` that is a collection of strings representing +email addresses. You control this data. Your application +needs to add and remove email addresses from your Person +instances. Therefore, use a **list** to define the field +type of `emails`. +> +> On the other hand, when you query the realm for all +Persons over the age of 25, it would not make sense for +you to add or remove Persons directly to the resulting +collection. The contents of that collection only change +when the query matches a different set of Persons. +Therefore, Realm gives you a **results** collection. +> + +> Note: +> Since Realm automatically determines the contents of +implicit inverse relationship collections, you may not add +or remove objects from such a collection. +Therefore, the type of such a one-to-many relationship +property is actually a results collection, not a list. +> diff --git a/docs/guides/model-data/data-types/counters.md b/docs/guides/model-data/data-types/counters.md new file mode 100644 index 0000000000..1f4934f8c4 --- /dev/null +++ b/docs/guides/model-data/data-types/counters.md @@ -0,0 +1,142 @@ +# Counters - Java SDK +Realm offers `MutableRealmInteger`, a wrapper around numeric values, +to help better synchronize numeric changes across multiple clients. + +Typically, incrementing or decrementing a +`byte`, `short`, `int`, or `long` field of a Realm +object looks something like this: + +1. Read the current value of the field. +2. Update that value in memory to a new value based on the increment or +decrement. +3. Write a new value back to the field. + +When multiple distributed clients attempt this at the same time, +updates reaching clients in different orders can +result in different values on different clients. `MutableRealmInteger` +improves on this by translating numeric updates into sync operations +that can be executed in any order to converge to the same value. + +`MutableRealmInteger` fields are backed by traditional numeric types, +so no migration is required when changing a field from `byte`, `short`, +`int` or `long` to `MutableRealmInteger`. + +The following example demonstrates a `MutableRealmInteger` field that +counts the number of ghosts found in a haunted house: + +#### Java + +```java +import io.realm.MutableRealmInteger; +import io.realm.RealmObject; +import io.realm.annotations.Required; + +public class HauntedHouse extends RealmObject { + @Required + private final MutableRealmInteger ghosts = MutableRealmInteger.valueOf(0); + public HauntedHouse() {} + public MutableRealmInteger getGhosts() { return ghosts; } +} + +``` + +#### Kotlin + +```kotlin +import io.realm.MutableRealmInteger +import io.realm.RealmObject +import io.realm.annotations.Required + +open class HauntedHouse: RealmObject() { + @Required + val ghosts: MutableRealmInteger = MutableRealmInteger.valueOf(0) +} + +``` + +> Important: +> `MutableRealmInteger` is a live object like `RealmObject`, +`RealmResults` and `RealmList`. This means the value contained +inside the `MutableRealmInteger` can change when a realm is +written to. For this reason `MutableRealmInteger` fields must be +marked final in Java and `val` in Kotlin. +> + +## Usage +The `counter.increment()` +and `counter.decrement()` +operators ensure that increments and decrements from multiple distributed +clients are aggregated correctly. + +To change a `MutableRealmInteger` value, call `increment()` or +`decrement()` within a write transaction: + +#### Java + +```java +HauntedHouse house = realm.where(HauntedHouse.class) + .findFirst(); +realm.executeTransaction(r -> { + Log.v("EXAMPLE", "Number of ghosts: " + house.getGhosts().get()); // 0 + house.getGhosts().increment(1); + Log.v("EXAMPLE", "Number of ghosts: " + house.getGhosts().get()); // 1 + house.getGhosts().increment(5); + Log.v("EXAMPLE", "Number of ghosts: " + house.getGhosts().get()); // 6 + house.getGhosts().decrement(2); + Log.v("EXAMPLE", "Number of ghosts: " + house.getGhosts().get()); // 4 +}); + +``` + +#### Kotlin + +```kotlin +val house = realm.where(HauntedHouse::class.java) + .findFirst()!! +realm.executeTransaction { + Log.v("EXAMPLE", "Number of ghosts: ${house.ghosts.get()}") // 0 + house.ghosts.increment(1) + Log.v("EXAMPLE", "Number of ghosts: ${house.ghosts.get()}") // 1 + house.ghosts.increment(5) + Log.v("EXAMPLE", "Number of ghosts: ${house.ghosts.get()}") // 6 + house.ghosts.decrement(2) + Log.v("EXAMPLE", "Number of ghosts: ${house.ghosts.get()}") // 4 +} + +``` + +You can assign a `MutableRealmInteger` a new value with a call to +`counter.set()` +within a write transaction. + +> Warning: +> Use the `set()` operator with extreme care. `set()` ignores +the effects of any prior calls to `increment()` or `decrement()`. +Although the value of a `MutableRealmInteger` always converges +across devices, the specific value on which it converges depends on +the actual order in which operations took place. +Mixing `set()` with `increment()` and `decrement()` is +not advised unless fuzzy counting is acceptable. +> + +#### Java + +```java +realm.executeTransaction(r -> { + house.getGhosts().set(42); +}); + +``` + +#### Kotlin + +```kotlin +realm.executeTransaction { + house!!.ghosts.set(42) +} + +``` + +Since `MutableRealmInteger` instances retain a reference to their +parent object, neither object can be garbage collected while you still +retain a reference to the `MutableRealmInteger`. diff --git a/docs/guides/model-data/data-types/embedded-objects.md b/docs/guides/model-data/data-types/embedded-objects.md new file mode 100644 index 0000000000..15b9bf3f6c --- /dev/null +++ b/docs/guides/model-data/data-types/embedded-objects.md @@ -0,0 +1,326 @@ +# Embedded Objects - Java SDK +An embedded object is a special type of Realm object that models complex data about a specific object. +Embedded objects are similar to relationships, but they provide additional constraints and +map more naturally to the denormalized document model. + +Realm enforces unique ownership constraints that treat each embedded +object as nested data inside of a single, specific parent object. An +embedded object inherits the lifecycle of its parent object and cannot +exist as an independent Realm object. Realm automatically deletes +embedded objects if their parent object is deleted or when overwritten +by a new embedded object instance. + +> Warning: +> When you delete a Realm object, Realm automatically deletes any +embedded objects referenced by that object. Any objects that your +application must persist after the deletion of their parent object +should use relationships +instead. +> + +## Embedded Object Data Models +You can define embedded object types using either Realm object models or +a server-side document schema. Embedded object types are reusable and +composable. You can use the same embedded object type in multiple parent +object types and you can embed objects inside of other embedded objects. + +> Important: +> Embedded objects cannot have a primary key. +> + +### Realm Object Models +To define an embedded object, derive a class from `RealmObject` and set the `embedded` property of the +`RealmClass` annotation +to `true`. You can reference an embedded object type from parent +object types in the same way as you would define a relationship: + +#### Java + +```java +// Define an embedded object +@RealmClass(embedded = true) +public class Address extends RealmObject { + String street; + String city; + String country; + String postalCode; + + public Address(String street, String city, String country, String postalCode) { + this.street = street; + this.city = city; + this.country = country; + this.postalCode = postalCode; + } + + public Address() {} +} + +// Define an object containing one embedded object +public class Contact extends RealmObject { + @PrimaryKey + private ObjectId _id = new ObjectId(); + String name = ""; + + // Embed a single object. + // Embedded object properties must be marked optional + Address address; + + public Contact(String name, Address address) { + this.name = name; + this.address = address; + } + + public Contact() {} +} + +// Define an object containing an array of embedded objects +public class Business extends RealmObject { + @PrimaryKey + private ObjectId _id = new ObjectId(); + String name = ""; + + // Embed an array of objects + RealmList
addresses = new RealmList
(); + + public Business(String name, RealmList
addresses) { + this.name = name; + this.addresses = addresses; + } + + public Business() {} +} + +``` + +#### Kotlin + +```kotlin +// Define an embedded object +@RealmClass(embedded = true) +open class Address( + var street: String? = null, + var city: String? = null, + var country: String? = null, + var postalCode: String? = null +): RealmObject() {} + +// Define an object containing one embedded object +open class Contact(_name: String = "", _address: Address? = null) : RealmObject() { + @PrimaryKey var _id: ObjectId = ObjectId() + var name: String = _name + + // Embed a single object. + // Embedded object properties must be marked optional + var address: Address? = _address +} + +// Define an object containing an array of embedded objects +open class Business(_name: String = "", _addresses: RealmList
= RealmList()) : RealmObject() { + @PrimaryKey var _id: ObjectId = ObjectId() + var name: String = _name + + // Embed an array of objects + var addresses: RealmList
= _addresses +} + +``` + +### JSON Schema +Embedded objects map to embedded documents in the parent type's schema. + +```json +{ + "title": "Contact", + "bsonType": "object", + "required": ["_id"], + "properties": { + "_id": { "bsonType": "objectId" }, + "name": { "bsonType": "string" }, + "address": { + "title": "Address", + "bsonType": "object", + "properties": { + "street": { "bsonType": "string" }, + "city": { "bsonType": "string" }, + "country": { "bsonType": "string" }, + "postalCode": { "bsonType": "string" } + } + } + } +} +``` + +```json +{ + "title": "Business", + "bsonType": "object", + "required": ["_id", "name"], + "properties": { + "_id": { "bsonType": "objectId" }, + "name": { "bsonType": "string" }, + "addresses": { + "bsonType": "array", + "items": { + "title": "Address", + "bsonType": "object", + "properties": { + "street": { "bsonType": "string" }, + "city": { "bsonType": "string" }, + "country": { "bsonType": "string" }, + "postalCode": { "bsonType": "string" } + } + } + } + } +} +``` + +## Read and Write Embedded Objects +### Create an Embedded Object +To create an embedded object, assign an instance of the embedded object +to a parent object's property. + +#### Java + +```java +// open realm + +Address address = new Address("123 Fake St.", "Springfield", "USA", "90710"); +Contact contact = new Contact("Nick Riviera", address); + +realm.executeTransaction(transactionRealm -> { + transactionRealm.insert(contact); +}); + +realm.close(); + +``` + +#### Kotlin + +```kotlin +// open realm + +val address = Address("123 Fake St.", "Springfield", "USA", "90710") +val contact = Contact("Nick Riviera", address) + +realm.executeTransaction { transactionRealm -> + transactionRealm.insert(contact) +} + +realm.close() + +``` + +### Update an Embedded Object Property +To update a property in an embedded object, modify the property in a +write transaction: + +#### Java + +```java +// assumes that at least one contact already exists in this partition +Contact resultContact = realm.where(Contact.class).findFirst(); + +realm.executeTransaction(transactionRealm -> { + resultContact.address.street = "Hollywood Upstairs Medical College"; + resultContact.address.city = "Los Angeles"; + resultContact.address.postalCode = "90210"; + Log.v("EXAMPLE", "Updated contact: " + resultContact); +}); + +realm.close(); + +``` + +#### Kotlin + +```kotlin +// assumes that at least one contact already exists in this partition +val result = realm.where().findFirst()!! + +realm.executeTransaction { transactionRealm -> + result.address?.street = "Hollywood Upstairs Medical College" + result.address?.city = "Los Angeles" + result.address?.postalCode = "90210" + Log.v("EXAMPLE", "Updated contact: ${result.name}") +} + +realm.close() + +``` + +### Overwrite an Embedded Object +To overwrite an embedded object, reassign the embedded object property +of a party to a new instance in a write transaction: + +#### Java + +```java +// assumes that at least one contact already exists in this partition +Contact oldContact = realm.where(Contact.class).findFirst(); + +realm.executeTransaction(transactionRealm -> { + Address newAddress = new Address( + "Hollywood Upstairs Medical College", + "Los Angeles", + "USA" + "90210" + ); + oldContact.address = newAddress; + Log.v("EXAMPLE", "Replaced contact: " + oldContact); +}); + +realm.close(); + +``` + +#### Kotlin + +```kotlin +// assumes that at least one contact already exists +val oldContact = realm.where().findFirst()!! + +realm.executeTransaction { transactionRealm -> + val newAddress = Address( + "Hollywood Upstairs Medical College", + "Los Angeles", + "USA", + "90210") + oldContact.address = newAddress + Log.v("EXAMPLE", "Updated contact: $oldContact") +} + +realm.close() + +``` + +### Query a Collection on Embedded Object Properties +Use dot notation to filter or sort a collection of objects based on an embedded object +property value: + +> Note: +> It is not possible to query embedded objects directly. Instead, +access embedded objects through a query for the parent object type. +> + +#### Java + +```java +RealmResults losAngelesContacts = realm.where(Contact.class) + .equalTo("address.city", "Los Angeles") + .sort("address.street").findAll(); +Log.v("EXAMPLE", "Los Angeles contacts: " + losAngelesContacts); + +``` + +#### Kotlin + +```kotlin +val losAngelesContacts = realm.where() + .equalTo("address.city", "Los Angeles") + .sort("address.street").findAll() +Log.v("EXAMPLE", "Los Angeles Contacts: $losAngelesContacts") + +``` + diff --git a/docs/guides/model-data/data-types/enums.md b/docs/guides/model-data/data-types/enums.md new file mode 100644 index 0000000000..703e5858ea --- /dev/null +++ b/docs/guides/model-data/data-types/enums.md @@ -0,0 +1,123 @@ +# Enumerations - Java SDK +Enumerations, also known as enums, are not supported natively in the +Java SDK. However, you can use Java and Kotlin enums in your +Realm objects if you follow these steps. + +## Usage +To use an enum in a Realm object class, define a field +with a type matching the underlying data type of your enum. Create +getters and setters for the field that convert the field value between +the underlying value and the enum type. You can use the Java's built-in +[Enum.valueOf()](https://docs.oracle.com/javase/7/docs/api/java/lang/Enum.html#valueOf(java.lang.Class,%20java.lang.String)) +method to convert from the underlying type to the enum type. + +#### Java + +```kotlin +public enum FrogState { + TADPOLE("Tadpole"), + FROG("Frog"), + OLD_FROG("Old Frog"); + + private String state; + FrogState(String state) { + this.state = state; + } + public String getState() { + return state; + } +} + +``` + +```java +import io.realm.RealmObject; + +public class Frog extends RealmObject { + String name; + String state = FrogState.TADPOLE.getState(); + // realm-required empty constructor + public Frog() {} + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public FrogState getState() { + // because state is actually a String and another client could assign an invalid value, + // default the state to "TADPOLE" if the state is unreadable + FrogState currentState = null; + try { + // fetches the FrogState enum value associated with the current internal string value + currentState = FrogState.valueOf(state); + } catch (IllegalArgumentException e) { + currentState = FrogState.TADPOLE; + } + return currentState; + } + public void setState(FrogState value) { + // users set state using a FrogState, but it is saved as a string internally + this.state = value.getState(); + } +} + +``` + +```java +Frog frog = realm.createObject(Frog.class); +frog.setName("Jonathan Livingston Applesauce"); +// set the state using the enum +frog.setState(FrogState.FROG); + +// fetching the state returns an enum +FrogState currentJonathanState = frog.getState(); + +``` + +#### Kotlin + +```kotlin +enum class FrogState(val state: String) { + TADPOLE("Tadpole"), + FROG("Frog"), + OLD_FROG("Old Frog"); +} + +``` + +```kotlin +import io.realm.RealmObject +import java.lang.IllegalArgumentException + +open class Frog // realm-required empty constructor + : RealmObject() { + var name: String? = null + private var state: String = FrogState.TADPOLE.state + var stateEnum: FrogState + get() { + // because state is actually a String and another client could assign an invalid value, + // default the state to "TADPOLE" if the state is unreadable + return try { + // fetches the FrogState enum value associated with the current internal string value + FrogState.valueOf(state) + } catch (e: IllegalArgumentException) { + FrogState.TADPOLE + } + } + set(value) { + // users set state using a FrogState, but it is saved as a string internally + state = value.state + } +} + +``` + +```kotlin +val frog = realm.createObject(Frog::class.java) +frog.name = "Jonathan Livingston Applesauce" +// set the state using the enum +frog.stateEnum = FrogState.FROG + +// fetching the state returns an enum +val currentJonathanState: FrogState = frog.stateEnum + +``` + diff --git a/docs/guides/model-data/data-types/field-types.md b/docs/guides/model-data/data-types/field-types.md new file mode 100644 index 0000000000..8926ad7fcd --- /dev/null +++ b/docs/guides/model-data/data-types/field-types.md @@ -0,0 +1,44 @@ +# Field Types - Java SDK +Realm supports the following field data types: + +- `Boolean` or `boolean` +- `Integer` or `int` +- `Short` or `short` +- `Long` or `long` +- `Byte` or `byte[]` +- `Double` or `double` +- `Float` or `float` +- `String` +- `Date` +- `Decimal128` from `org.bson.types` +- `ObjectId` from `org.bson.types` +- `UUID` from `java.util.UUID` +- Any `RealmObject` subclass +- `RealmList` +- `RealmAny` +- `RealmSet` +- `RealmDictionary` + +The `Byte`, `Short`, `Integer`, and `Long` types and their +lowercase primitive alternatives are all stored as `Long` values +within Realm. Similarly, Realm stores objects +of the `Float` and `float` types as type `Double`. + +Realm does not support fields with modifiers `final` and +`volatile`, though you can use fields with those modifiers if you +ignore them. If you choose to provide custom +constructors, you must declare a public constructor with no arguments. + +## Updating Strings and Byte Arrays +Since Realm operates on fields as a whole, it's not possible +to directly update individual elements of strings or byte arrays. Instead, +you'll need to read the whole field, make your modification to individual +elements, and then write the entire field back again in a transaction block. + +## Object IDs and UUIDs +`ObjectId` and `UUID` (Universal Unique Identifier) both provide +unique values that can be used as identifiers for objects. +`ObjectId` is a +12-byte unique value. `UUID` is a [standardized](https://tools.ietf.org/html/rfc4122) 16-byte +unique value. Both types are indexable +and can be used as primary keys. diff --git a/docs/guides/model-data/data-types/realmany.md b/docs/guides/model-data/data-types/realmany.md new file mode 100644 index 0000000000..3dd1f28f10 --- /dev/null +++ b/docs/guides/model-data/data-types/realmany.md @@ -0,0 +1,293 @@ +# RealmAny - Java SDK +> Version added: 10.6.0 + +You can use the `RealmAny` data type to create +Realm object fields that can contain any of several +underlying types. You can store multiple `RealmAny` instances in +`RealmList`, `RealmDictionary`, or `RealmSet` fields. To change +the value of a `RealmAny` field, assign a new `RealmAny` instance +with a different underlying value. `RealmAny` fields are indexable, but +cannot be used as primary keys. + +> Note: +> `RealmAny` objects can refer to any +supported field type +*except*: +> +> - `RealmAny` +> - `RealmList` +> - `RealmSet` +> - `RealmDictionary` +> + +## Usage +To create a `RealmAny` instance, use the +`RealmAny.valueOf()` method +to assign an initial value or `RealmAny.nullValue()` to assign no +value. `RealmAny` instances are immutable just like `String` or +`Integer` instances; if you want to assign a new value to a +`RealmAny` field, you must create a new `RealmAny` instance. + +> Warning: +> `RealmAny` instances are always nullable. Additionally, instances can contain a value +of type `RealmAny.Type.NULL`. +> + +#### Java + +```java +import com.mongodb.realm.examples.model.kotlin.Person; + +import io.realm.RealmAny; +import io.realm.RealmObject; + +public class Frog extends RealmObject { + String name; + RealmAny bestFriend; + // realm-required empty constructor + public Frog() {} + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public RealmAny getBestFriend() { return bestFriend; } + public void setBestFriend(RealmAny bestFriend) { this.bestFriend = bestFriend; } + public String bestFriendToString() { + switch(bestFriend.getType()) { + case NULL: { + return "no best friend"; + } + case STRING: { + return bestFriend.asString(); + } + case OBJECT: { + if (bestFriend.getValueClass().equals(Person.class)) { + Person person = bestFriend.asRealmModel(Person.class); + return person.getName(); + } + } + default: { + return "unknown type"; + } + } + } +} + +``` + +```java + Frog frog = realm.createObject(Frog.class); + frog.setName("Jonathan Livingston Applesauce"); + + // set RealmAny field to a null value + frog.setBestFriend(RealmAny.nullValue()); + Log.v("EXAMPLE", "Best friend: " + frog.bestFriendToString()); + + // possible types for RealmAny are defined in RealmAny.Type + Assert.assertTrue(frog.getBestFriend().getType() == RealmAny.Type.NULL); + + // set RealmAny field to a string with RealmAny.valueOf a string value + frog.setBestFriend(RealmAny.valueOf("Greg")); + Log.v("EXAMPLE", "Best friend: " + frog.bestFriendToString()); + + // RealmAny instances change type as you reassign to different values + Assert.assertTrue(frog.getBestFriend().getType() == RealmAny.Type.STRING); + + // set RealmAny field to a realm object, also with valueOf + Person person = new Person("Jason Funderburker"); + + frog.setBestFriend(RealmAny.valueOf(person)); + Log.v("EXAMPLE", "Best friend: " + frog.bestFriendToString()); + + // You can also extract underlying Realm Objects from RealmAny with asRealmModel + Person bestFriendObject = frog.getBestFriend().asRealmModel(Person.class); + Log.v("EXAMPLE", "Best friend: " + bestFriendObject.getName()); + + // RealmAny fields referring to any Realm Object use the OBJECT type + Assert.assertTrue(frog.getBestFriend().getType() == RealmAny.Type.OBJECT); + + // you can't put a RealmList in a RealmAny field directly, + // ...but you can set a RealmAny field to a RealmObject that contains a list + GroupOfPeople persons = new GroupOfPeople(); + // GroupOfPeople contains a RealmList of people + persons.getPeople().add("Rand"); + persons.getPeople().add("Perrin"); + persons.getPeople().add("Mat"); + + frog.setBestFriend(RealmAny.valueOf(persons)); + Log.v("EXAMPLE", "Best friend: " + + frog.getBestFriend().asRealmModel(GroupOfPeople.class).getPeople().toString()); + +``` + +#### Kotlin + +```kotlin +import io.realm.RealmAny +import io.realm.RealmObject + +open class Frog(var bestFriend: RealmAny? = RealmAny.nullValue()) : RealmObject() { + var name: String? = null + open fun bestFriendToString(): String { + if (bestFriend == null) { + return "null" + } + return when (bestFriend!!.type) { + RealmAny.Type.NULL -> { + "no best friend" + } + RealmAny.Type.STRING -> { + bestFriend!!.asString() + } + RealmAny.Type.OBJECT -> { + if (bestFriend!!.valueClass == Person::class.java) { + val person = bestFriend!!.asRealmModel(Person::class.java) + person.name + } + "unknown type" + } + else -> { + "unknown type" + } + } + } +} + +``` + +```kotlin +val frog = realm.createObject(Frog::class.java) +frog.name = "George Washington" + +// set RealmAny field to a null value + +// set RealmAny field to a null value +frog.bestFriend = RealmAny.nullValue() +Log.v("EXAMPLE", "Best friend: " + frog.bestFriendToString()) + +// possible types for RealmAny are defined in RealmAny.Type +Assert.assertEquals(frog.bestFriend?.type, RealmAny.Type.NULL) + +// set RealmAny field to a string with RealmAny.valueOf a string value +frog.bestFriend = RealmAny.valueOf("Greg") +Log.v("EXAMPLE", "Best friend: " + frog.bestFriendToString()) + +// RealmAny instances change type as you reassign to different values +Assert.assertEquals(frog.bestFriend?.type, RealmAny.Type.STRING) + +// set RealmAny field to a realm object, also with valueOf +val person = Person("Jason Funderburker") + +frog.bestFriend = RealmAny.valueOf(person) +Log.v("EXAMPLE", "Best friend: " + frog.bestFriendToString()) + +// You can also extract underlying Realm Objects from RealmAny with asRealmModel +val bestFriendObject = frog.bestFriend?.asRealmModel(Person::class.java) +Log.v("EXAMPLE", "Best friend: " + bestFriendObject?.name) + +// RealmAny fields referring to any Realm Object use the OBJECT type +Assert.assertEquals(frog.bestFriend?.type, RealmAny.Type.OBJECT) + +// you can't put a RealmList in a RealmAny field directly, +// ...but you can set a RealmAny field to a RealmObject that contains a list +val persons = GroupOfPeople() +// GroupOfPeople contains a RealmList of people +persons.people.add("Rand") +persons.people.add("Perrin") +persons.people.add("Mat") + +frog.bestFriend = RealmAny.valueOf(persons) +Log.v("EXAMPLE", "Best friend: " + + frog.bestFriend?.asRealmModel(GroupOfPeople::class.java) + ?.people.toString()) + +``` + +## Queries +You can query a `RealmAny` field just like any other data type. +Operators that only work with certain types, such as string +operators and arithmetic operators, ignore +values that do not contain that type. Negating such operators matches +values that do not contain the type. Type queries match the underlying +type, rather than `RealmAny`. Arithmetic operators convert numeric +values implicitly to compare across types. + +## Notifications +To subscribe to changes to a `RealmAny` field, use the +`RealmObject.addChangeListener` +method of the enclosing object. You can use the +`ObjectChangeSet` +parameter to determine if the `RealmAny` field changed. + +#### Java + +```java +AtomicReference frog = new AtomicReference(); +realm.executeTransaction(r -> { + frog.set(realm.createObject(Frog.class)); + frog.get().setName("Jonathan Livingston Applesauce"); +}); + +RealmObjectChangeListener objectChangeListener = + new RealmObjectChangeListener() { + @Override + public void onChange(@NotNull Frog frog, @Nullable ObjectChangeSet changeSet) { + if (changeSet != null) { + Log.v("EXAMPLE", "Changes to fields: " + + Arrays.toString(changeSet.getChangedFields())); + if (changeSet.isFieldChanged("best_friend")) { + Log.v("EXAMPLE", "RealmAny best friend field changed to : " + + frog.bestFriendToString()); + } + } + } +}; + +frog.get().addChangeListener(objectChangeListener); + +realm.executeTransaction(r -> { + // set RealmAny field to a null value + frog.get().setBestFriend(RealmAny.nullValue()); + Log.v("EXAMPLE", "Best friend: " + frog.get().bestFriendToString()); + + // set RealmAny field to a string with RealmAny.valueOf a string value + frog.get().setBestFriend(RealmAny.valueOf("Greg")); + +}); + +``` + +#### Kotlin + +```kotlin +var frog: Frog? = null + +realm.executeTransaction { r: Realm? -> + frog = realm.createObject(Frog::class.java) + frog?.name = "Jonathan Livingston Applesauce" +} + +val objectChangeListener + = RealmObjectChangeListener { frog, changeSet -> + if (changeSet != null) { + Log.v("EXAMPLE", "Changes to fields: " + + changeSet.changedFields) + if (changeSet.isFieldChanged("best_friend")) { + Log.v("EXAMPLE", "RealmAny best friend field changed to : " + + frog.bestFriendToString()) + } + } +} + +frog?.addChangeListener(objectChangeListener) + +realm.executeTransaction { r: Realm? -> + // set RealmAny field to a null value + frog?.bestFriend = RealmAny.nullValue() + Log.v("EXAMPLE", "Best friend: " + frog?.bestFriendToString()) + + // set RealmAny field to a string with RealmAny.valueOf a string value + frog?.bestFriend = RealmAny.valueOf("Greg") +} + +``` + diff --git a/docs/guides/model-data/data-types/realmdictionary.md b/docs/guides/model-data/data-types/realmdictionary.md new file mode 100644 index 0000000000..c2dec0e899 --- /dev/null +++ b/docs/guides/model-data/data-types/realmdictionary.md @@ -0,0 +1,244 @@ +# RealmDictionary - Java SDK +> Version added: 10.6.0 + +You can use the `RealmDictionary` data type to manage a collection of +unique `String` keys paired with values. `RealmDictionary` +implements Java's `Map` interface, so it works just like the built-in +`HashMap` class, except managed `RealmDictionary` instances persist +their contents to a realm. `RealmDictionary` instances that +contain Realm objects store references to those objects. +When you delete a Realm object from a realm, any +references to that object in a `RealmDictionary` become `null` +values. + +## Usage +To create a field of type `RealmDictionary`, define an object property +of type `RealmDictionary`, where `T` defines the values you would +like to store in your `RealmDictionary`. Currently, `RealmDictionary` +instances can only use keys of type `String`. + +The following table shows which methods you can use to complete common +collection tasks with `RealmDictionary`: + +|Task|Method| +| --- | --- | +|Add an object to a `RealmDictionary`|`put()` (or the `[]` operator in Kotlin)| +|Add multiple objects to a `RealmDictionary`|`putAll()`| +|Check if the dictionary contains an specific key|`containsKey()`| +|Check if the dictionary contains a specific value|`containsValue()`| + +#### Java + +```java +import io.realm.RealmDictionary; +import io.realm.RealmObject; + +public class Frog extends RealmObject { + String name; + RealmDictionary nicknamesToFriends; + // realm-required empty constructor + public Frog() {} + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public RealmDictionary getNicknamesToFriends() { return nicknamesToFriends; } + public void setNicknamesToFriends(RealmDictionary nicknamesToFriends) { this.nicknamesToFriends = nicknamesToFriends; } +} + +``` + +```java +Frog frog = realm.createObject(Frog.class); +frog.setName("George Washington"); + +// get the RealmDictionary field from the object we just created +RealmDictionary dictionary = frog.getNicknamesToFriends(); + +// add key/value to the dictionary +Frog wirt = realm.createObject(Frog.class); +wirt.setName("Wirt"); +dictionary.put("tall frog", wirt); + +// add multiple keys/values to the dictionary +Frog greg = realm.createObject(Frog.class); +greg.setName("Greg"); +Frog beatrice = realm.createObject(Frog.class); +beatrice.setName("Beatrice"); +dictionary.putAll(Map.of("small frog", greg, "feathered frog", beatrice)); + +// check for the presence of a key +Assert.assertTrue(dictionary.containsKey("small frog")); + +// check for the presence of a value +Assert.assertTrue(dictionary.containsValue(greg)); + +// remove a key +dictionary.remove("feathered frog"); +Assert.assertFalse(dictionary.containsKey("feathered frog")); + +// deleting a Realm object does NOT remove it from the dictionary +int sizeOfDictionaryBeforeDelete = dictionary.size(); +greg.deleteFromRealm(); +// deleting greg object did not reduce the size of the dictionary +Assert.assertEquals(sizeOfDictionaryBeforeDelete, dictionary.size()); +// but greg object IS now null: +Assert.assertEquals(dictionary.get("small frog"), null); + +``` + +#### Kotlin + +```kotlin +import io.realm.RealmDictionary +import io.realm.RealmObject + +open class Frog + : RealmObject() { + var name: String? = null + var nicknamesToFriends: RealmDictionary = RealmDictionary() +} + +``` + +```kotlin +val frog = + realm.createObject(Frog::class.java) +frog.name = "George Washington" + +// get the RealmDictionary field from the object we just created +val dictionary = frog.nicknamesToFriends + +// add key/value to the dictionary +val wirt = + realm.createObject(Frog::class.java) +wirt.name = "Wirt" +dictionary["tall frog"] = wirt + +// add multiple keys/values to the dictionary +val greg = + realm.createObject(Frog::class.java) +greg.name = "Greg" +val beatrice = + realm.createObject(Frog::class.java) +beatrice.name = "Beatrice" +dictionary.putAll(mapOf( + Pair("small frog", greg), + Pair("feathered frog", beatrice))) + +// check for the presence of a key +Assert.assertTrue(dictionary.containsKey("small frog")) + +// check for the presence of a value +Assert.assertTrue(dictionary.containsValue(greg)) + +// remove a key +dictionary.remove("feathered frog") +Assert.assertFalse(dictionary.containsKey("feathered frog")) + +// deleting a Realm object does NOT remove it from the dictionary +val sizeOfDictionaryBeforeDelete = dictionary.size +greg.deleteFromRealm() +// deleting greg object did not reduce the size of the dictionary +Assert.assertEquals( + sizeOfDictionaryBeforeDelete.toLong(), + dictionary.size.toLong() +) +// but greg object IS now null: +Assert.assertEquals(dictionary["small frog"], null) + +``` + +## Notifications +To subscribe to changes to a `RealmDictionary`, pass a +`MapChangeListener` +implementation to the `RealmSet.addChangeListener` method. +Your `MapChangeListener` implementation must define an +`onChange()` method, which accepts a reference to the changed `RealmDictionary` +and a set of changes as parameters. You can access the keys +added to the dictionary as well as the keys removed from the dictionary +through the `MapChangeSet` parameter. + +#### Java + +```java +AtomicReference frog = new AtomicReference(); +realm.executeTransaction(r -> { + frog.set(realm.createObject(Frog.class)); + frog.get().setName("Jonathan Livingston Applesauce"); +}); + +MapChangeListener mapChangeListener = + new MapChangeListener() { + @Override + public void onChange(RealmMap map, + MapChangeSet changes) { + for (String insertion : changes.getInsertions()) { + Log.v("EXAMPLE", + "Inserted key: " + insertion + + ", Inserted value: " + map.get(insertion).getName()); + } + } + }; + +frog.get().getNicknamesToFriends().addChangeListener(mapChangeListener); + +realm.executeTransaction(r -> { + // get the RealmDictionary field from the object we just created + RealmDictionary dictionary = frog.get().getNicknamesToFriends(); + + // add key/value to the dictionary + Frog wirt = realm.createObject(Frog.class); + wirt.setName("Wirt"); + dictionary.put("tall frog", wirt); + + // add multiple keys/values to the dictionary + Frog greg = realm.createObject(Frog.class); + greg.setName("Greg"); + Frog beatrice = realm.createObject(Frog.class); + beatrice.setName("Beatrice"); + dictionary.putAll(Map.of("small frog", greg, "feathered frog", beatrice)); + +}); + +``` + +#### Kotlin + +```kotlin +var frog: Frog? = null +realm.executeTransaction { r: Realm? -> + frog = realm.createObject(Frog::class.java) + frog?.name = "Jonathan Livingston Applesauce" +} + +val mapChangeListener: MapChangeListener + = MapChangeListener { map, changes -> + for (insertion in changes.insertions) { + Log.v("EXAMPLE", + "Inserted key: $insertion, Inserted value: ${map[insertion]!!.name}") + } +} + +frog?.nicknamesToFriends?.addChangeListener(mapChangeListener) + +realm.executeTransaction { r: Realm? -> + // get the RealmDictionary field from the object we just created + val dictionary = frog!!.nicknamesToFriends + + // add key/value to the dictionary + val wirt = realm.createObject(Frog::class.java) + wirt.name = "Wirt" + dictionary["tall frog"] = wirt + + // add multiple keys/values to the dictionary + val greg = realm.createObject(Frog::class.java) + greg.name = "Greg" + val beatrice = realm.createObject(Frog::class.java) + beatrice.name = "Beatrice" + dictionary.putAll(mapOf( + Pair("small frog", greg), + Pair("feathered frog", beatrice))) +} + +``` + diff --git a/docs/guides/model-data/data-types/realmset.md b/docs/guides/model-data/data-types/realmset.md new file mode 100644 index 0000000000..618f5de4bc --- /dev/null +++ b/docs/guides/model-data/data-types/realmset.md @@ -0,0 +1,265 @@ +# RealmSet - Java SDK +> Version added: 10.6.0 + +You can use the `RealmSet` data type +to manage a collection of unique keys. `RealmSet` implements Java's +`Set` interface, so it works just like the built-in `HashSet` class, +except managed `RealmSet` instances persist their contents to a +realm. `RealmSet` instances that contain Realm objects +actually only store references to those objects, so deleting a +Realm object from a realm also deletes that object from +any `RealmSet` instances that contain the object. + +Because `RealmSet` implements `RealmCollection`, it has some useful +mathematical methods, such as `sum`, `min`, and `max`. For a complete +list of available `RealmSet` methods, see: [the RealmSet API +reference](https://www.mongodb.com/docs/realm-sdks/java/latest/io/realm/RealmSet.html). + +## Method Limitations +You cannot use the following `Realm` methods on objects that contain +a field of type `RealmSet`: + +- `Realm.insert()` +- `Realm.insertOrUpdate()` +- `Realm.createAllFromJson()` +- `Realm.createObjectFromJson()` +- `Realm.createOrUpdateAllFromJson()` +- `Realm.createOrUpdateObjectFromJson()` + +## Usage +To create a field of type `RealmSet`, define an object property of +type `RealmSet`, where `E` defines the keys you would like to +store in your `RealmSet`. + +- Add an object to a `RealmSet` with +`RealmSet.add()` +- Add multiple objects with +`RealmSet.addAll()` +- Check if the set contains a specific object with +`RealmSet.contains()` +- Check if the set contains all of multiple objects with +`RealmSet.containsAll()` + +#### Java + +```java +import io.realm.RealmObject; +import io.realm.RealmSet; + +public class Frog extends RealmObject { + String name; + RealmSet favoriteSnacks; + // realm-required empty constructor + public Frog() {} + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public RealmSet getFavoriteSnacks() { return favoriteSnacks; } + public void setFavoriteSnacks(RealmSet favoriteSnacks) { this.favoriteSnacks = favoriteSnacks; } +} + +``` + +```java +import io.realm.RealmObject; + +public class Snack extends RealmObject { + private String name; + public Snack() {} + + public String getName() { return name; } + public void setName(String name) { this.name = name; } +} + +``` + +```java +Frog frog = realm.createObject(Frog.class); +frog.setName("George Washington"); + +// get the RealmSet field from the object we just created +RealmSet set = frog.getFavoriteSnacks(); + +// add value to the RealmSet +Snack flies = realm.createObject(Snack.class); +flies.setName("flies"); +set.add(flies); + +// add multiple values to the RealmSet +Snack water = realm.createObject(Snack.class); +water.setName("water"); +Snack verySmallRocks = realm.createObject(Snack.class); +verySmallRocks.setName("verySmallRocks"); +set.addAll(Arrays.asList(water, verySmallRocks)); + +// check for the presence of a key with contains +Assert.assertTrue(set.contains(flies)); + +// check for the presence of multiple keys with containsAll +Snack biscuits = realm.createObject(Snack.class); +biscuits.setName("biscuits"); +Assert.assertTrue(set.containsAll(Arrays.asList(water, biscuits)) == false); + +// remove string from a set +set.remove(verySmallRocks); + +// set no longer contains that string +Assert.assertTrue(set.contains(verySmallRocks) == false); + +// deleting a Realm object also removes it from any RealmSets +int sizeOfSetBeforeDelete = set.size(); +flies.deleteFromRealm(); +// deleting flies object reduced the size of the set by one +Assert.assertTrue(sizeOfSetBeforeDelete == set.size() + 1); + +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.RealmSet + +open class Frog + : RealmObject() { + var name: String = "" + var favoriteSnacks: RealmSet = RealmSet(); +} + +``` + +```kotlin +import io.realm.RealmObject + +open class Snack : RealmObject() { + var name: String? = null +} + +``` + +```kotlin +val frog = realm.createObject(Frog::class.java) +frog.name = "Jonathan Livingston Applesauce" + +// get the RealmSet field from the object we just created +val set = frog.favoriteSnacks + +// add value to the RealmSet +val flies = realm.createObject(Snack::class.java) +flies.name = "flies" +set.add(flies) + +// add multiple values to the RealmSet +val water = realm.createObject(Snack::class.java) +water.name = "water" +val verySmallRocks = realm.createObject(Snack::class.java) +verySmallRocks.name = "verySmallRocks" +set.addAll(listOf(water, verySmallRocks)) + +// check for the presence of a key with contains +Assert.assertTrue(set.contains(flies)) + +// check for the presence of multiple keys with containsAll +val biscuits = realm.createObject(Snack::class.java) +biscuits.name = "biscuits" +Assert.assertTrue(set.containsAll(Arrays.asList(water, biscuits)) == false) + +// remove string from a set +set.remove(verySmallRocks) + +// set no longer contains that string +Assert.assertTrue(set.contains(verySmallRocks) == false) + +// deleting a Realm object also removes it from any RealmSets +val sizeOfSetBeforeDelete = set.size +flies.deleteFromRealm() +// deleting flies object reduced the size of the set by one +Assert.assertTrue(sizeOfSetBeforeDelete == set.size + 1) + +``` + +## Notifications +To subscribe to changes to a `RealmSet`, pass a +`SetChangeListener` +implementation to the `RealmSet.addChangeListener` method. +Your `SetChangeListener` implementation must define an +`onChange()` method, which accepts a reference to the changed `RealmSet` +and a set of changes as parameters. You can access the number of items +added to the set as well as the number of items removed from the set +through the `SetChangeSet` parameter. + +#### Java + +```java +AtomicReference frog = new AtomicReference(); +realm.executeTransaction(r -> { + frog.set(realm.createObject(Frog.class)); + frog.get().setName("Jonathan Livingston Applesauce"); +}); + +SetChangeListener setChangeListener = new SetChangeListener() { + @Override + public void onChange(@NotNull RealmSet set, SetChangeSet changes) { + Log.v("EXAMPLE", "Set changed: " + + changes.getNumberOfInsertions() + " new items, " + + changes.getNumberOfDeletions() + " items removed."); + } +}; +frog.get().getFavoriteSnacks().addChangeListener(setChangeListener); + +realm.executeTransaction(r -> { + // get the RealmSet field from the object we just created + RealmSet set = frog.get().getFavoriteSnacks(); + + // add value to the RealmSet + Snack flies = realm.createObject(Snack.class); + flies.setName("flies"); + set.add(flies); + + // add multiple values to the RealmSet + Snack water = realm.createObject(Snack.class); + water.setName("water"); + Snack verySmallRocks = realm.createObject(Snack.class); + verySmallRocks.setName("verySmallRocks"); + set.addAll(Arrays.asList(water, verySmallRocks)); + +}); + +``` + +#### Kotlin + +```kotlin +var frog :Frog? = null +realm.executeTransaction { r: Realm? -> + frog = realm.createObject(Frog::class.java) + frog?.name = "Jonathan Livingston Applesauce" +} + +val setChangeListener: SetChangeListener + = SetChangeListener { set, changes -> + Log.v("EXAMPLE", "Set changed: " + + changes.numberOfInsertions + " new items, " + + changes.numberOfDeletions + " items removed.") +} +frog?.favoriteSnacks?.addChangeListener(setChangeListener) + +realm.executeTransaction { r: Realm? -> + // get the RealmSet field from the object we just created + val set = frog!!.favoriteSnacks + + // add value to the RealmSet + val flies = realm.createObject(Snack::class.java) + flies.name = "flies" + set.add(flies) + + // add multiple values to the RealmSet + val water = realm.createObject(Snack::class.java) + water.name = "water" + val verySmallRocks = realm.createObject(Snack::class.java) + verySmallRocks.name = "verySmallRocks" + set.addAll(Arrays.asList(water, verySmallRocks)) +} + +``` + diff --git a/docs/guides/model-data/define-a-realm-object-model.md b/docs/guides/model-data/define-a-realm-object-model.md new file mode 100644 index 0000000000..dbf56218b0 --- /dev/null +++ b/docs/guides/model-data/define-a-realm-object-model.md @@ -0,0 +1,1095 @@ +# Define a Realm Object Model - Java SDK +## Define a Realm Object +To define a Realm object in your application, +create a subclass of `RealmObject` +or implement `RealmModel`. + +> Important: +> - All Realm objects must provide an empty constructor. +> - All Realm objects must use the `public` visibility modifier in Java +or the `open` visibility modifier in Kotlin. +> + +> Note: +> Class names are limited to a maximum of 57 UTF-8 characters. +> + +### Extend RealmObject +The following code block shows a Realm object that +describes a Frog. This Frog class can be stored in +Realm because it `extends` the `RealmObject` class. + +#### Java + +```java +import io.realm.RealmObject; + +// To add an object to your Realm Schema, extend RealmObject +public class Frog extends RealmObject { + private String name; + private int age; + private String species; + private String owner; + public Frog(String name, int age, String species, String owner) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject + +// providing default values for each constructor parameter +// fulfills the need for an empty constructor +open class Frog( + var name: String? = null, + var age: Int = 0, + var species: String? = null, + var owner: String? = null +) : RealmObject() // To add an object to your Realm Schema, extend RealmObject +``` + +### Implement RealmModel +The following code block shows a Realm object that +describes a Frog. This Frog class can +be stored in Realm because it `implements` the +`RealmModel` class and uses the `@RealmClass` annotation: + +#### Java + +```java +import io.realm.RealmModel; +import io.realm.annotations.RealmClass; + +@RealmClass +public class Frog implements RealmModel { + private String name; + private int age; + private String species; + private String owner; + public Frog(String name, int age, String species, String owner) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + } + public Frog() {} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } +} +``` + +> Important: +> All Realm objects must use the `public` +visibility modifier. +> + +#### Kotlin + +```kotlin +import io.realm.RealmModel +import io.realm.annotations.RealmClass + +@RealmClass +open class Frog : RealmModel { + var name: String? = null + var age = 0 + var species: String? = null + var owner: String? = null + + constructor(name: String?, age: Int, species: String?, owner: String?) { + this.name = name + this.age = age + this.species = species + this.owner = owner + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +> Important: +> All Realm objects must use the `open` +visibility modifier. +> + +> Tip: +> When you create a Realm object by extending the `RealmObject` +class, you can access `RealmObject` class methods dynamically on +instances of your Realm object. Realm objects +created by implementing `RealmModel` can access those same methods +statically through the `RealmObject` class: +> +> #### Java +> +> ```java +> // With RealmObject +> frogRealmObject.isValid(); +> frogRealmObject.addChangeListener(listener); +> +> // With RealmModel +> RealmObject.isValid(frogRealmModel); +> RealmObject.addChangeListener(frogRealmModel, listener); +> +> ``` +> +> +> #### Kotlin +> +> ```kotlin +> // With RealmObject +> frogRealmObject?.isValid +> frogRealmObject?.addChangeListener(listener) +> +> // With RealmModel +> RealmObject.isValid(frogRealmModel) +> RealmObject.addChangeListener(frogRealmModel, listener) +> +> ``` +> +> + +## Lists +Realm objects can contain lists of non-Realm-object data +types: + +#### Java + +Unlike lists of Realm objects, these lists can contain +null values. If null values shouldn't be allowed, use the +@Required annotation. + +```java +import io.realm.RealmList; +import io.realm.RealmObject; + +public class Frog extends RealmObject { + private String name; + private int age; + private String species; + private String owner; + private RealmList favoriteColors; + public Frog(String name, int age, String species, String owner, RealmList favoriteColors) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + this.favoriteColors = favoriteColors; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } + public RealmList getFavoriteColors() { return favoriteColors; } + public void setFavoriteColors(RealmList favoriteColors) { this.favoriteColors = favoriteColors; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmList +import io.realm.RealmObject + +open class Frog : RealmObject { + var name: String? = null + var age = 0 + var species: String? = null + var owner: String? = null + var favoriteColors : RealmList? = null + + constructor( + name: String?, + age: Int, + species: String?, + owner: String?, + favoriteColors: RealmList? + ) { + this.name = name + this.age = age + this.species = species + this.owner = owner + this.favoriteColors = favoriteColors + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +> Seealso: +> Data Types: Lists +> + +## Define an Embedded Object Field +Realm provides the ability to nest objects within other +objects. This has several advantages: + +- When you delete an object that contains another object, the delete +operation removes both objects from the realm, so unused objects +don't accumulate in your realm file, taking up valuable space on +user's mobile devices. + +To embed an object, set the `embedded` property of the +`@RealmClass` +annotation to `true` on the class that you'd like to nest within +another class: + +#### Java + +```java +import io.realm.RealmObject; +import io.realm.annotations.RealmClass; + +@RealmClass(embedded=true) +public class Fly extends RealmObject { + private String name; + public Fly(String name) { + this.name = name; + } + public Fly() {} // RealmObject subclasses must provide an empty constructor +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.RealmClass + +@RealmClass(embedded = true) +open class Fly : RealmObject { + private var name: String? = null + + constructor(name: String?) { + this.name = name + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +Then, any time you reference that class from another class, +Realm will embed the referenced class within the enclosing +class, as in the following example: + +#### Java + +```java +import io.realm.RealmObject; + +public class Frog extends RealmObject { + private String name; + private int age; + private String species; + private String owner; + private Fly lastMeal; + public Frog(String name, int age, String species, String owner, Fly lastMeal) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + this.lastMeal = lastMeal; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } + public Fly getLastMeal() { return lastMeal; } + public void setLastMeal(Fly lastMeal) { this.lastMeal = lastMeal; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject + +open class Frog : RealmObject { + var name: String? = null + var age = 0 + var species: String? = null + var owner: String? = null + var lastMeal: Fly? = null + + constructor( + name: String?, + age: Int, + species: String?, + owner: String?, + lastMeal: Fly? + ) { + this.name = name + this.age = age + this.species = species + this.owner = owner + this.lastMeal = lastMeal + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +> Seealso: +> Data Types: Embedded Objects +> + +## Annotations +Use annotations to customize your Realm object models. + +### Primary Key +> Version added: 10.6.0 +> Realm automatically indexes +primary key fields. Previously, Realm only indexed `String` primary +keys automatically. +> + +Realm treats fields marked with the +`@PrimaryKey` annotation +as primary keys for their corresponding object schema. Primary keys are +subject to the following limitations: + +- You can define only one primary key per object schema. +- Primary key values must be unique across all instances of an object +in a realm. Attempting to insert a duplicate primary key value +results in a `RealmPrimaryKeyConstraintException`. +- Primary key values are immutable. To change the primary key value of +an object, you must delete the original object and insert a new object +with a different primary key value. +- Embedded objects cannot define a +primary key. + +You can create a primary key with any of the following types: + +- `String` +- `UUID` +- `ObjectId` +- `Integer` or `int` +- `Long` or `long` +- `Short` or `short` +- `Byte` or `byte[]` + +Non-primitive types can contain a value of `null` as a primary key +value, but only for one object of a particular type, since each primary +key value must be unique. Attempting to insert an object with an existing +primary key into a realm will result in a +`[RealmPrimaryKeyConstraintException`. + +Realm automatically indexes +primary key fields, which allows you to efficiently read and modify +objects based on their primary key. + +You cannot change the primary key field for an object type after adding +any object of that type to a realm. + +Embedded objects cannot contain primary keys. + +You may optionally define a primary key for an object type as part of +the object schema with the +`@PrimaryKey` annotation: + +#### Java + +```java +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; + +public class Frog extends RealmObject { + @PrimaryKey private String name; + private int age; + private String species; + private String owner; + public Frog(String name, int age, String species, String owner) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.PrimaryKey + +open class Frog : RealmObject { + @PrimaryKey var name : String? = null + var age = 0 + var species: String? = null + var owner: String? = null + + constructor(name: String?, age: Int, species: String?, owner: String?) { + this.name = name + this.age = age + this.species = species + this.owner = owner + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +### Required Fields +#### Java + +```java +import io.realm.RealmObject; +import io.realm.annotations.Required; + +public class Frog extends RealmObject { + @Required private String name; + private int age; + private String species; + private String owner; + public Frog(String name, int age, String species, String owner) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.Required + +open class Frog : RealmObject { + @Required var name: String? = null + var age = 0 + var species: String? = null + var owner: String? = null + + constructor(name: String?, age: Int, species: String?, owner: String?) { + this.name = name + this.age = age + this.species = species + this.owner = owner + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +### Optional Fields +Fields marked with Java object types and Kotlin nullable types +(ending with `?`) are nullable by default. All other types +(primitives, non-nullable Kotlin object types) are required by default. +You can mark a nullable field with the `@Required` +annotation to prevent that field from holding a null value. +`RealmLists` are never nullable, but +you can use the `@Required` annotation to prevent objects in a list +from holding a null value, even if the base type would otherwise allow it. +You cannot mark a `RealmList` of `RealmObject` subtypes as required. + +You can make any of the following types required: + +- `String` +- `UUID` +- `ObjectId` +- `Integer` +- `Long` +- `Short` +- `Byte` or `byte[]` +- `Boolean` +- `Float` +- `Double` +- `Date` +- `RealmList` + +Primitive types such as `int` and the `RealmList` type are +implicitly required. Fields with the `RealmObject` type are always +nullable, and cannot be made required. + +> Important: +> In Kotlin, types are non-nullable by default unless you explicitly +add a `?` suffix to the type. You can only annotate +nullable types. Using the +`@Required` annotation on non-nullable types will fail compilation. +> + +#### Java + +Nullable fields are optional by default in Realm, unless +otherwise specified with the @Required +annotation. The following types are nullable: + +- `String` +- `Date` +- `UUID` +- `ObjectId` +- `Integer` +- `Long` +- `Short` +- `Byte` or `byte[]` +- `Boolean` +- `Float` +- `Double` + +Primitive types like `int` and `long` are non-nullable by +default and cannot be made nullable, as they cannot be set to a +null value. + +#### Kotlin + +In Kotlin, fields are considered nullable only if a field is +marked nullable with the Kotlin [? operator](https://kotlinlang.org/docs/reference/null-safety.html) except +for the following types: + +- `String` +- `Date` +- `UUID` +- `ObjectId` +- `Decimal128` +- `RealmAny` + +You can require any type that ends with the Kotlin `?` +operator, such as `Int?`. + +The `RealmList` type is non-nullable by default and cannot be +made nullable. + +### Default Field Values +To assign a default value to a field, use the built-in language features +to assign default values. + +#### Java + +Use the class constructor(s) to assign default values: + +```java +import io.realm.RealmObject; + +public class Frog extends RealmObject { + private String name = "Kitty"; + private int age; + private String species; + private String owner; + public Frog(String name, int age, String species, String owner) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } +} +``` + +#### Kotlin + +Assign default values in the field declaration: + +```kotlin +import io.realm.RealmObject + +open class Frog : RealmObject { + var name = "Kitty" + var age = 0 + var species: String? = null + var owner: String? = null + + constructor(name: String, age: Int, species: String?, owner: String?) { + this.name = name + this.age = age + this.species = species + this.owner = owner + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +> Note: +> While default values ensure that a newly created object cannot contain +a value of `null` (unless you specify a default value of `null`), +they do not impact the nullability of a field. To make a field +non-nullable, see Required Fields. +> + +### Index a Field +**Indexes** support the efficient execution of queries in +Realm. Without indexes, Realm must perform a +*collection scan*, i.e. scan every document in a collection, to select +those documents that match a query. If an appropriate index exists for a +query, Realm can use the index to limit the number of +documents that it must inspect. + +Indexes are special data structures that store a small portion of a +realm's data in an easy to traverse form. The index stores the value +of a specific field ordered by the value of the field. The ordering of +the index entries supports efficient equality matches and range-based +query operations. + +Adding an index can speed up some queries at the cost of slightly slower write +times and additional storage and memory overhead. Indexes require space in your +realm file, so adding an index to a property will increase disk space consumed +by your realm file. Each index entry is a minimum of 12 bytes. + +You can index fields with the following types: + +- `String` +- `UUID` +- `ObjectId` +- `Integer` or `int` +- `Long` or `long` +- `Short` or `short` +- `Byte` or `byte[]` +- `Boolean` or `bool` +- `Date` +- `RealmAny` + +Realm creates indexes for fields annotated with +`@Index`. + +To index a field, use the `@Index` +annotation: + +#### Java + +```java +import io.realm.RealmObject; +import io.realm.annotations.Index; + +public class Frog extends RealmObject { + private String name; + private int age; + @Index private String species; + private String owner; + public Frog(String name, int age, String species, String owner) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.Index + +open class Frog : RealmObject { + var name: String? = null + var age = 0 + @Index var species : String? = null + var owner: String? = null + + constructor(name: String?, age: Int, species: String?, owner: String?) { + this.name = name + this.age = age + this.species = species + this.owner = owner + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +### Ignore a Field +If you don't want to save a field in your model to a realm, you can +ignore a field. + +Ignore a field from a Realm object model with the +`@Ignore` annotation: + +#### Java + +```java +import io.realm.RealmObject; +import io.realm.annotations.Ignore; + +public class Frog extends RealmObject { + private String name; + private int age; + private String species; + // can you ever really own a frog persistently? + @Ignore private String owner; + public Frog(String name, int age, String species, String owner) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.Ignore + +open class Frog : RealmObject { + var name: String? = null + var age = 0 + var species: String? = null + // can you ever really own a frog persistently? + @Ignore var owner : String? = null + + constructor(name: String?, age: Int, species: String?, owner: String?) { + this.name = name + this.age = age + this.species = species + this.owner = owner + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +> Note: +> Fields marked `static` or `transient` are always ignored, and do +not need the `@Ignore` annotation. +> + +### Rename a Field +By default, Realm uses the name defined in the model class +to represent fields internally. In some cases you might want to change +this behavior: + +- To make it easier to work across platforms, since naming conventions differ. +- To change a field name in Kotlin without forcing a migration. + +Choosing an internal name that differs from the name used in model classes +has the following implications: + +- Migrations must use the internal name when creating classes and fields. +- Schema errors reported will use the internal name. + +Use the `@RealmField` +annotation to rename a field: + +#### Java + +```java +import io.realm.RealmObject; +import io.realm.annotations.RealmField; + +public class Frog extends RealmObject { + private String name; + private int age; + @RealmField("latinName") private String species; + private String owner; + public Frog(String name, int age, String species, String owner) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.RealmField + +open class Frog : RealmObject { + var name: String? = null + var age = 0 + @RealmField("latinName") var species: String? = null + var owner: String? = null + + constructor(name: String?, age: Int, species: String?, owner: String?) { + this.name = name + this.age = age + this.species = species + this.owner = owner + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +Alternatively, you can also assign a naming policy at the module or +class levels to change the way that Realm interprets field +names. + +You can define a +`naming policy` +at the module level, +which will affect all classes included in the module: + +#### Java + +```java +import io.realm.annotations.RealmModule; +import io.realm.annotations.RealmNamingPolicy; + +@RealmModule( + allClasses = true, + classNamingPolicy = RealmNamingPolicy.LOWER_CASE_WITH_UNDERSCORES, + fieldNamingPolicy = RealmNamingPolicy.LOWER_CASE_WITH_UNDERSCORES +) +public class MyModule { +} +``` + +#### Kotlin + +```kotlin +import io.realm.annotations.RealmModule +import io.realm.annotations.RealmNamingPolicy + +@RealmModule( + allClasses = true, + classNamingPolicy = RealmNamingPolicy.LOWER_CASE_WITH_UNDERSCORES, + fieldNamingPolicy = RealmNamingPolicy.LOWER_CASE_WITH_UNDERSCORES +) +open class MyModule +``` + +You can also define a +`naming policy` +at the class level, which overrides module level settings: + +#### Java + +```java +import io.realm.RealmObject; +import io.realm.annotations.RealmClass; +import io.realm.annotations.RealmNamingPolicy; + +@RealmClass(fieldNamingPolicy = RealmNamingPolicy.PASCAL_CASE) +public class Frog extends RealmObject { + private String name; + private int age; + private String species; + private String owner; + public Frog(String name, int age, String species, String owner) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.RealmClass +import io.realm.annotations.RealmNamingPolicy + +@RealmClass(fieldNamingPolicy = RealmNamingPolicy.PASCAL_CASE) +open class Frog : RealmObject { + var name: String? = null + var age = 0 + var species: String? = null + var owner: String? = null + + constructor(name: String?, age: Int, species: String?, owner: String?) { + this.name = name + this.age = age + this.species = species + this.owner = owner + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +### Rename a Class +By default, Realm uses the name defined in the model class +to represent classes internally. In some cases you might want to change +this behavior: + +- To support multiple model classes with the same simple name in different packages. +- To make it easier to work across platforms, since naming conventions differ. +- To use a class name that is longer than the 57 character limit enforced by Realm. +- To change a class name in Kotlin without forcing a migration. + +Use the `@RealmClass` +annotation to rename a class: + +#### Java + +```java +import io.realm.RealmObject; +import io.realm.annotations.RealmClass; + +@RealmClass(name = "ShortBodiedTaillessAmphibian") +public class Frog extends RealmObject { + private String name; + private int age; + private String species; + private String owner; + public Frog(String name, int age, String species, String owner) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.annotations.RealmClass + +@RealmClass(name = "Short_Bodied_Tailless_Amphibian") +open class Frog : RealmObject { + var name: String? = null + var age = 0 + var species: String? = null + var owner: String? = null + + constructor(name: String?, age: Int, species: String?, owner: String?) { + this.name = name + this.age = age + this.species = species + this.owner = owner + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +## Omit Classes from your Realm Schema +By default, your application's Realm Schema includes all +classes that extend `RealmObject`. If you only want to include a +subset of classes that extend `RealmObject` in your Realm +Schema, you can include that subset of classes in a module and open +your realm using that module: + +#### Java + +```java +import io.realm.annotations.RealmModule; + +@RealmModule(classes = { Frog.class, Fly.class }) +public class MyModule { +} +``` + +#### Kotlin + +```kotlin +import io.realm.annotations.RealmModule + +@RealmModule(classes = [Frog::class, Fly::class]) +open class MyModule +``` + diff --git a/docs/guides/model-data/modify-an-object-schema.md b/docs/guides/model-data/modify-an-object-schema.md new file mode 100644 index 0000000000..35f35a6811 --- /dev/null +++ b/docs/guides/model-data/modify-an-object-schema.md @@ -0,0 +1,243 @@ +# Change an Object Model - Java SDK +#### Local + +The following examples demonstrate how to add, delete, and modify +properties in a schema. First, make the required schema change. +Then, increment the schema version. Finally, if the change is +breaking (destructive) create a corresponding migration function to move data from the original schema +to the updated schema. + +> Note: +> Assume that each schema change shown in the following example +occurs after the application has used the existing schema. The +new schema version numbers apply only after you open the +realm and explicitly specify the new version number. +In other words, you can't specify version 3 without previously +specifying and using versions 0, 1, and 2. +> + +A realm using schema version `0` has a `Person` object type: + +#### Java + +```java +public class Person extends RealmObject { // Realm schema version 0 + @Required + public String firstName; + @Required + public int age; +} + +``` + +#### Kotlin + +```kotlin +class Person: RealmObject { // Realm schema version 0 + var firstName: String = "" + var age: int = 0 +} + +``` + +### A. Add a Property +The following example adds a `lastName` property to the +original Person schema: + +#### Java + +```java +public class Person extends RealmObject { // Realm schema version 1 + @Required + public String firstName; + @Required + public String lastName; + @Required + public int age; +} +``` + +#### Kotlin + +```kotlin +class Person: RealmObject { // Realm schema version 1 + var firstName: String = "" + var lastName: String = "" + var age: int = 0 +} + +``` + +### B. Delete a Property +The following example uses a combined +`fullName` property instead of the separate `firstName` and +`lastName` property in the original Person schema: + +#### Java + +```java +public class Person extends RealmObject { // Realm schema version 2 + @Required + public String fullName; + @Required + public int age; +} + +``` + +#### Kotlin + +```kotlin +class Person: RealmObject { // Realm schema version 2 + var fullName: String = "" + var age: int = 0 +} + +``` + +### C. Modify a Property Type or Rename a Property +The following example modifies the `age` property in the +original Person schema by +renaming it to `birthday` and changing the type to `Date`: + +#### Java + +```java +public class Person extends RealmObject { // Realm schema version 3 + @Required + public String fullName; + @Required + public Date birthday = new Date(); +} + +``` + +#### Kotlin + +```kotlin +class Person: RealmObject { // Realm schema version 3 + var fullName: String = "" + var birthday: Date = Date() +} + +``` + +### D. Migration Functions +To migrate the realm to conform to the updated +`Person` schema, set the realm's +schema version to `3` +and define a migration function to set the value of +`fullName` based on the existing `firstName` and +`lastName` properties and the value of `birthday` based on +`age`: + +#### Java + +```java +public class Migration implements RealmMigration { + @Override + public void migrate(DynamicRealm realm, long oldVersion, long newVersion) { + Long version = oldVersion; + + // DynamicRealm exposes an editable schema + RealmSchema schema = realm.getSchema(); + + // Changes from version 0 to 1: Adding lastName. + // All properties will be initialized with the default value "". + if (version == 0L) { + schema.get("Person") + .addField("lastName", String.class, FieldAttribute.REQUIRED); + version++; + } + + // Changes from version 1 to 2: combine firstName/lastName into fullName + if (version == 1L) { + schema.get("Person") + .addField("fullName", String.class, FieldAttribute.REQUIRED) + .transform( DynamicRealmObject obj -> { + String name = "${obj.getString("firstName")} ${obj.getString("lastName")}"; + obj.setString("fullName", name); + }) + .removeField("firstName") + .removeField("lastName"); + version++; + } + + // Changes from version 2 to 3: replace age with birthday + if (version == 2L) { + schema.get("Person") + .addField("birthday", Date::class.java, FieldAttribute.REQUIRED) + .transform(DynamicRealmObject obj -> { + Int birthYear = Date().year - obj.getInt("age"); + obj.setDate("birthday", Date(birthYear, 1, 1)); + }) + .removeField("age"); + version++; + } + } +}; + +@RealmModule(classes = { Person.class }) +public class Module {} + +RealmConfiguration config = new RealmConfiguration.Builder() + .modules(new Module()) + .schemaVersion(3) // Must be bumped when the schema changes + .migration(new Migration()) // Migration to run instead of throwing an exception + .build(); + +``` + +#### Kotlin + +```kotlin +val migration = object: RealmMigration { + override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { + var version: Long = oldVersion + + // DynamicRealm exposes an editable schema + val schema: RealmSchema = realm.schema + + // Changes from version 0 to 1: Adding lastName. + // All properties will be initialized with the default value "". + if (version == 0L) { + schema.get("Person")!! + .addField("lastName", String::class.java, FieldAttribute.REQUIRED) + version++ + } + + // Changes from version 1 to 2: Combining firstName/lastName into fullName + if (version == 1L) { + schema.get("Person")!! + .addField("fullName", String::class.java, FieldAttribute.REQUIRED) + .transform { obj: DynamicRealmObject -> + val name = "${obj.getString("firstName")} ${obj.getString("lastName")}" + obj.setString("fullName", name) + } + .removeField("firstName") + .removeField("lastName") + version++ + } + + // Changes from version 2 to 3: Replace age with birthday + if (version == 2L) { + schema.get("Person")!! + .addField("birthday", Date::class.java, FieldAttribute.REQUIRED) + .transform { obj: DynamicRealmObject -> + var birthYear = Date().year - obj.getInt("age") + obj.setDate("birthday", Date(birthYear, 1, 1)) + } + .removeField("age") + version++ + } + } +} + +@RealmModule(classes = { Person::class.java }) +class Module + +val config = RealmConfiguration.Builder() + .schemaVersion(3) // Must be bumped when the schema changes + .migration(migration) // Migration to run instead of throwing an exception + .build() +``` diff --git a/docs/guides/model-data/relationships.md b/docs/guides/model-data/relationships.md new file mode 100644 index 0000000000..4286cf962f --- /dev/null +++ b/docs/guides/model-data/relationships.md @@ -0,0 +1,344 @@ +# Relationships - Java SDK +## Relationships +Realm allows you to define explicit relationships between the types of +objects in an App. A relationship is an object property that references +another Realm object rather than one of the primitive data types. You +define relationships by setting the type of an object property to +another object type in the property schema. + +Relationships are direct references to other objects in a realm, which +means that you don't need bridge tables or explicit joins to define a +relationship like you would in a relational database. Instead you can +access related objects by reading and writing to the property that +defines the relationship. Realm executes read operations +lazily as they come in, so querying a relationship is just as performant +as reading a regular property. + +There are three primary types of relationships between objects: + +- One-to-One Relationship +- One-to-Many Relationship +- Inverse Relationship + +You can define relationships, collections, and embedded objects in your +object schema using the following types: + +- `RealmObject` +- `RealmList ` + +Use annotations to indicate whether a given field represents a foreign +key relationship or an embedded object relationship. For more +information, see Relationship Annotations. + +### To-One Relationship +A **to-one** relationship means that an object is related in a specific +way to no more than one other object. You define a to-one relationship +for an object type in its object schema by +specifying a property where the type is the related Realm object type. + +Setting a relationship field to null removes the connection between +objects, but Realm does not delete the referenced object +unless that object is embedded. + +### To-Many Relationship +A **to-many** relationship means that an object is related in a specific +way to multiple objects. You can create a relationship between one object +and any number of objects using a field of type `RealmList` +where `T` is a Realm object in your application: + +### Inverse Relationship +An **inverse relationship** links an object back to any other objects that refer +to it in a defined to-one or to-many relationship. Relationship definitions are +unidirectional, so you must explicitly define a property in the object's model +as an inverse relationship. + +For example, the to-many relationship "User has many Tasks" does not +automatically create the inverse relationship "Task belongs to User". If you +don't specify the inverse relationship in the object model, you would need to +run a separate query to look up the user that is assigned to a given task. + +To define an inverse relationship, define a `LinkingObjects` property in your +object model. The `LinkingObjects` definition specifies the object type and +property name of the relationship that it inverts. + +Realm automatically updates implicit relationships whenever an +object is added or removed in the specified relationship. You cannot manually +set the value of an inverse relationship property. + +Fields annotated with `@LinkingObjects` must be: + +- marked `final` +- of type `RealmResults` where `T` is the type at the opposite +end of the relationship + +Since relationships are many-to-one or many-to-many, following inverse +relationships can result in zero, one, or many objects. + +Like any other `RealmResults` set, you can +query an inverse relationship. + +## Define a Relationship Field + +> Warning: +> Realm objects use getters and setters to persist updated +field values to your realms. Always use getters and setters for +updates. +> + +### Many-to-One +To set up a many-to-one or one-to-one relationship, create a field +whose type is a Realm object in your application: + +#### Java + +```java +import io.realm.RealmObject; + +public class Frog extends RealmObject { + private String name; + private int age; + private String species; + private String owner; + private Frog bestFriend; + public Frog(String name, int age, String species, String owner, Frog bestFriend) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + this.bestFriend = bestFriend; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } + public Frog getBestFriend() { return bestFriend; } + public void setBestFriend(Frog bestFriend) { this.bestFriend = bestFriend; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject + +open class Frog : RealmObject { + var name: String? = null + var age = 0 + var species: String? = null + var owner: String? = null + var bestFriend: Frog? = null + + constructor( + name: String?, + age: Int, + species: String?, + owner: String?, + bestFriend: Frog? + ) { + this.name = name + this.age = age + this.species = species + this.owner = owner + this.bestFriend = bestFriend + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +> Important: +> When you declare a to-one relationship in your object model, it must +be an optional property. If you try to make a to-one relationship +required, Realm throws an exception at runtime. +> + +Each `Frog` references either zero `Frog` instances or one other `Frog` instance. Nothing +prevents multiple `Frog` instances from referencing the same `Frog` +as a best friend; the distinction between a many-to-one and a one-to-one +relationship is up to your application. + +### Many-to-Many +#### Java + +```java +import io.realm.RealmList; +import io.realm.RealmObject; + +public class Frog extends RealmObject { + private String name; + private int age; + private String species; + private String owner; + private RealmList bestFriends; + public Frog(String name, int age, String species, String owner, RealmList bestFriends) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + this.bestFriends = bestFriends; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } + public RealmList getBestFriends() { return bestFriends; } + public void setBestFriends(RealmList bestFriends) { this.bestFriends = bestFriends; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmList +import io.realm.RealmObject + +open class Frog : RealmObject { + var name: String? = null + var age = 0 + var species: String? = null + var owner: String? = null + var bestFriends: RealmList? = null + + constructor( + name: String?, + age: Int, + species: String?, + owner: String?, + bestFriends: RealmList? + ) { + this.name = name + this.age = age + this.species = species + this.owner = owner + this.bestFriends = bestFriends + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +`RealmList` s are containers of `RealmObject` s, but otherwise behave +like a regular collection. You can use the same object in multiple +`RealmList` s. + +### Inverse Relationships +By default, Realm relationships are unidirectional. You +can follow a link from one class to a referenced class, but not in the +opposite direction. Consider the following class defining a `Toad` with +a list of `frogFriends`: + +#### Java + +```java +import io.realm.RealmList; +import io.realm.RealmObject; + +public class Toad extends RealmObject { + private RealmList frogFriends; + public Toad(RealmList frogFriends) { + this.frogFriends = frogFriends; + } + public Toad() {} + + public RealmList getFrogFriends() { return frogFriends; } + public void setFrogFriends(RealmList frogFriends) { this.frogFriends = frogFriends; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmList +import io.realm.RealmObject + +open class Toad : RealmObject { + var frogFriends: RealmList? = null + + constructor(frogFriends: RealmList?) { + this.frogFriends = frogFriends + } + + constructor() {} +} +``` + +You can provide a link in the opposite direction, from `Frog` to `Toad`, +with the `@LinkingObjects` +annotation on a `final` (in Java) or `val` (in Kotlin) field of type +`RealmResults`: + +#### Java + +```java +import io.realm.RealmObject; +import io.realm.RealmResults; +import io.realm.annotations.LinkingObjects; + +public class Frog extends RealmObject { + private String name; + private int age; + private String species; + private String owner; + @LinkingObjects("frogFriends") + private final RealmResults toadFriends = null; + + public Frog(String name, int age, String species, String owner) { + this.name = name; + this.age = age; + this.species = species; + this.owner = owner; + } + public Frog(){} // RealmObject subclasses must provide an empty constructor + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public int getAge() { return age; } + public void setAge(int age) { this.age = age; } + public String getSpecies() { return species; } + public void setSpecies(String species) { this.species = species; } + public String getOwner() { return owner; } + public void setOwner(String owner) { this.owner = owner; } +} +``` + +#### Kotlin + +```kotlin +import io.realm.RealmObject +import io.realm.RealmResults +import io.realm.annotations.LinkingObjects + +open class Frog : RealmObject { + var name: String? = null + var age = 0 + var species: String? = null + var owner: String? = null + @LinkingObjects("frogFriends") + private val toadFriends: RealmResults? = null + + constructor(name: String?, age: Int, species: String?, owner: String?) { + this.name = name + this.age = age + this.species = species + this.owner = owner + } + + constructor() {} // RealmObject subclasses must provide an empty constructor +} +``` + +> Important: +> Inverse relationship fields must be marked `final`. +> diff --git a/docs/guides/quick-start-local.md b/docs/guides/quick-start-local.md new file mode 100644 index 0000000000..b79bc54636 --- /dev/null +++ b/docs/guides/quick-start-local.md @@ -0,0 +1,675 @@ +# Quick Start - Java SDK + +This page contains information to quickly get Realm +integrated into your app. Before you begin, ensure you have: + +- Installed the Java SDK + +## Initialize Realm +Before you can use Realm in your app, you must +initialize the Realm library. Your application should +initialize Realm just once each time the application runs. + +To initialize the Realm library, provide an Android +`context` to the `Realm.init()` static function. You can provide +an Activity, Fragment, or Application `context` for initialization with no +difference in behavior. You can initialize the Realm library +in the `onCreate()` method of an [application subclass](https://developer.android.com/reference/android/app/Application) to +ensure that you only initialize Realm once each time the +application runs. + +#### Java + +```java +Realm.init(this); // context, usually an Activity or Application + +``` + +#### Kotlin + +```kotlin +Realm.init(this) // context, usually an Activity or Application + +``` + +> Tip: +> If you create your own `Application` subclass, you must add it to your +application's `AndroidManifest.xml` to execute your custom +application logic. Set the `android.name` property of your manifest's +application definition to ensure that Android instantiates your `Application` +subclass before any other class when a user launches your application. +> +> ```xml +> +> package="com.mongodb.example"> +> +> android:name=".MyApplicationSubclass" +> ... +> /> +> +> ``` +> + +## Define Your Object Model +Your application's **data model** defines the structure of data +stored within Realm. +You can define your application's data model via Kotlin or +Java classes in your application code with +Realm Object Models. + +To define your application's data model, add the following class +definitions to your application code: + +#### Java + +```java +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.Required; + +public class Task extends RealmObject { + @PrimaryKey private String name; + @Required private String status = TaskStatus.Open.name(); + + public void setStatus(TaskStatus status) { this.status = status.name(); } + public String getStatus() { return this.status; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public Task(String _name) { this.name = _name; } + public Task() {} +} + +``` + +```java + +public enum TaskStatus { + Open("Open"), + InProgress("In Progress"), + Complete("Complete"); + + String displayName; + TaskStatus(String displayName) { + this.displayName = displayName; + } +} + +``` + +#### Kotlin + +```kotlin + +enum class TaskStatus(val displayName: String) { + Open("Open"), + InProgress("In Progress"), + Complete("Complete"), +} + +open class Task() : RealmObject() { + @PrimaryKey + var name: String = "task" + + @Required + var status: String = TaskStatus.Open.name + var statusEnum: TaskStatus + get() { + // because status is actually a String and another client could assign an invalid value, + // default the status to "Open" if the status is unreadable + return try { + TaskStatus.valueOf(status) + } catch (e: IllegalArgumentException) { + TaskStatus.Open + } + } + set(value) { status = value.name } +} + +``` + +## Open a Realm +Use `RealmConfiguration` to control the specifics of the realm you +would like to open, including the name or location of the realm, +whether to allow synchronous reads or writes to a realm on the UI +thread, and more. + +#### Java + +```java +String realmName = "My Project"; +RealmConfiguration config = new RealmConfiguration.Builder().name(realmName).build(); + +Realm backgroundThreadRealm = Realm.getInstance(config); + +``` + +#### Kotlin + +```kotlin +val realmName: String = "My Project" +val config = RealmConfiguration.Builder().name(realmName).build() + +val backgroundThreadRealm : Realm = Realm.getInstance(config) + +``` + +## Create, Read, Update, and Delete Objects +Once you have opened a realm, you can modify the +objects within that realm in a +write transaction block. + +> Important: +> By default, you can only read or write to a realm in your +application's UI thread using +asynchronous transactions. That is, +you can only use `Realm` methods whose name ends with the word +`Async` in the main thread of your Android application unless you +explicitly allow the use of synchronous methods. +> +> This restriction exists for the benefit of your application users: +performing read and write operations on the UI thread can lead to +unresponsive or slow UI interactions, so it's usually best to handle +these operations either asynchronously or in a background thread. + +To create a new `Task`, instantiate an instance of the +`Task` class and add it to the realm in a write block: + +#### Java + +```java +Task Task = new Task("New Task"); +backgroundThreadRealm.executeTransaction (transactionRealm -> { + transactionRealm.insert(Task); +}); + +``` + +#### Kotlin + +```kotlin +val task : Task = Task() +task.name = "New Task" +backgroundThreadRealm.executeTransaction { transactionRealm -> + transactionRealm.insert(task) +} + +``` + +You can retrieve a live collection +of all items in the realm: + +#### Java + +```java +// all Tasks in the realm +RealmResults Tasks = backgroundThreadRealm.where(Task.class).findAll(); + +``` + +#### Kotlin + +```kotlin +// all tasks in the realm +val tasks : RealmResults = backgroundThreadRealm.where().findAll() + +``` + +You can also filter that collection using a filter: + +#### Java + +```java +// you can also filter a collection +RealmResults TasksThatBeginWithN = Tasks.where().beginsWith("name", "N").findAll(); +RealmResults openTasks = Tasks.where().equalTo("status", TaskStatus.Open.name()).findAll(); + +``` + +#### Kotlin + +```kotlin +// you can also filter a collection +val tasksThatBeginWithN : List = tasks.where().beginsWith("name", "N").findAll() +val openTasks : List = tasks.where().equalTo("status", TaskStatus.Open.name).findAll() + +``` + +To modify a task, update its properties in a write transaction block: + +#### Java + +```java +Task otherTask = Tasks.get(0); + +// all modifications to a realm must happen inside of a write block +backgroundThreadRealm.executeTransaction( transactionRealm -> { + Task innerOtherTask = transactionRealm.where(Task.class).equalTo("_id", otherTask.getName()).findFirst(); + innerOtherTask.setStatus(TaskStatus.Complete); +}); + +``` + +#### Kotlin + +```kotlin +val otherTask: Task = tasks[0]!! + +// all modifications to a realm must happen inside of a write block +backgroundThreadRealm.executeTransaction { transactionRealm -> + val innerOtherTask : Task = transactionRealm.where().equalTo("name", otherTask.name).findFirst()!! + innerOtherTask.status = TaskStatus.Complete.name +} + +``` + +Finally, you can delete a task by calling the `deleteFromRealm()` +method in a write transaction block: + +#### Java + +```java +Task yetAnotherTask = Tasks.get(0); +String yetAnotherTaskName = yetAnotherTask.getName(); +// all modifications to a realm must happen inside of a write block +backgroundThreadRealm.executeTransaction( transactionRealm -> { + Task innerYetAnotherTask = transactionRealm.where(Task.class).equalTo("_id", yetAnotherTaskName).findFirst(); + innerYetAnotherTask.deleteFromRealm(); +}); + +``` + +#### Kotlin + +```kotlin +val yetAnotherTask: Task = tasks.get(0)!! +val yetAnotherTaskName: String = yetAnotherTask.name +// all modifications to a realm must happen inside of a write block +backgroundThreadRealm.executeTransaction { transactionRealm -> + val innerYetAnotherTask : Task = transactionRealm.where().equalTo("name", yetAnotherTaskName).findFirst()!! + innerYetAnotherTask.deleteFromRealm() +} + +``` + +## Watch for Changes +You can watch a realm, collection, or object for changes by attaching a custom +`OrderedRealmCollectionChangeListener` with the `addChangeListener()` +method: + +#### Java + +```java +// all Tasks in the realm +RealmResults Tasks = uiThreadRealm.where(Task.class).findAllAsync(); + +Tasks.addChangeListener(new OrderedRealmCollectionChangeListener>() { + @Override + public void onChange(RealmResults collection, OrderedCollectionChangeSet changeSet) { + // process deletions in reverse order if maintaining parallel data structures so indices don't change as you iterate + OrderedCollectionChangeSet.Range[] deletions = changeSet.getDeletionRanges(); + for (OrderedCollectionChangeSet.Range range : deletions) { + Log.v("QUICKSTART", "Deleted range: " + range.startIndex + " to " + (range.startIndex + range.length - 1)); + } + + OrderedCollectionChangeSet.Range[] insertions = changeSet.getInsertionRanges(); + for (OrderedCollectionChangeSet.Range range : insertions) { + Log.v("QUICKSTART", "Inserted range: " + range.startIndex + " to " + (range.startIndex + range.length - 1)); } + + OrderedCollectionChangeSet.Range[] modifications = changeSet.getChangeRanges(); + for (OrderedCollectionChangeSet.Range range : modifications) { + Log.v("QUICKSTART", "Updated range: " + range.startIndex + " to " + (range.startIndex + range.length - 1)); } + } +}); + +``` + +#### Kotlin + +```kotlin +// all tasks in the realm +val tasks : RealmResults = realm.where().findAllAsync() + +tasks.addChangeListener(OrderedRealmCollectionChangeListener> { collection, changeSet -> + // process deletions in reverse order if maintaining parallel data structures so indices don't change as you iterate + val deletions = changeSet.deletionRanges + for (i in deletions.indices.reversed()) { + val range = deletions[i] + Log.v("QUICKSTART", "Deleted range: ${range.startIndex} to ${range.startIndex + range.length - 1}") + } + + val insertions = changeSet.insertionRanges + for (range in insertions) { + Log.v("QUICKSTART", "Inserted range: ${range.startIndex} to ${range.startIndex + range.length - 1}") + } + + val modifications = changeSet.changeRanges + for (range in modifications) { + Log.v("QUICKSTART", "Updated range: ${range.startIndex} to ${range.startIndex + range.length - 1}") + } +}) + +``` + +## Complete Example +If you're running this project in a fresh Android Studio project, you can +copy and paste this file into your application's `MainActivity` -- just +remember to: + +- use a package declaration at the top of the file for your own project +- update the `import` statements for `Task` and `TaskStatus` if +you're using java + +#### Java + +```java +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.Required; + +public class Task extends RealmObject { + @PrimaryKey private String name; + @Required private String status = TaskStatus.Open.name(); + + public void setStatus(TaskStatus status) { this.status = status.name(); } + public String getStatus() { return this.status; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public Task(String _name) { this.name = _name; } + public Task() {} +} + +``` + +```java + +public enum TaskStatus { + Open("Open"), + InProgress("In Progress"), + Complete("Complete"); + + String displayName; + TaskStatus(String displayName) { + this.displayName = displayName; + } +} + +``` + +```java +import io.realm.OrderedCollectionChangeSet; + +import android.os.Bundle; + +import androidx.appcompat.app.AppCompatActivity; +import android.util.Log; + +import io.realm.OrderedRealmCollectionChangeListener; + +import io.realm.Realm; +import io.realm.RealmConfiguration; +import io.realm.RealmResults; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; + +import com.mongodb.realm.examples.model.java.Task; +import com.mongodb.realm.examples.model.java.TaskStatus; + +public class MainActivity extends AppCompatActivity { + Realm uiThreadRealm; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Realm.init(this); // context, usually an Activity or Application + + String realmName = "My Project"; + RealmConfiguration config = new RealmConfiguration.Builder().name(realmName).build(); + + uiThreadRealm = Realm.getInstance(config); + + addChangeListenerToRealm(uiThreadRealm); + + FutureTask Task = new FutureTask(new BackgroundQuickStart(), "test"); + ExecutorService executorService = Executors.newFixedThreadPool(2); + executorService.execute(Task); + + } + + private void addChangeListenerToRealm(Realm realm) { + // all Tasks in the realm + RealmResults Tasks = uiThreadRealm.where(Task.class).findAllAsync(); + + Tasks.addChangeListener(new OrderedRealmCollectionChangeListener>() { + @Override + public void onChange(RealmResults collection, OrderedCollectionChangeSet changeSet) { + // process deletions in reverse order if maintaining parallel data structures so indices don't change as you iterate + OrderedCollectionChangeSet.Range[] deletions = changeSet.getDeletionRanges(); + for (OrderedCollectionChangeSet.Range range : deletions) { + Log.v("QUICKSTART", "Deleted range: " + range.startIndex + " to " + (range.startIndex + range.length - 1)); + } + + OrderedCollectionChangeSet.Range[] insertions = changeSet.getInsertionRanges(); + for (OrderedCollectionChangeSet.Range range : insertions) { + Log.v("QUICKSTART", "Inserted range: " + range.startIndex + " to " + (range.startIndex + range.length - 1)); } + + OrderedCollectionChangeSet.Range[] modifications = changeSet.getChangeRanges(); + for (OrderedCollectionChangeSet.Range range : modifications) { + Log.v("QUICKSTART", "Updated range: " + range.startIndex + " to " + (range.startIndex + range.length - 1)); } + } + }); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + // the ui thread realm uses asynchronous transactions, so we can only safely close the realm + // when the activity ends and we can safely assume that those transactions have completed + uiThreadRealm.close(); + } + + public class BackgroundQuickStart implements Runnable { + + @Override + public void run() { + String realmName = "My Project"; + RealmConfiguration config = new RealmConfiguration.Builder().name(realmName).build(); + + Realm backgroundThreadRealm = Realm.getInstance(config); + + Task Task = new Task("New Task"); + backgroundThreadRealm.executeTransaction (transactionRealm -> { + transactionRealm.insert(Task); + }); + + // all Tasks in the realm + RealmResults Tasks = backgroundThreadRealm.where(Task.class).findAll(); + + // you can also filter a collection + RealmResults TasksThatBeginWithN = Tasks.where().beginsWith("name", "N").findAll(); + RealmResults openTasks = Tasks.where().equalTo("status", TaskStatus.Open.name()).findAll(); + + Task otherTask = Tasks.get(0); + + // all modifications to a realm must happen inside of a write block + backgroundThreadRealm.executeTransaction( transactionRealm -> { + Task innerOtherTask = transactionRealm.where(Task.class).equalTo("_id", otherTask.getName()).findFirst(); + innerOtherTask.setStatus(TaskStatus.Complete); + }); + + Task yetAnotherTask = Tasks.get(0); + String yetAnotherTaskName = yetAnotherTask.getName(); + // all modifications to a realm must happen inside of a write block + backgroundThreadRealm.executeTransaction( transactionRealm -> { + Task innerYetAnotherTask = transactionRealm.where(Task.class).equalTo("_id", yetAnotherTaskName).findFirst(); + innerYetAnotherTask.deleteFromRealm(); + }); + + // because this background thread uses synchronous realm transactions, at this point all + // transactions have completed and we can safely close the realm + backgroundThreadRealm.close(); + } + } +} + +``` + +#### Kotlin + +```kotlin + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import android.util.Log +import io.realm.* +import io.realm.annotations.PrimaryKey + +import io.realm.annotations.Required +import io.realm.kotlin.where +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.FutureTask + +class MainActivity : AppCompatActivity() { + lateinit var uiThreadRealm: Realm + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + Realm.init(this) // context, usually an Activity or Application + + val realmName: String = "My Project" + val config = RealmConfiguration.Builder() + .name(realmName) + .build() + + uiThreadRealm = Realm.getInstance(config) + + addChangeListenerToRealm(uiThreadRealm) + + val task : FutureTask = FutureTask(BackgroundQuickStart(), "test") + val executorService: ExecutorService = Executors.newFixedThreadPool(2) + executorService.execute(task) + + } + + fun addChangeListenerToRealm(realm : Realm) { + // all tasks in the realm + val tasks : RealmResults = realm.where().findAllAsync() + + tasks.addChangeListener(OrderedRealmCollectionChangeListener> { collection, changeSet -> + // process deletions in reverse order if maintaining parallel data structures so indices don't change as you iterate + val deletions = changeSet.deletionRanges + for (i in deletions.indices.reversed()) { + val range = deletions[i] + Log.v("QUICKSTART", "Deleted range: ${range.startIndex} to ${range.startIndex + range.length - 1}") + } + + val insertions = changeSet.insertionRanges + for (range in insertions) { + Log.v("QUICKSTART", "Inserted range: ${range.startIndex} to ${range.startIndex + range.length - 1}") + } + + val modifications = changeSet.changeRanges + for (range in modifications) { + Log.v("QUICKSTART", "Updated range: ${range.startIndex} to ${range.startIndex + range.length - 1}") + } + }) + } + + override fun onDestroy() { + super.onDestroy() + // the ui thread realm uses asynchronous transactions, so we can only safely close the realm + // when the activity ends and we can safely assume that those transactions have completed + uiThreadRealm.close() + } + + class BackgroundQuickStart : Runnable { + + override fun run() { + val realmName: String = "My Project" + val config = RealmConfiguration.Builder().name(realmName).build() + + val backgroundThreadRealm : Realm = Realm.getInstance(config) + + val task : Task = Task() + task.name = "New Task" + backgroundThreadRealm.executeTransaction { transactionRealm -> + transactionRealm.insert(task) + } + + // all tasks in the realm + val tasks : RealmResults = backgroundThreadRealm.where().findAll() + + // you can also filter a collection + val tasksThatBeginWithN : List = tasks.where().beginsWith("name", "N").findAll() + val openTasks : List = tasks.where().equalTo("status", TaskStatus.Open.name).findAll() + + val otherTask: Task = tasks[0]!! + + // all modifications to a realm must happen inside of a write block + backgroundThreadRealm.executeTransaction { transactionRealm -> + val innerOtherTask : Task = transactionRealm.where().equalTo("name", otherTask.name).findFirst()!! + innerOtherTask.status = TaskStatus.Complete.name + } + + val yetAnotherTask: Task = tasks.get(0)!! + val yetAnotherTaskName: String = yetAnotherTask.name + // all modifications to a realm must happen inside of a write block + backgroundThreadRealm.executeTransaction { transactionRealm -> + val innerYetAnotherTask : Task = transactionRealm.where().equalTo("name", yetAnotherTaskName).findFirst()!! + innerYetAnotherTask.deleteFromRealm() + } + + // because this background thread uses synchronous realm transactions, at this point all + // transactions have completed and we can safely close the realm + backgroundThreadRealm.close() + } + + } +} + +enum class TaskStatus(val displayName: String) { + Open("Open"), + InProgress("In Progress"), + Complete("Complete"), +} + +open class Task() : RealmObject() { + @PrimaryKey + var name: String = "task" + + @Required + var status: String = TaskStatus.Open.name + var statusEnum: TaskStatus + get() { + // because status is actually a String and another client could assign an invalid value, + // default the status to "Open" if the status is unreadable + return try { + TaskStatus.valueOf(status) + } catch (e: IllegalArgumentException) { + TaskStatus.Open + } + } + set(value) { status = value.name } +} + +``` + +## Output +Running the above code should produce output resembling the following: + +```shell +Successfully authenticated anonymously. + +Updated range: 0 to 1 + +Deleted range: 0 to 1 + +Successfully logged out. +``` diff --git a/docs/guides/react-to-changes.md b/docs/guides/react-to-changes.md new file mode 100644 index 0000000000..63857233f9 --- /dev/null +++ b/docs/guides/react-to-changes.md @@ -0,0 +1,382 @@ +# React to Changes - Java SDK +Objects in Realm clients are **live objects** that +update automatically to reflect data changes and emit +notification events that you +can subscribe to whenever their underlying data changes. + +Any modern app should be able to react when data changes, +regardless of where that change originated. When a user adds +a new item to a list, you may want to update the UI, show a +notification, or log a message. When someone updates that +item, you may want to change its visual state or fire off a +network request. Finally, when someone deletes the item, you +probably want to remove it from the UI. Realm's notification +system allows you to watch for and react to changes in your +data, independent of the writes that caused the changes. + +Realm emits three kinds of notifications: + +- Realm notifications whenever a specific realm commits a write transaction. +- Collection notifications whenever any Realm object in a collection changes, including inserts, updates, and deletes. +- Object notifications whenever a specific Realm object changes, including updates and deletes. + +## Auto-Refresh +Realm objects accessed on a thread associated with a +[Looper](https://developer.android.com/reference/android/os/Looper.html) automatically +update periodically to reflect changes to underlying data. + +The Android UI thread always contains a `Looper` instance. If you need +to keep Realm objects around for long periods of time on +any other thread, you should configure a `Looper` for that thread. + +> Warning: +> Realms on a thread without a [Looper](https://developer.android.com/reference/android/os/Looper) +do not automatically advance their version. This can increase the size of the +realm in memory and on disk. Avoid using realm instances on +non-Looper threads when possible. If you *do* open a realm on a non-Looper +thread, close the realm when you're done using it. +> + +## Register a Realm Change Listener +You can register a notification handler on an entire realm. +Realm calls the notification handler whenever any write +transaction involving that realm is committed. The +handler receives no information about the change. + +This is useful when you want to know that there has been a +change but do not care to know specifically what changed. +For example, proof of concept apps often use this +notification type and simply refresh the entire UI when +anything changes. As the app becomes more sophisticated and +performance-sensitive, the app developers shift to more +granular notifications. + +> Example: +> Suppose you are writing a real-time collaborative app. To +give the sense that your app is buzzing with collaborative +activity, you want to have an indicator that lights up when +any change is made. In that case, a realm notification +handler would be a great way to drive the code that controls +the indicator. The following code shows how to observe a realm +for changes with with `addChangeListener()`: +> +> #### Java +> +> ```java +> public class MyActivity extends Activity { +> private Realm realm; +> private RealmChangeListener realmListener; +> +> @Override +> protected void onCreate(Bundle savedInstanceState) { +> super.onCreate(savedInstanceState); +> realm = Realm.getDefaultInstance(); +> realmListener = new RealmChangeListener() { +> @Override +> public void onChange(Realm realm) { +> // ... do something with the updates (UI, etc.) ... +> } +> }; +> // Observe realm notifications. +> realm.addChangeListener(realmListener); +> } +> +> @Override +> protected void onDestroy() { +> super.onDestroy(); +> // Remove the listener. +> realm.removeChangeListener(realmListener); +> // Close the Realm instance. +> realm.close(); +> } +> } +> +> ``` +> +> +> #### Kotlin +> +> ```kotlin +> class MyActivity : Activity() { +> private lateinit var realm: Realm +> private lateinit var realmListener: RealmChangeListener +> +> override fun onCreate(savedInstanceState: Bundle?) { +> super.onCreate(savedInstanceState) +> realm = Realm.getDefaultInstance() +> realmListener = RealmChangeListener { +> // ... do something with the updates (UI, etc.) ... +> } +> // Observe realm notifications. +> realm.addChangeListener(realmListener) +> } +> +> override fun onDestroy() { +> super.onDestroy() +> // Remove the listener. +> realm.removeChangeListener(realmListener) +> // Close the Realm instance. +> realm.close() +> } +> } +> +> ``` +> +> + +> Important: +> All threads that contain a `Looper` automatically refresh +`RealmObject` and `RealmResult` instances when new changes are +written to the realm. As a result, it isn't necessary to fetch +those objects again when reacting to a `RealmChangeListener`, since +those objects are already updated and ready to be redrawn to the +screen. +> + +## Register a Collection Change Listener +You can register a notification handler on a specific +collection within a realm. The handler receives a +description of changes since the last notification. +Specifically, this description consists of three lists of +indices: + +- The indices of the objects that were deleted. +- The indices of the objects that were inserted. +- The indices of the objects that were modified. + +Stop notification delivery by calling the `removeChangeListener()` or +`removeAllChangeListeners()` methods. Notifications also stop if: + +- the object on which the listener is registered gets garbage collected. +- the realm instance closes. + +Keep a strong reference to the object you're listening to +for as long as you need the notifications. + +> Important: +> In collection notification handlers, always apply changes +in the following order: deletions, insertions, then +modifications. Handling insertions before deletions may +result in unexpected behavior. +> + +Realm emits an initial notification after retrieving the +collection. After that, Realm delivers collection +notifications asynchronously whenever a write transaction +adds, changes, or removes objects in the collection. + +Unlike realm notifications, collection notifications contain +detailed information about the change. This enables +sophisticated and selective reactions to changes. Collection +notifications provide all the information needed to manage a +list or other view that represents the collection in the UI. + +The following code shows how to observe a collection for +changes with `addChangeListener()`: + +#### Java + +```java +RealmResults dogs = realm.where(Dog.class).findAll(); +// Set up the collection notification handler. +OrderedRealmCollectionChangeListener> changeListener = (collection, changeSet) -> { + // For deletions, notify the UI in reverse order if removing elements the UI + OrderedCollectionChangeSet.Range[] deletions = changeSet.getDeletionRanges(); + for (int i = deletions.length - 1; i >= 0; i--) { + OrderedCollectionChangeSet.Range range = deletions[i]; + Log.v("EXAMPLE", range.length + " dogs deleted at " + range.startIndex); + } + OrderedCollectionChangeSet.Range[] insertions = changeSet.getInsertionRanges(); + for (OrderedCollectionChangeSet.Range range : insertions) { + Log.v("EXAMPLE", range.length + " dogs inserted at " + range.startIndex); + } + OrderedCollectionChangeSet.Range[] modifications = changeSet.getChangeRanges(); + for (OrderedCollectionChangeSet.Range range : modifications) { + Log.v("EXAMPLE", range.length + " dogs modified at " + range.startIndex); + } +}; +// Observe collection notifications. +dogs.addChangeListener(changeListener); + +``` + +#### Kotlin + +```kotlin +val dogs = realm.where(Dog::class.java).findAll() +// Set up the collection notification handler. +val changeListener = + OrderedRealmCollectionChangeListener { collection: RealmResults?, changeSet: OrderedCollectionChangeSet -> + // For deletions, notify the UI in reverse order if removing elements the UI + val deletions = changeSet.deletionRanges + for (i in deletions.indices.reversed()) { + val range = deletions[i] + Log.v("EXAMPLE", "${range.length} dogs deleted at ${range.startIndex}") + } + val insertions = changeSet.insertionRanges + for (range in insertions) { + Log.v("EXAMPLE", "${range.length} dogs inserted at ${range.startIndex}") + } + val modifications = changeSet.changeRanges + for (range in modifications) { + Log.v("EXAMPLE", "${range.length} dogs modified at ${range.startIndex}") + } + } +// Observe collection notifications. +dogs.addChangeListener(changeListener) + +``` + +## Register an Object Change Listener +You can register a notification handler on a specific object +within a realm. Realm notifies your handler: + +- When the object is deleted. +- When any of the object's properties change. + +The handler receives information about what fields changed +and whether the object was deleted. + +Stop notification delivery by calling the `removeChangeListener()` or +`removeAllChangeListeners()` methods. Notifications also stop if: + +- the object on which the listener is registered gets garbage collected. +- the realm instance closes. + +Keep a strong reference of the object you're listening to +for as long as you need the notifications. + +The following code shows how create a new instance of a class +in a realm and observe that instance for changes with +`addChangeListener()`: + +#### Java + +```java +// Create a dog in the realm. +AtomicReference dog = new AtomicReference(); +realm.executeTransaction(transactionRealm -> { + dog.set(transactionRealm.createObject(Dog.class, new ObjectId())); + dog.get().setName("Max"); +}); + +// Set up the listener. +RealmObjectChangeListener listener = (changedDog, changeSet) -> { + if (changeSet.isDeleted()) { + Log.i("EXAMPLE", "The dog was deleted"); + return; + } + for (String fieldName : changeSet.getChangedFields()) { + Log.i("EXAMPLE", "Field '" + fieldName + "' changed."); + } +}; + +// Observe object notifications. +dog.get().addChangeListener(listener); + +// Update the dog to see the effect. +realm.executeTransaction(r -> { + dog.get().setName("Wolfie"); // -> "Field 'name' was changed." +}); + +``` + +#### Kotlin + +```kotlin +// Create a dog in the realm. +var dog = Dog() +realm.executeTransaction { transactionRealm -> + dog = transactionRealm.createObject(Dog::class.java, ObjectId()) + dog.name = "Max" +} + +// Set up the listener. +val listener = RealmObjectChangeListener { changedDog: Dog?, changeSet: ObjectChangeSet? -> + if (changeSet!!.isDeleted) { + Log.i("EXAMPLE", "The dog was deleted") + } else { + for (fieldName in changeSet.changedFields) { + Log.i( + "EXAMPLE", + "Field '$fieldName' changed." + ) + } + } +} + +// Observe object notifications. +dog.addChangeListener(listener) + +// Update the dog to see the effect. +realm.executeTransaction { r: Realm? -> + dog.name = "Wolfie" // -> "Field 'name' was changed." +} + +``` + +## Unregister a Change Listener +You can unregister a change listener by passing your change listener to +`Realm.removeChangeListener()`. +You can unregister all change listeners currently subscribed to changes +in a realm or any of its linked objects or collections with +`Realm.removeAllChangeListeners()`. + +## Use Realm in System Apps on Custom ROMs +Realm uses named pipes in order to support notifications and +access to the realm file from multiple processes. While this is +allowed by default for normal user apps, it is disallowed for system +apps. + +You can define a system apps by setting +`android:sharedUserId="android.uid.system"` in the Android manifest. +When working with a system app, you may see a security violation in +Logcat that looks something like this: + +``` +05-24 14:08:08.984 6921 6921 W .realmsystemapp: type=1400 audit(0.0:99): avc: denied { write } for name="realm.testapp.com.realmsystemapp-Bfqpnjj4mUvxWtfMcOXBCA==" dev="vdc" ino=14660 scontext=u:r:system_app:s0 tcontext=u:object_r:apk_data_file:s0 tclass=dir permissive=0 +05-24 14:08:08.984 6921 6921 W .realmsystemapp: type=1400 audit(0.0:100): avc: denied { write } for name="realm.testapp.com.realmsystemapp-Bfqpnjj4mUvxWtfMcOXBCA==" dev="vdc" ino=14660 scontext=u:r:system_app:s0 tcontext=u:object_r:apk_data_file:s0 tclass=dir permissive=0 +``` + +In order to fix this you need to adjust the SELinux security rules in +the ROM. This can be done by using the tool `audit2allow`, which ships +as part of AOSP: + +1. Pull the current policy from the device: `adb pull /sys/fs/selinux/policy` +2. Copy the SELinux error inside a text file called input.txt. +3. Run the `audit2allow` tool: `audit2allow -p policy -i input.txt` +4. The tool should output a rule you can add to your existing policy +to enable the use of Realm. + +An example of such a policy is provided below: + +``` +# Allow system_app to create named pipes required by Realm +# Credit: https://github.com/mikalackis/platform_vendor_ariel/blob/master_oreo/sepolicy/system_app.te +allow system_app fuse:fifo_file create; +allow system_app system_app_data_file:fifo_file create; +allow system_app system_app_data_file:fifo_file { read write }; +allow system_app system_app_data_file:fifo_file open; +``` + +> Seealso: +> `audit2allow` is produced when compiling AOSP/ROM and only runs on +Linux. You can read more about it [here](https://source.android.com/security/selinux/validate#using_audit2allow). +> + +> Note: +> Since Android Oreo, Google changed the way it configures SELinux. +The default security policies are now much more modularized. +Read more about that +[here](https://source.android.com/security/selinux/images/SELinux_Treble.pdf). +> + +## Change Notification Limits +Changes in nested documents deeper than four levels down do not trigger +change notifications. + +If you have a data structure where you need to listen for changes five +levels down or deeper, workarounds include: + +- Refactor the schema to reduce nesting. +- Add something like "push-to-refresh" to enable users to manually refresh data. diff --git a/docs/guides/realm-files.md b/docs/guides/realm-files.md new file mode 100644 index 0000000000..6b7fb377fb --- /dev/null +++ b/docs/guides/realm-files.md @@ -0,0 +1,239 @@ +# Work with Realm Files - Java SDK +A **realm** is a set of related objects that conform to a pre-defined +schema. Realms may contain more than one type of data as long as a +schema exists for each type. + +Every realm stores data in a separate realm file that +contains a binary encoding of each object in the realm. You can +automatically synchronize realm across multiple +devices and set up reactive +event handlers that call a +function any time an object in a realm is created, +modified, or deleted. + +## The Realm Lifecycle +Every realm instance consumes a significant amount of resources. +Opening and closing a realm are both expensive operations, but +keeping a realm open also incurs significant resource overhead. To +maximize the performance of your application, you should minimize the +number of open realms at any given time and limit the number of +open and close operations used. + +However, opening a realm is not always consistently expensive. +If the realm is already open within the same process or thread, +opening an additional instance requires fewer resources: + +- If the realm is not open within the same process, opening the +realm is expensive. +- If the realm is already open on a different thread within the +same process, opening the realm is less expensive, but still +nontrivial. +- If the realm is already open on the same thread within the same +process, opening the realm requires minimal additional resources. + +When you open a realm for the first time, Realm +performs the memory-mapping and schema validation required to read and +write data to the realm. Additional instances of that +realm on the same thread use the same underlying resources. +Instances of that realm on separate threads use some of the same +underlying resources. + +When all connections to a realm are closed in +a thread, Realm frees the thread resources used to +connect to that realm. When all connections to a realm are +closed in a process, Realm frees all resources used to +connect to that realm. + +As a best practice, we recommend tying the realm instance +lifecycle to the lifecycles of the views that observe the realm. For +instance, consider a `RecyclerView` that displays `RealmResults` +data via a `Fragment`. You could: + +- Open a single realm that contains the data for that view +in the `Fragment.onCreateView()` lifecycle method. +- Close that same realm in the `Fragment.onDestroyView()` +lifecycle method. + +> Note: +> If your realm is especially large, fetching a realm instance +in `Fragment.onCreateView()` may briefly block rendering. If +opening your realm in `onCreateView()` causes performance +issues, consider managing the realm from `Fragment.onStart()` +and `Fragment.onStop()` instead. +> + +If multiple `Fragment` instances require access to the same dataset, +you could manage a single realm in the enclosing `Activity`: + +- Open the realm in the `Activity.onCreate()` lifecycle method. +- Close the realm in the `Activity.onDestroy()` lifecycle method. + +## Multi-process +You cannot access encrypted or +[delete me]s +simultaneously from different processes. However, local realms +function normally across processes, so you can read, write, and +receive notifications from multiple APKs. + +## Realm Schema +A **Realm Schema** is a list of valid object schemas that each define an object type that an App +may persist. All objects in a realm must conform to the Realm Schema. + +By default, the SDK automatically adds all classes in your project +that derive from `RealmObject` to the +realm schema. + +Client applications provide a Realm Schema when they open a +realm. If a realm already contains data, then Realm +validates each existing object to ensure that an object schema was +provided for its type and that it meets all of the constraints specified +in the schema. + +> Example: +> A realm that contains basic data about books in libraries might use a +schema like the following: +> +> ```json +> [ +> { +> "type": "Library", +> "properties": { +> "address": "string", +> "books": "Book[]" +> } +> }, +> { +> "type": "Book", +> "primaryKey": "isbn", +> "properties": { +> "isbn": "string", +> "title": "string", +> "author": "string", +> "numberOwned": { "type": "int?", "default": 0 }, +> "numberLoaned": { "type": "int?", "default": 0 } +> } +> } +> ] +> ``` +> + +## Find Your Realm File +Realm stores a binary encoded version of every object +and type in a realm in a single `.realm` file. + +The filesystem used by Android emulators is not directly accessible +from the machine running Realm Studio. You must download the file +from the emulator before you can access it. + +First, find the path of the file on the emulator: + +```java +// Run this on the device to find the path on the emulator +Realm realm = Realm.getDefaultInstance(); +Log.i("Realm", realm.getPath()); +``` + +Then, download the file using ADB. You can do this while the app +is running. + +```java +> adb pull +``` + +You can also upload the modified file again using ADB, but only +when the app isn't running. Uploading a modified file while the +app is running can corrupt the file. + +```java +> adb push +``` + +> Seealso: +> Realm creates additional files for each realm. +To learn more about these files, see Realm Internals. +> + +## Realm File Size +Realm usually takes up less space on disk than an +equivalent SQLite database. However, in order to give you a consistent +view of your data, Realm operates on multiple versions of a +realm. If many versions of a realm are opened simultaneously, +the realm file can require additional space on disk. + +These versions take up an amount of space dependent on the amount of +changes in each transaction. Many small transactions have the same +overhead as a small number of large transactions. + +Unexpected file size growth usually happens for one of three reasons: + +1. *You open a realm on a background thread and forget to close it +again.* As a result, Realm retains a reference to the +older version of data on the background thread. Because +Realm automatically updates realms to the most +recent version on threads with loopers, the UI thread and other +Looper threads do not have this problem. +2. *You hold references to too many versions of frozen objects.* +Frozen objects preserve the version of a realm that existed when +the object was first frozen. If you need to freeze a large number of +objects, consider using `Realm.copyFromRealm()` instead to only preserve the +data you need. +3. *You read some data from a realm. Then, you block the thread with +a long-running operation. Meanwhile, you write many times to the +realm on other threads.* This causes Realm to +create many intermediate versions. You can avoid this by: batching the +writes, avoiding leaving the realm open while otherwise blocking the +background thread. + +### Limit the Maximum Number of Active Versions +You can set `maxNumberOfActiveVersions()` +when building your `RealmConfiguration` to throw an +`IllegalStateException` if your application opens more versions of +a realm than the permitted number. Versions are created when +executing a write transaction. + +Realm automatically removes older versions of data once +they are no longer used by your application. However, +Realm does not free the space used by older versions of +data; instead, that space is used for new writes to the realm. + +### Compact a Realm +You can remove unused space by **compacting** the realm file: + +- Manually: call `compactRealm()` +- Automatically: specify the `compactOnLaunch()` +builder option when opening the first connection to a realm in your +Android application + +> Important: +> Every production application should implement compacting to +periodically reduce realm file size. +> + +## Backup and Restore Realms +Realm persists realms to disk using files on your +Android device. To back up a realm, find your realm file and copy it to a safe location. You should close +all instances of the realm before copying it. + +Alternatively, you can also use `realm.writeCopyTo()` to write a compacted +version of a realm to a destination file. + +> Seealso: +> If you want to back up a realm to an external location like +Google Drive, see the following article series: ([Part 1](https://medium.com/glucosio-project/example-class-to-export-import-a-realm-database-on-java-c429ade2b4ed#.80ibsc7wm), +[Part 2](https://medium.com/glucosio-project/backup-restore-a-realm-database-on-google-drive-with-drive-api-c238515a5975#.qbuugb322), +[Part 3](https://medium.com/glucosio-project/build-a-nice-ux-to-backup-and-sync-your-app-data-on-google-drive-3-3-a3b598cab68b#.5mjk4w4se)). +> + +## Modules +Realm Modules describe the set of Realm objects +that can be stored in a realm. By default, Realm +automatically creates a Realm Module that contains all +Realm objects defined in your application. +You can define a `RealmModule` +to restrict a realm to a subset of classes defined in an application. +If you produce a library that uses Realm, you can use a +Realm Module to explicitly include only the Realm +objects defined in your library in your realm. This allows +applications that include your library to also use Realm +without managing object name conflicts and migrations with your library's +defined Realm objects. diff --git a/docs/guides/realm-files/bundle-a-realm.md b/docs/guides/realm-files/bundle-a-realm.md new file mode 100644 index 0000000000..fec4242784 --- /dev/null +++ b/docs/guides/realm-files/bundle-a-realm.md @@ -0,0 +1,53 @@ +# Bundle a Realm File - Java SDK +Realm supports **bundling** realm files. When you bundle +a realm file, you include a database and all of its data in your +application download. + +This allows users to start applications for the first time with a set of +initial data. + +## Overview +To create and bundle a realm file with your application: + +1. Create a realm file that +contains the data you'd like to bundle. +2. Bundle the realm file in the +//src/main/assets folder of your production +application. +3. In your production application, +open the realm from the bundled asset file. + +## Create a Realm File for Bundling +1. Build a temporary realm app that shares the data model of your +application. +2. Open a realm and add the data you wish to bundle. +3. Use the `writeCopyTo()` +method to copy the realm to a new file. + +`writeCopyTo()` automatically compacts your realm to the smallest +possible size before copying. + +## Bundle a Realm File in Your Production Application +Now that you have a copy of the realm that contains the initial data, +bundle it with your production application. + +1. Search your application logs to find the location of the realm file +copy you just created. +2. Using the "Device File Explorer" widget in the bottom right of your +Android Studio window, navigate to the file. +3. Right click on the file and select "Save As". Navigate to the +//src/main/assets folder of your production application. +Save a copy of the realm file there. + +> Tip: +> If your application does not already contain an asset folder, you can +create one by right clicking on your top-level application +folder () in Android Studio and selecting +New > Folder > Assets Folder in the menu. +> + +## Open a Realm from a Bundled Realm File +Now that you have a copy of the realm included with your production +application, you need to add code to use it. Use the `assetFile()` +method when configuring your realm to open the realm +from the bundled file. diff --git a/docs/guides/realm-files/encryption.md b/docs/guides/realm-files/encryption.md new file mode 100644 index 0000000000..2b6b67e19e --- /dev/null +++ b/docs/guides/realm-files/encryption.md @@ -0,0 +1,400 @@ +# Encrypt a Realm - Java SDK +## Overview +You can encrypt your realms to ensure that the data stored to disk can't be +read outside of your application. You encrypt the realm file on disk with AES-256 + +SHA-2 by supplying a 64-byte encryption key when opening a +realm. + +Realm transparently encrypts and decrypts data with standard +[AES-256 encryption](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) using the +first 256 bits of the given 512-bit encryption key. Realm +uses the other 256 bits of the 512-bit encryption key to validate +integrity using a [hash-based message authentication code +(HMAC)](https://en.wikipedia.org/wiki/HMAC). + +> Warning: +> Do not use cryptographically-weak hashes for realm encryption keys. +For optimal security, we recommend generating random rather than derived +encryption keys. +> + +## Considerations +The following are key impacts to consider when encrypting a realm. + +### Storing & Reusing Keys +You **must** pass the same encryption key to `RealmConfiguration.Builder.encryptionKey()` each +time you open the realm. +If you don't provide a key or specify the wrong key for an encrypted +realm, the Realm SDK throws an error. + +Apps should store the encryption key in the +[Android KeyStore](https://developer.android.com/training/articles/keystore.html) so +that other apps cannot read the key. + +### Performance Impact +Reads and writes on encrypted realms can be up to 10% slower than unencrypted realms. + +### Accessing an Encrypted Realm from Multiple Processes +> Version changed: 10.14.0 + +Starting with Realm Java SDK version 10.14.0, Realm supports opening +the same encrypted realm in multiple processes. + +If your app uses Realm Java SDK version 10.14.0 or earlier, attempting to +open an encrypted realm from multiple processes throws this error: +`Encrypted interprocess sharing is currently unsupported.` + +## Example +The following steps describe the recommended way to use the +[Android KeyStore](https://developer.android.com/training/articles/keystore.html) for encryption with +Realm: + +1. Generate an asymmetric RSA key that Android can securely store and +retrieve using the Android KeyStore. Versions M and above require user PIN or fingerprint to unlock +the KeyStore. +2. Generate a symmetric key (AES) you can use to encrypt the realm. +3. Encrypt the symmetric AES key using your private RSA key. +4. Store the encrypted AES key on filesystem (in a +`SharedPreferences`, for example). + +When you need to use your encrypted realm: + +1. Retrieve your encrypted AES key. +2. Decrypt your encrypted AES key using the public RSA key. +3. Use the decrypted AES key in the `RealmConfiguration` to open the +encrypted realm. + +> Seealso: +> For an end-to-end example of storing and reusing encryption keys, see +the [store_password](https://github.com/realm/realm-java/tree/feature/example/store_password/examples/StoreEncryptionPassword) example project, which demonstrates the +fingerprint API. +> + +### Generate and Store an Encryption Key +The following code demonstrates how to securely generate and store an +encryption key for a realm: + +#### Java + +```java +// Create a key to encrypt a realm and save it securely in the keystore +public byte[] getNewKey() { + // open a connection to the android keystore + KeyStore keyStore; + try { + keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + } catch (KeyStoreException | NoSuchAlgorithmException + | CertificateException | IOException e) { + Log.v("EXAMPLE", "Failed to open the keystore."); + throw new RuntimeException(e); + } + + // create a securely generated random asymmetric RSA key + byte[] realmKey = new byte[Realm.ENCRYPTION_KEY_LENGTH]; + new SecureRandom().nextBytes(realmKey); + + // create a cipher that uses AES encryption -- we'll use this to encrypt our key + Cipher cipher; + try { + cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + + "/" + KeyProperties.BLOCK_MODE_CBC + + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + Log.e("EXAMPLE", "Failed to create a cipher."); + throw new RuntimeException(e); + } + + // generate secret key + KeyGenerator keyGenerator; + try { + keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + "AndroidKeyStore"); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + Log.e("EXAMPLE", "Failed to access the key generator."); + throw new RuntimeException(e); + } + KeyGenParameterSpec keySpec = new KeyGenParameterSpec.Builder( + "realm_key", + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + .setUserAuthenticationRequired(true) + .setUserAuthenticationValidityDurationSeconds( + AUTH_VALID_DURATION_IN_SECOND) + .build(); + try { + keyGenerator.init(keySpec); + } catch (InvalidAlgorithmParameterException e) { + Log.e("EXAMPLE", "Failed to generate a secret key."); + throw new RuntimeException(e); + } + keyGenerator.generateKey(); + + // access the generated key in the android keystore, then + // use the cipher to create an encrypted version of the key + byte[] initializationVector; + byte[] encryptedKeyForRealm; + try { + SecretKey secretKey = + (SecretKey) keyStore.getKey("realm_key", null); + cipher.init(Cipher.ENCRYPT_MODE, secretKey); + encryptedKeyForRealm = cipher.doFinal(realmKey); + initializationVector = cipher.getIV(); + } catch (InvalidKeyException | UnrecoverableKeyException + | NoSuchAlgorithmException | KeyStoreException + | BadPaddingException | IllegalBlockSizeException e) { + Log.e("EXAMPLE", "Failed encrypting the key with the secret key."); + throw new RuntimeException(e); + } + + // keep the encrypted key in shared preferences + // to persist it across application runs + byte[] initializationVectorAndEncryptedKey = + new byte[Integer.BYTES + + initializationVector.length + + encryptedKeyForRealm.length]; + ByteBuffer buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey); + buffer.order(ByteOrder.BIG_ENDIAN); + buffer.putInt(initializationVector.length); + buffer.put(initializationVector); + buffer.put(encryptedKeyForRealm); + activity.getSharedPreferences("realm_key", Context.MODE_PRIVATE).edit() + .putString("iv_and_encrypted_key", + Base64.encodeToString(initializationVectorAndEncryptedKey, Base64.NO_WRAP)) + .apply(); + + return realmKey; // pass to a realm configuration via encryptionKey() +} + +``` + +#### Kotlin + +```kotlin +// Create a key to encrypt a realm and save it securely in the keystore +fun getNewKey(): ByteArray { + // open a connection to the android keystore + val keyStore: KeyStore + try { + keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + } catch (e: Exception) { + Log.v("EXAMPLE", "Failed to open the keystore.") + throw RuntimeException(e) + } + + // create a securely generated random asymmetric RSA key + val realmKey = ByteArray(Realm.ENCRYPTION_KEY_LENGTH) + SecureRandom().nextBytes(realmKey) + + // create a cipher that uses AES encryption -- we'll use this to encrypt our key + val cipher: Cipher + cipher = try { + Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + + "/" + KeyProperties.BLOCK_MODE_CBC + + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7) + } catch (e: Exception) { + Log.e("EXAMPLE", "Failed to create a cipher.") + throw RuntimeException(e) + } + + // generate secret key + val keyGenerator: KeyGenerator + keyGenerator = try { + KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + "AndroidKeyStore") + } catch (e: NoSuchAlgorithmException) { + Log.e("EXAMPLE", "Failed to access the key generator.") + throw RuntimeException(e) + } + val keySpec = KeyGenParameterSpec.Builder( + "realm_key", + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + .setUserAuthenticationRequired(true) + .setUserAuthenticationValidityDurationSeconds( + AUTH_VALID_DURATION_IN_SECOND) + .build() + try { + keyGenerator.init(keySpec) + } catch (e: InvalidAlgorithmParameterException) { + Log.e("EXAMPLE", "Failed to generate a secret key.") + throw RuntimeException(e) + } + keyGenerator.generateKey() + + // access the generated key in the android keystore, then + // use the cipher to create an encrypted version of the key + val initializationVector: ByteArray + val encryptedKeyForRealm: ByteArray + try { + val secretKey = keyStore.getKey("realm_key", null) as SecretKey + cipher.init(Cipher.ENCRYPT_MODE, secretKey) + encryptedKeyForRealm = cipher.doFinal(realmKey) + initializationVector = cipher.iv + } catch (e: Exception) { + Log.e("EXAMPLE", "Failed encrypting the key with the secret key.") + throw RuntimeException(e) + } + + // keep the encrypted key in shared preferences + // to persist it across application runs + val initializationVectorAndEncryptedKey = ByteArray(Integer.BYTES + + initializationVector.size + + encryptedKeyForRealm.size) + val buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey) + buffer.order(ByteOrder.BIG_ENDIAN) + buffer.putInt(initializationVector.size) + buffer.put(initializationVector) + buffer.put(encryptedKeyForRealm) + activity!!.getSharedPreferences("realm_key", Context.MODE_PRIVATE).edit() + .putString("iv_and_encrypted_key", + Base64.encodeToString(initializationVectorAndEncryptedKey, Base64.NO_WRAP)) + .apply() + return realmKey // pass to a realm configuration via encryptionKey() +} + +``` + +### Access an Existing Encryption Key +The following code demonstrates how to access and decrypt a securely +stored encryption key for a realm: + +#### Java + +```java +// Access the encrypted key in the keystore, decrypt it with the secret, +// and use it to open and read from the realm again +public byte[] getExistingKey() { + // open a connection to the android keystore + KeyStore keyStore; + try { + keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + } catch (KeyStoreException | NoSuchAlgorithmException + | CertificateException | IOException e) { + Log.e("EXAMPLE", "Failed to open the keystore."); + throw new RuntimeException(e); + } + + // access the encrypted key that's stored in shared preferences + byte[] initializationVectorAndEncryptedKey = Base64.decode(activity + .getSharedPreferences("realm_key", Context.MODE_PRIVATE) + .getString("iv_and_encrypted_key", null), Base64.DEFAULT); + ByteBuffer buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey); + buffer.order(ByteOrder.BIG_ENDIAN); + + // extract the length of the initialization vector from the buffer + int initializationVectorLength = buffer.getInt(); + // extract the initialization vector based on that length + byte[] initializationVector = new byte[initializationVectorLength]; + buffer.get(initializationVector); + // extract the encrypted key + byte[] encryptedKey = new byte[initializationVectorAndEncryptedKey.length + - Integer.BYTES + - initializationVectorLength]; + buffer.get(encryptedKey); + + // create a cipher that uses AES encryption to decrypt our key + Cipher cipher; + try { + cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + + "/" + KeyProperties.BLOCK_MODE_CBC + + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + Log.e("EXAMPLE", "Failed to create cipher."); + throw new RuntimeException(e); + } + + // decrypt the encrypted key with the secret key stored in the keystore + byte[] decryptedKey; + try { + final SecretKey secretKey = + (SecretKey) keyStore.getKey("realm_key", null); + final IvParameterSpec initializationVectorSpec = + new IvParameterSpec(initializationVector); + cipher.init(Cipher.DECRYPT_MODE, secretKey, initializationVectorSpec); + decryptedKey = cipher.doFinal(encryptedKey); + } catch (InvalidKeyException e) { + Log.e("EXAMPLE", "Failed to decrypt. Invalid key."); + throw new RuntimeException(e); + } catch (UnrecoverableKeyException | NoSuchAlgorithmException + | BadPaddingException | KeyStoreException + | IllegalBlockSizeException | InvalidAlgorithmParameterException e) { + Log.e("EXAMPLE", + "Failed to decrypt the encrypted realm key with the secret key."); + throw new RuntimeException(e); + } + return decryptedKey; // pass to a realm configuration via encryptionKey() +} + +``` + +#### Kotlin + +```kotlin +// Access the encrypted key in the keystore, decrypt it with the secret, +// and use it to open and read from the realm again +fun getExistingKey(): ByteArray { + // open a connection to the android keystore + val keyStore: KeyStore + try { + keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + } catch (e: Exception) { + Log.e("EXAMPLE", "Failed to open the keystore.") + throw RuntimeException(e) + } + + // access the encrypted key that's stored in shared preferences + val initializationVectorAndEncryptedKey = Base64.decode(activity + ?.getSharedPreferences("realm_key", Context.MODE_PRIVATE) + ?.getString("iv_and_encrypted_key", null), Base64.DEFAULT) + val buffer = ByteBuffer.wrap(initializationVectorAndEncryptedKey) + buffer.order(ByteOrder.BIG_ENDIAN) + + // extract the length of the initialization vector from the buffer + val initializationVectorLength = buffer.int + // extract the initialization vector based on that length + val initializationVector = ByteArray(initializationVectorLength) + buffer[initializationVector] + // extract the encrypted key + val encryptedKey = ByteArray(initializationVectorAndEncryptedKey.size + - Integer.BYTES + - initializationVectorLength) + buffer[encryptedKey] + + // create a cipher that uses AES encryption to decrypt our key + val cipher: Cipher + cipher = try { + Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + + "/" + KeyProperties.BLOCK_MODE_CBC + + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7) + } catch (e: Exception) { + Log.e("EXAMPLE", "Failed to create cipher.") + throw RuntimeException(e) + } + + // decrypt the encrypted key with the secret key stored in the keystore + val decryptedKey: ByteArray + decryptedKey = try { + val secretKey = keyStore.getKey("realm_key", null) as SecretKey + val initializationVectorSpec = IvParameterSpec(initializationVector) + cipher.init(Cipher.DECRYPT_MODE, secretKey, initializationVectorSpec) + cipher.doFinal(encryptedKey) + } catch (e: InvalidKeyException) { + Log.e("EXAMPLE", "Failed to decrypt. Invalid key.") + throw RuntimeException(e) + } catch (e: Exception ) { + Log.e("EXAMPLE", + "Failed to decrypt the encrypted realm key with the secret key.") + throw RuntimeException(e) + } + return decryptedKey // pass to a realm configuration via encryptionKey() +} + +``` diff --git a/docs/guides/realm-files/open-and-close-a-realm.md b/docs/guides/realm-files/open-and-close-a-realm.md new file mode 100644 index 0000000000..3ea9a9d5ec --- /dev/null +++ b/docs/guides/realm-files/open-and-close-a-realm.md @@ -0,0 +1,364 @@ +# Open & Close a Realm - Java SDK +Interacting with realms in an Android +application uses the following high-level series of steps: + +1. Create a configuration for the realm you want to open. +2. Open the realm using the config. +3. Close the realm to free up resources when you're finished. + +## The Default Realm +You can save any `RealmConfiguration` +as the default for your application using the +`setDefaultConfiguration()` +method: + +#### Java + +```java +RealmConfiguration config = new RealmConfiguration.Builder() + .name("default-realm") + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) + .compactOnLaunch() + .inMemory() + .build(); +// set this config as the default realm +Realm.setDefaultConfiguration(config); +``` + +#### Kotlin + +```kotlin +val config = RealmConfiguration.Builder() + .name("default-realm") + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) + .compactOnLaunch() + .inMemory() + .build() +// set this config as the default realm +Realm.setDefaultConfiguration(config) +``` + +You can then use +`getDefaultConfiguration()` +to access that configuration, or +`getDefaultInstance()` +to open a realm with that configuration: + +#### Java + +```java +Realm realm = Realm.getDefaultInstance(); +Log.v("EXAMPLE","Successfully opened the default realm at: " + realm.getPath()); +``` + +#### Kotlin + +```kotlin +val realm = Realm.getDefaultInstance() +Log.v("EXAMPLE","Successfully opened the default realm at: ${realm.path}") +``` + +## Local Realms +Local realms store data only on the client device. You can customize +the settings for a local realm with `RealmConfiguration`. + +### Local Realm Configuration +To configure settings for a realm, create a +`RealmConfiguration` with a +`RealmConfiguration.Builder`. +The following example configures a local realm with: + +- the file name "alternate-realm" +- synchronous reads explicitly allowed on the UI thread +- synchronous writes explicitly allowed on the UI thread +- automatic compaction when launching the realm to save file space + +#### Java + +```java +RealmConfiguration config = new RealmConfiguration.Builder() + .name("alternate-realm") + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) + .compactOnLaunch() + .build(); + +Realm realm = Realm.getInstance(config); +Log.v("EXAMPLE", "Successfully opened a realm at: " + realm.getPath()); + +``` + +#### Kotlin + +```kotlin +val config = RealmConfiguration.Builder() + .name("alternate-realm") + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) + .compactOnLaunch() + .build() +val realm = Realm.getInstance(config) +Log.v("EXAMPLE", "Successfully opened a realm at: ${realm.path}") + +``` + +> Important: +> By default, you can only read or write to a realm in your +application's UI thread using +asynchronous transactions. That is, +you can only use `Realm` methods whose name ends with the word +`Async` in the main thread of your Android application unless you +explicitly allow the use of synchronous methods. +> +> This restriction exists for the benefit of your application users: +performing read and write operations on the UI thread can lead to +unresponsive or slow UI interactions, so it's usually best to handle +these operations either asynchronously or in a background thread. + +### Open a Local Realm +To open a realm, create a +`RealmConfiguration` with +`RealmConfiguration.Builder` and +pass the resulting `RealmConfiguration` to +`getInstance()` +or `getInstanceAsync()`: + +#### Java + +```java +RealmConfiguration config = new RealmConfiguration.Builder() + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) + .build(); + +Realm realm; +try { + realm = Realm.getInstance(config); + Log.v("EXAMPLE", "Successfully opened a realm at: " + realm.getPath()); +} catch (RealmFileException ex) { + Log.v("EXAMPLE", "Error opening the realm."); + Log.v("EXAMPLE", ex.toString()); +} + +``` + +#### Kotlin + +```kotlin +val config = RealmConfiguration.Builder() + .allowQueriesOnUiThread(true) + .allowWritesOnUiThread(true) + .build() + +var realm: Realm +try { + realm = Realm.getInstance(config) + Log.v("EXAMPLE", "Successfully opened a realm at: ${realm.path}") +} catch(ex: RealmFileException) { + Log.v("EXAMPLE", "Error opening the realm.") + Log.v("EXAMPLE", ex.toString()) +} + +``` + +### Read-Only Realms +It's sometimes useful to ship a prepared realm file with your app +that contains shared data that does not frequently change. You can use +the `readOnly()` +method when configuring your realm to make it read-only. This can +prevent accidental writes to the realm and causes the realm to +throw an `IllegalStateException` if a write occurs. + +> Warning: +> Read-only realms are only enforced as read-only in process. +The realm file itself is still writeable. +> + +#### Java + +```java +RealmConfiguration config = new RealmConfiguration.Builder() + .assetFile("bundled.realm") + .readOnly() + .modules(new BundledRealmModule()) + .build(); +``` + +#### Kotlin + +```kotlin +val config = RealmConfiguration.Builder() + .assetFile("readonly.realm") + .readOnly() + .modules(BundledRealmModule()) + .build() +``` + +### In-Memory Realms +You can create a realm that runs entirely in memory without being written +to a file. When memory runs low on an Android device, in-memory realms +may [swap](https://en.wikipedia.org/wiki/Memory_paging#Terminology) temporarily from main +memory to disk space. The SDK deletes all files created by an in-memory +realm when: + +- the realm closes +- all references to that realm fall out of scope + +To create an in-memory realm, use `inMemory()` +when configuring your realm: + +#### Java + +```java +RealmConfiguration config = new RealmConfiguration.Builder() + .inMemory() + .name("java.transient.realm") + .build(); +Realm realm = Realm.getInstance(config); +``` + +#### Kotlin + +```kotlin +val config = RealmConfiguration.Builder() + .inMemory() + .name("kt.transient.realm") + .build() +val realm = Realm.getInstance(config) +``` + +### Dynamic Realms +Conventional realms define a schema using `RealmObject` subclasses +or the `RealmModel` interface. A +`DynamicRealm` uses strings to +define a schema at runtime. Opening a dynamic realm uses the same +configuration as a conventional realm, but dynamic realms ignore +all configured schema, migration, and schema versions. + +Dynamic realms offer flexibility at the expense of type safety and +performance. As a result, only use dynamic realms when that +flexibility is required, such as during migrations, manual client +resets, and when working with string-based data like CSV files or JSON. + +To open a Dynamic Realm with a mutable schema, use +`DynamicRealm`: + +#### Java + +```java +RealmConfiguration config = new RealmConfiguration.Builder() + .allowWritesOnUiThread(true) + .allowQueriesOnUiThread(true) + .name("java.dynamic.realm") + .build(); +DynamicRealm dynamicRealm = DynamicRealm.getInstance(config); + +// all objects in a DynamicRealm are DynamicRealmObjects +AtomicReference frog = new AtomicReference<>(); +dynamicRealm.executeTransaction(transactionDynamicRealm -> { + // add type Frog to the schema with name and age fields + dynamicRealm.getSchema() + .create("Frog") + .addField("name", String.class) + .addField("age", int.class); + frog.set(transactionDynamicRealm.createObject("Frog")); + frog.get().set("name", "Wirt Jr."); + frog.get().set("age", 42); +}); + +// access all fields in a DynamicRealm using strings +String name = frog.get().getString("name"); +int age = frog.get().getInt("age"); + +// because an underlying schema still exists, +// accessing a field that does not exist throws an exception +try { + frog.get().getString("doesn't exist"); +} catch (IllegalArgumentException e) { + Log.e("EXAMPLE", "That field doesn't exist."); +} + +// Queries still work normally +RealmResults frogs = dynamicRealm.where("Frog") + .equalTo("name", "Wirt Jr.") + .findAll(); +``` + +#### Kotlin + +```kotlin +val config = RealmConfiguration.Builder() + .allowWritesOnUiThread(true) + .allowQueriesOnUiThread(true) + .name("kt.dynamic.realm") + .build() +val dynamicRealm = DynamicRealm.getInstance(config) + +// all objects in a DynamicRealm are DynamicRealmObjects +var frog: DynamicRealmObject? = null +dynamicRealm.executeTransaction { transactionDynamicRealm: DynamicRealm -> + // add type Frog to the schema with name and age fields + dynamicRealm.schema + .create("Frog") + .addField("name", String::class.java) + .addField("age", Integer::class.java) + frog = transactionDynamicRealm.createObject("Frog") + frog?.set("name", "Wirt Jr.") + frog?.set("age", 42) +} + +// access all fields in a DynamicRealm using strings +val name = frog?.getString("name") +val age = frog?.getInt("age") + +// because an underlying schema still exists, +// accessing a field that does not exist throws an exception +try { + frog?.getString("doesn't exist") +} catch (e: IllegalArgumentException) { + Log.e("EXAMPLE", "That field doesn't exist.") +} + +// Queries still work normally +val frogs = dynamicRealm.where("Frog") + .equalTo("name", "Wirt Jr.") + .findAll() +``` + +## Close a Realm +It is important to remember to call the `close()` method when done with a +realm instance to free resources. Neglecting to close realms can lead to an +`OutOfMemoryError`. + +#### Java + +```java +realm.close(); + +``` + +#### Kotlin + +```kotlin +realm.close() + +``` + +## Configure Which Classes to Include in Your Realm Schema +Realm modules are collections of Realm object +models. Specify a module or modules when opening a realm to control +which classes Realm should include in your schema. If you +do not specify a module, Realm uses the default module, +which includes all Realm objects defined in your +application. + +> Note: +> Libraries that include Realm must expose and use their +schema through a module. Doing so prevents the library from +generating the default `RealmModule`, which would conflict with +the default `RealmModule` used by any app that includes the library. +Apps using the library access library classes through the module. +> + diff --git a/docs/guides/realm-query-language.md b/docs/guides/realm-query-language.md new file mode 100644 index 0000000000..f86ae0ae54 --- /dev/null +++ b/docs/guides/realm-query-language.md @@ -0,0 +1,735 @@ +# Realm Query Language +Realm Query Language (RQL) is a string-based query language to constrain +searches when retrieving objects from a realm. SDK-specific methods pass queries +to the Realm query engine, which retrieves matching objects from the realm. +Realm Query Language syntax is based on [NSPredicate](https://developer.apple.com/documentation/foundation/nspredicate). + +Queries evaluate a predicate for every object in the collection being queried. +If the predicate resolves to `true`, the results collection includes the object. + +You can use Realm Query Language in most Realm SDKs with your SDK's filter +or query methods. The Swift SDK is the exception, as it uses the +NSPredicate query API. +Some SDKs also support idiomatic APIs for querying realms in their language. + +You can also use Realm Query Language to browse for data in +Realm Studio. Realm Studio is a visual tool +to view, edit, and design Realm files. + +## Examples on This Page +Many of the examples in this page use a simple data set for a to-do list app. +The two Realm object types are `Project` and `Item`. + +- An `Item` has a name, assignee's name, and completed flag. +There is also an arbitrary number for priority (higher is more important) +and a count of minutes spent working on it. +- A `Project` has zero or more `Items` and an optional quota +for minimum number of to-do items expected to be completed. + +See the schema for these two classes, `Project` and `Item`, below: + +### Java + +```java +public class Item extends RealmObject { + ObjectId id = new ObjectId(); + String name; + Boolean isComplete = false; + String assignee; + Integer priority = 0; + Integer progressMinutes = 0; + @LinkingObjects("items") + final RealmResults projects = null; +} +public class Project extends RealmObject { + ObjectId id = new ObjectId(); + String name; + RealmList items; + Integer quota = null; +} +``` + +### Kotlin + +```kotlin +open class Item(): RealmObject() { + var id: ObjectId = new ObjectId() + @FullText + lateinit var name: String + var isComplete: Boolean = false + var assignee: String? = null + var priority: Int = 0 + var progressMinutes: Int = 0 +} + +open class Project(): RealmObject() { + var id: ObjectId = new ObjectId() + lateinit var name: String + lateinit var items: RealmList + var quota: Int? = null +} +``` + + + +## Expressions +Filters consist of **expressions** in a predicate. An expression consists of +one of the following: + +- The name of a property of the object currently being evaluated. +- An operator and up to two argument expression(s). For example, in the +expression `A + B`, the entirety of `A + B` is an expression, but `A` +and `B` are also argument expressions to the operator `+`. +- A value, such as a string (`'hello'`) or a number (`5`). + +```javascript +"progressMinutes > 1 AND assignee == $0", "Ali" + +``` + +## Parameterized Queries +Create parameterized queries to interpolate variables into prepared +Realm Query Language statements. The syntax for interpolated variables is +`$`, starting at `0`. Pass the positional arguments as +additional arguments to Realm SDK methods that use Realm Query Language. + +Include just one parameter with `$0`. + +```js +"progressMinutes > 1 AND assignee == $0", "Ali" + +``` + +Include multiple parameters with ascending integers starting at `$0`. + +```js +"progressMinutes > $0 AND assignee == $1", 1, "Alex" + +``` + +### Query Formats +The following table shows how a query should be formatted when serialized and +parameterized for the following data types: + +|Type|Parameterized Example|Serialized Example|Note| +| --- | --- | --- | --- | +|Boolean|"setting == $0", false|"setting == false"|`true` or `false` values.| +|String|"name == $0", "George"|"name == 'George'"|Applies to `string` and `char` data type.| +|Number|"age > $0", 5.50|"age > 5.50"|Applies to `int`, `short`, `long`, `double`, `Decimal128`, and `float` data types.| +|Date|"date < $0", dateObject|"date < 2021-02-20@17:30:15:0"|For parameterized date queries, you must pass in a date object. For serialized date queries, you can represented the date in the following formats: As an explicit date and time- YYYY-MM-DD@HH:mm:ss:nn (year-month-day@hours:minutes:seconds:nanoseconds) As a `datetime` relative to the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time)- Ts:n (T, designates the start of the time; `s`, seconds; `n`, nanoseconds) Parameterized `Date` object| +|ObjectID|"_id == $0", oidValue|"_id == oid(507f1f77bcf86cd799439011)"|For parameterized ObjectId queries, you must pass in an ObjectId. For serialized ObjectId queries, the string representation is `oid()`.| +|UUID|"id == $0", uuidValue|"id == uuid(d1b186e1-e9e0-4768-a1a7-c492519d47ee)"|For parameterized UUID queries, you must pass in a UUID. For serialized UUID queries, the string representation is `uuid()`.| +|Binary|"value == $0", "binary"|"value == 'binary'"|For ASCII characters, RQL serializes the binary value like a string, with quotes. For non-printable characters, RQL serializes the binary to a base 64 value.| +|List|"ANY items.name == {$0, $1}", "milk", "bread"|"ANY items.name == {'milk', 'bread'}"|Applies for list, collections, and sets. A parameterized value should be used for each member of the list.| +|RealmObject|"ANY items == $0", obj("Item", oid(6489f036f7bd0546377303ab))|"ANY items == obj('Item', oid(6489f036f7bd0546377303ab))"|To pass in a RealmObject, you need the class and primary key of the object.| + +## Dot Notation +When referring to an object property, you can use **dot notation** to refer +to child properties of that object. You can even refer to the properties of +embedded objects and relationships with dot notation. + +For example, consider a query on an object with a `workplace` property that +refers to a Workplace object. The Workplace object has an embedded object +property, `address`. You can chain dot notations to refer to the zipcode +property of that address: + +```js +"workplace.address.zipcode == 10019" + +``` + +## Nil Type +Realm Query Language include the `nil` type to represent a null pointer. +You can either reference `nil` directly in your queries or with a parameterized query. +If you're using a parameterized query, each SDK maps its respective null pointer +to `nil`. + +```js +"assignee == nil" + +``` + +```js +// comparison to language null pointer +"assignee == $0", null + +``` + +## Comparison Operators +The most straightforward operation in a search is to compare +values. + +> Important: +> The type on both sides of the operator must be equivalent. For +example, comparing an ObjectId with string will result in a precondition +failure with a message like: +> +> ``` +> "Expected object of type object id for property 'id' on object of type +> 'User', but received: 11223344556677889900aabb (Invalid value)" +> ``` +> +> You can compare any numeric type with any other numeric type, +including decimal, float, and Decimal128. +> + +|Operator|Description| +| --- | --- | +|`BETWEEN {number1, number2}`|Evaluates to `true` if the left-hand numerical or date expression is between or equal to the right-hand range. For dates, this evaluates to `true` if the left-hand date is within the right-hand date range.| +|== , =|Evaluates to `true` if the left-hand expression is equal to the right-hand expression.| +|>|Evaluates to `true` if the left-hand numerical or date expression is greater than the right-hand numerical or date expression. For dates, this evaluates to `true` if the left-hand date is later than the right-hand date.| +|>=|Evaluates to `true` if the left-hand numerical or date expression is greater than or equal to the right-hand numerical or date expression. For dates, this evaluates to `true` if the left-hand date is later than or the same as the right-hand date.| +|IN|Evaluates to `true` if the left-hand expression is in the right-hand list. This is equivalent to and used as a shorthand for `== ANY`.| +|<|Evaluates to `true` if the left-hand numerical or date expression is less than the right-hand numerical or date expression. For dates, this evaluates to `true` if the left-hand date is earlier than the right-hand date.| +|<=|Evaluates to `true` if the left-hand numeric expression is less than or equal to the right-hand numeric expression. For dates, this evaluates to `true` if the left-hand date is earlier than or the same as the right-hand date.| +|!= , <>|Evaluates to `true` if the left-hand expression is not equal to the right-hand expression.| + +> Example: +> The following example uses Realm Query Language's comparison operators to: +> +> - Find high priority to-do items by comparing the value of the `priority` +property value with a threshold number, above which priority can be considered high. +> - Find long-running to-do items by seeing if the `progressMinutes` property +is at or above a certain value. +> - Find unassigned to-do items by finding items where the `assignee` property +is equal to `null`. +> - Find to-do items within a certain time range by finding items where the +`progressMinutes` property is between two numbers. +> - Find to-do items with a certain amount of `progressMinutes` from the +given list. +> +> ```javascript +> // Find high priority to-do items by comparing the value of the ``priority`` +> // property value with a threshold number, above which priority can be considered high. +> "priority > $0", 5 +> +> // Find long-running to-do items by seeing if the progressMinutes property is at or above a certain value. +> "progressMinutes > $0", 120 +> +> // Find unassigned to-do items by finding items where the assignee property is equal to null. +> "assignee == $0", null +> +> // Find to-do items within a certain time range by finding items +> // where the progressMinutes property is between two numbers. +> "progressMinutes BETWEEN { $0 , $1 }", 30, 60 +> +> // Find to-do items with a certain amount of progressMinutes from the given list. +> "progressMinutes IN { $0, $1, $2, $3, $4, $5 }", 10, 20, 30, 40, 50, 60 +> +> ``` +> + +## Logical Operators +Make compound predicates using logical operators. + +|Operator|Description| +| --- | --- | +|AND &&|Evaluates to `true` if both left-hand and right-hand expressions are `true`.| +|NOT !|Negates the result of the given expression.| +|OR \\|\\||Evaluates to `true` if either expression returns `true`.| + +> Example: +> We can use the query language's logical operators to find +all of Ali's completed to-do items. That is, we find all items +where the `assignee` property value is equal to 'Ali' AND +the `isComplete` property value is `true`: +> +> ```javascript +> "assignee == $0 AND isComplete == $1", "Ali", true +> +> ``` +> + +## String Operators +Compare string values using these string operators. +Regex-like wildcards allow more flexibility in search. + +> Note: +> You can use the following modifiers with the string operators: +> +> - `[c]` for case insensitivity. `"name CONTAINS[c] $0", 'a'` +> + +|Operator|Description| +| --- | --- | +|BEGINSWITH|Evaluates to `true` if the left-hand string expression begins with the right-hand string expression. This is similar to `contains`, but only matches if the right-hand string expression is found at the beginning of the left-hand string expression.| +|CONTAINS|Evaluates to `true` if the right-hand string expression is found anywhere in the left-hand string expression.| +|ENDSWITH|Evaluates to `true` if the left-hand string expression ends with the right-hand string expression. This is similar to `contains`, but only matches if the left-hand string expression is found at the very end of the right-hand string expression.| +|LIKE|Evaluates to `true` if the left-hand string expression matches the right-hand string wildcard string expression. A wildcard string expression is a string that uses normal characters with two special wildcard characters: The `*` wildcard matches zero or more of any character The `?` wildcard matches any character. For example, the wildcard string "d?g" matches "dog", "dig", and "dug", but not "ding", "dg", or "a dog".| +|== , =|Evaluates to `true` if the left-hand string is lexicographically equal to the right-hand string.| +|!= , <>|Evaluates to `true` if the left-hand string is not lexicographically equal to the right-hand string.| + +> Example: +> We use the query engine's string operators to find: +> +> - Projects with a name starting with the letter 'e' +> - Projects with names that contain 'ie' +> +> ```javascript +> "name BEGINSWITH[c] $0", 'e' +> +> "name CONTAINS $0", 'ie' +> +> ``` +> + +## ObjectId and UUID Operators +Query [BSON ObjectIds](https://www.mongodb.com/docs/manual/reference/method/ObjectId/) and +[UUIDs](https://www.mongodb.com/docs/manual/reference/method/UUID/). +These data types are often used as primary keys. + +To query with ObjectIds, use a parameterized query. Pass the ObjectId or UUID +you're querying against as the argument. + +```js +"_id == $0", oidValue + +``` + +You can also put a string representation of the ObjectId you're evaluating +in `oid()`. + +```js +"_id == oid(6001c033600510df3bbfd864)" + +``` + +To query with UUIDs, put a string representation of the UUID you're evaluating +in `uuid()`. + +```js +"id == uuid(d1b186e1-e9e0-4768-a1a7-c492519d47ee)" + +``` + +|Operator|Description| +| --- | --- | +|== , =|Evaluates to `true` if the left-hand value is equal to the right-hand value.| +|!= , <>|Evaluates to `true` if the left-hand value is not equal to the right-hand value.| + +## Arithmetic Operators +Perform basic arithmetic in one side of a RQL expression when evaluating +numeric data types. + +```js + "2 * priority > 6" + // Is equivalent to + "priority >= 2 * (2 - 1) + 2" + +``` + +You can also use multiple object properties together in a mathematic operation. + +```js +"progressMinutes * priority == 90" + +``` + +|Operator|Description| +| --- | --- | +|*|Multiplication.| +|/|Division.| +|+|Addition.| +|-|Subtraction.| +|()|Group expressions together.| + +## Type Operator +Check the type of a property using the `@type` operator. +You can only use the type operator with mixed types and dictionaries. + +Evaluate the property against a string representation of the data type name. +Refer to SDK documentation on the mapping from the SDK language's data types +to Realm data types. + +|Operator|Description| +| --- | --- | +|`@type`|Check if type of a property is the property name as a string. Use `==` and `!=` to compare equality.| + +```js + "mixedType.@type == 'string'" + + "mixedType.@type == 'bool'" + +``` + +## Dictionary Operators +Compare dictionary values using these dictionary operators. + +|Operator|Description| +| --- | --- | +|`@values`|Returns objects that have the value specified in the right-hand expression.| +|`@keys`|Returns objects that have the key specified in the right-hand expression.| +|`@size`, `@count`|The number of elements in a dictionary.| +|`Dictionary['key']`|Access the value at a key of a dictionary.| +|`ALL \| ANY \| NONE .@type`|Checks if the dictionary contains properties of certain type.| + +You can also use dictionary operators in combination with +comparison operators to filter objects +based on dictionary keys and values. The following examples show some ways +to use dictionary operators with comparison operators. All examples query +a collection of Realm objects with a dictionary property named `dict`. + +> Example: +> The following examples use various dictionary operators. +> +> ```js +> // Evaluates if there is a dictionary key with the name 'foo' +> "ANY dict.@keys == $0", 'foo' +> +> // Evaluates if there is a dictionary key with key 'foo' and value 'bar +> "dict['foo'] == $0", 'bar' +> +> // Evaluates if there is greater than one key-value pair in the dictionary +> "dict.@count > $0", 1 +> +> // Evaluates if dictionary has property of type 'string' +> "ANY dict.@type == 'string'" +> +> // Evaluates if all the dictionary's values are integers +> "ALL dict.@type == 'bool'" +> +> // Evaluates if dictionary does not have any values of type int +> "NONE dict.@type == 'double'" +> +> // ANY is implied. +> "dict.@type == 'string'" +> +> ``` +> + +## Date Operators +Query date types in a realm. + +Generally, you should use a parameterized query to pass a date data type +from the SDK language you are using to a query. + +```js +"timeCompleted < $0", someDate + +``` + +You can also specify dates in the following two ways: + +- As a specific date (in UTC)- `YYYY-MM-DD@HH:mm:ss:nnnnnnnnnn` (year-month-day@hours:minutes:seconds:nanoseconds), UTC. +You can also use `T` instead of `@` to separate the date from the time. +- As a time in seconds since the [Unix epoch](https://en.wikipedia.org/wiki/Unix_time)- `Ts:n`, where `T` designates the start of the time, +`s` is the number of seconds, and `n` is the number of nanoseconds. + +Date supports comparison operators. + +> Example: +> The following example shows how to use a parameterized query with +a date object: +> +> ```js +> var date = new Date("2021-02-20@17:30:15:0"); +> +> "timeCompleted > $0", date +> ``` +> + +## Aggregate Operators +Apply an aggregate operator to a collection property of a Realm +object. Aggregate operators traverse a collection and reduce it to a +single value. + +|Operator|Description| +| --- | --- | +|@avg|Evaluates to the average value of a given numerical property across a collection. If any values are `null`, they are not counted in the result.| +|@count|Evaluates to the number of objects in the given collection.| +|@max|Evaluates to the highest value of a given numerical property across a collection. `null` values are ignored.| +|@min|Evaluates to the lowest value of a given numerical property across a collection. `null` values are ignored.| +|@sum|Evaluates to the sum of a given numerical property across a collection, excluding `null` values.| + +> Example: +> These examples all query for projects containing to-do items that meet +this criteria: +> +> - Projects with average item priority above 5. +> - Projects with an item whose priority is less than 5. +> - Projects with an item whose priority is greater than 5. +> - Projects with more than 5 items. +> - Projects with long-running items. +> +> ```javascript +> var priorityNum = 5; +> +> "items.@avg.priority > $0", priorityNum +> +> "items.@max.priority < $0", priorityNum +> +> "items.@min.priority > $0", priorityNum +> +> "items.@count > $0", 5 +> +> "items.@sum.progressMinutes > $0", 100 +> +> ``` +> + +## Collection Operators +A **collection operator** lets you query list properties within a collection of objects. +Collection operators filter a collection by applying a predicate +to every element of a given list property of the object. +If the predicate returns true, the object is included in the output collection. + +|Operator|Description| +| --- | --- | +|`ALL`|Returns objects where the predicate evaluates to `true` for all objects in the collection.| +|`ANY`, `SOME`|Returns objects where the predicate evaluates to `true` for any objects in the collection.| +|`NONE`|Returns objects where the predicate evaluates to false for all objects in the collection.| + +> Example: +> This example uses collection operators to find projects that contain to-do items +matching certain criteria: +> +> ```js +> // Projects with no complete items. +> "NONE items.isComplete == $0", true +> +> // Projects that contain a item with priority 10 +> "ANY items.priority == $0", 10 +> +> // Projects that only contain completed items +> "ALL items.isComplete == $0", true +> +> // Projects with at least one item assigned to either Alex or Ali +> "ANY items.assignee IN { $0 , $1 }", "Alex", "Ali" +> +> // Projects with no items assigned to either Alex or Ali +> "NONE items.assignee IN { $0 , $1 }", "Alex", "Ali" +> +> ``` +> + +## List Comparisons +You can use comparison operators and +collection operators to filter based +on lists of data. + +You can compare any type of valid list. This includes: + +- collections of Realm objects, which let you filter against other data +in the realm. `"oid(631a072f75120729dc9223d9) IN items.id" +` +- lists defined directly in the query, which let you filter against +static data. You define static lists as a comma-separated list of +literal values enclosed in opening (`{`) and closing (`}`) braces. `"priority IN {0, 1, 2}" +` +- native list objects passed in a parameterized expression, which let you pass application data +directly to your queries. `const ids = [ + new BSON.ObjectId("631a072f75120729dc9223d9"), + new BSON.ObjectId("631a0737c98f89f5b81cd24d"), + new BSON.ObjectId("631a073c833a34ade21db2b2"), +]; +const parameterizedQuery = realm.objects("Item").filtered("id IN $0", ids); +` + +If you do not define a collection operator, a list expression defaults +to the `ANY` operator. + +> Example: +> These two list queries are equivalent: +> +> - `age == ANY {18, 21}` +> - `age == {18, 21}` +> +> Both of these queries return objects with an age property equal to +either 18 or 21. You could also do the opposite by returning objects +only if the age is not equal to either 18 or 21: +> +> - `age == NONE {18, 21}` +> + +The following table includes examples that illustrate how collection +operators interact with lists and comparison operators: + +|Expression|Match?|Reason| +| --- | --- | --- | +|`ANY {1, 2, 3} > ALL {1, 2}`|true|A value on the left (3) is greater than some value on the right (both 1 and 2)| +|`ANY {1, 2, 3} == NONE {1, 2}`|true|3 does not match either of 1 or 2| +|`ANY {4, 8} == ANY {5, 9, 11}`|false|Neither 4 nor 8 matches any value on the right (5, 9 or 11)| +|`ANY {1, 2, 7} <= NONE {1, 2}`|true|A value on the left (7) is not less than or equal to both 1 and 2| +|`ALL {1, 2} IN ANY {1, 2, 3}`|true|Every value on the left (1 and 2) is equal to 1, 2 or 3| +|`ALL {3, 1, 4, 3} == NONE {1, 2}`|false|1 matches a value in the NONE list (1 or 2)| +|`ALL {} in ALL {1, 2}`|true|An empty list matches all lists| +|`NONE {1, 2, 3, 12} > ALL {5, 9, 11}`|false|12 is bigger than all values on the right (5, 9, and 11)| +|`NONE {4, 8} > ALL {5, 9, 11}`|true|4 and 8 are both less than some value on the right (5, 9, or 11)| +|`NONE {0, 1} < NONE {1, 2}`|true|0 and 1 are both less than none of 1 and 2| + +## Full Text Search +You can use RQL to query on properties that have a full-text search (FTS) +annotation. FTS supports boolean match word searches, rather than searches for relevance. +For information on enabling FTS on a property, see the FTS documentation for +your SDK: + +- Flutter SDK +- Kotlin SDK +- .NET SDK +- Node.js SDK +- React Native SDK +- Swift SDK does not yet support Full-Text Search. + +To query these properties, use the `TEXT` predicate in your query. + +You can search for entire words or phrases, or limit your results with the following characters: + +- Exclude results for a word by placing the `-` character in front of the word. +- Specify prefixes by placing the `*` character at the end of a prefix. Suffix +searching is not currently supported. + +In the following example, we query the `Item.name` property: + +```js + // Filter for items with 'write' in the name + "name TEXT $0", "write" + + // Find items with 'write' but not 'tests' using '-' + "name TEXT $0", "write -tests" + + // Find items starting with 'wri-' using '*' + "name TEXT $0", "wri*" + +``` + +### Full-Text Search Tokenizer Details +Full-Text Search (FTS) indexes support: + +- Tokens are diacritics- and case-insensitive. +- Tokens can only consist of characters from ASCII and the Latin-1 supplement (western languages). +All other characters are considered whitespace. +- Words split by a hyphen (-) are split into two tokens. For example, `full-text` +splits into `full` and `text`. + +## Backlink Queries +A backlink is an inverse relationship link that lets you look up objects +that reference another object. Backlinks use the to-one and to-many +relationships defined in your object schemas but reverse the direction. +Every relationship that you define in your schema implicitly has a +corresponding backlink. + +You can access backlinks in queries using the +`@links..` syntax, where `` +and `` refer to a specific property on an object type +that references the queried object type. + +```js +// Find items that belong to a project with a quota greater than 10 (@links) +"@links.Project.items.quota > 10" + +``` + +You can also define a `linkingObjects` property to explicitly include +the backlink in your data model. This lets you reference the backlink +through an assigned property name using standard dot notation. + +```js +// Find items that belong to a project with a quota greater than 10 (LinkingObjects) +"projects.quota > 10" + +``` + +The result of a backlink is treated like a collection and supports +collection operators. + +```js + // Find items where any project that references the item has a quota greater than 0 + "ANY @links.Project.items.quota > 0" + // Find items where all projects that reference the item have a quota greater than 0 + "ALL @links.Project.items.quota > 0" + +``` + +You can use aggregate operators on the backlink collection. + +```js + // Find items that are referenced by multiple projects + "projects.@count > 1" + // Find items that are not referenced by any project + "@links.Project.items.@count == 0" + // Find items that belong to a project where the average item has + // been worked on for at least 5 minutes + "@links.Project.items.items.@avg.progressMinutes > 10" + +``` + +You can query the count of all relationships that point to an object by +using the `@count` operator directly on `@links`. + +```js +// Find items that are not referenced by another object of any type +"@links.@count == 0" + +``` + +## Subqueries +Iterate through list properties with another query using the +`SUBQUERY()` predicate function. + +Subqueries are useful for the following scenarios: + +- Matching each object in a list property on multiple conditions +- Counting the number of objects that match a subquery + +`SUBQUERY()` has the following structure: + +```js +SUBQUERY(, , ) +``` + +- `collection`: The name of the property to iterate through +- `variableName`: A variable name of the element to use in the subquery +- `predicate`: The subquery predicate. +Use the variable specified by `variableName` to refer to the +currently-iterated element. + +A subquery iterates through the given collection and checks the given predicate +against each object in the collection. The predicate can refer to the current +iterated object with the variable name passed to `SUBQUERY()`. + +A subquery expression resolves to a list of objects. +Realm only supports the `@count` aggregate operator on the result +of a subquery. This allows you to count how many objects in the subquery +input collection matched the predicate. + +You can use the count of the subquery result as you would any other number +in a valid expression. In particular, you can compare the count with the +number `0` to return all matching objects. + +> Example: +> The following example shows two subquery filters on a collection of projects. +> +> ```js +> // Returns projects with items that have not been completed +> // by a user named Alex. +> "SUBQUERY(items, $item, $item.isComplete == false AND $item.assignee == 'Alex').@count > 0" +> +> // Returns the projects where the number of completed items is +> // greater than or equal to the value of a project's `quota` property. +> "SUBQUERY(items, $item, $item.isComplete == true).@count >= quota" +> +> ``` +> + +## Sort, Distinct & Limit +Sort and limit the results collection of your query using additional operators. + +|Operator|Description| +| --- | --- | +|`SORT`|Specify the name of the property to compare, and whether to sort by ascending (`ASC`) or descending (`DESC`) order. If you specify multiple SORT fields, you must specify sort order for each field. With multiple sort fields, the query sorts by the first field, and then the second. For example, if you `SORT (priority DESC, name DESC)`, the query returns sorted by priority, and then by name when priority value is the same.| +|`DISTINCT`|Specify a name of the property to compare. Remove duplicates for that property in the results collection. If you specify multiple DISTINCT fields, the query removes duplicates by the first field, and then the second. For example, if you `DISTINCT (name, assignee)`, the query only removes duplicates where the values of both properties are the same.| +|`LIMIT`|Limit the results collection to the specified number.| + +> Example: +> Use the query engine's sort, distinct, and limit operators to find to-do items +where the assignee is Ali: +> +> - Sorted by priority in descending order +> - Enforcing uniqueness by name +> - Limiting the results to 5 items +> +> ```javascript +> "assignee == 'Ali' SORT(priority DESC) DISTINCT(name) LIMIT(5)" +> +> ``` +> diff --git a/docs/guides/test-and-debug/debugging.md b/docs/guides/test-and-debug/debugging.md new file mode 100644 index 0000000000..d96af446ac --- /dev/null +++ b/docs/guides/test-and-debug/debugging.md @@ -0,0 +1,53 @@ +# Debugging - Java SDK +## Android Studio Debugging +> Important: +> The Android Studio debugger can provide misleading values for +Realm object fields. For correct values, you can watch +accessor values instead, or use the Realm object +`toString()` method to see the latest field values. +> + +This section details information you should keep in mind when debugging +Realm applications with Android Studio to avoid incorrect +value reporting. When you watch a Realm object, +you'll see values displayed in the object's fields. These values +are incorrect because the field values themselves are not used. This is +because Realm creates a proxy object behind the scenes, overriding +the getters and setters to access the persisted data in the +realm. To see the correct values, add a watch on the accessors. + +In the image above, the debugger has stopped on line `113`. There are +three watch values: + +- The `person` variable +- The `person.getName()` accessor +- The `person.getAge()` accessor + +The code from lines `107` to `111` alters the `person` instance by +changing the name and age in a write transaction. On line `113`, the `person` watch instance reports +incorrect values for the *field* watch values. The watch values that use +the *accessors* report values that are correct. + +## NDK Debugging +The Realm Java SDK library contains native code. +Debugging NDK crashes can be cumbersome, as the default stack trace +provides minimal information. + +We recommend you use a crash reporting tool such as +[Crashlytics](http://www.crashlytics.com/). This gives you the +ability to track native errors and gather other valuable information. We +can help with your issues faster if you have this information. + +To enable NDK crash reporting in Crashlytics for +your project, add the following to the root of your application +`build.gradle` file: + +```groovy +crashlytics { + enableNdk true +} +``` + +> Note: +> The values `androidNdkOut` and `androidNdkLibsOut` are not needed. +> diff --git a/docs/guides/test-and-debug/log-realm-events.md b/docs/guides/test-and-debug/log-realm-events.md new file mode 100644 index 0000000000..ac005eb06d --- /dev/null +++ b/docs/guides/test-and-debug/log-realm-events.md @@ -0,0 +1,29 @@ +# Log Realm Events - Java SDK +The SDK logs events to the Android system log automatically. You can +view these events using [Logcat](https://developer.android.com//studio/debug/am-logcat). + +## Set the Client Log Level +Realm uses the log levels defined by [Log4J](https://logging.apache.org/log4j/1.2/apidocs/org/apache/log4j/Level.html). +To configure the log level for Realm logs in your application, pass a +`LogLevel` to +`RealmLog.setLevel()`: + +#### Java + +```java +RealmLog.setLevel(LogLevel.ALL); + +``` + +#### Kotlin + +```kotlin +RealmLog.setLevel(LogLevel.ALL) + +``` + +> Tip: +> To diagnose and troubleshoot errors while developing your application, set the +log level to `debug` or `trace`. For production deployments, decrease the +log level for improved performance. +> diff --git a/docs/guides/test-and-debug/testing.md b/docs/guides/test-and-debug/testing.md new file mode 100644 index 0000000000..6343477a70 --- /dev/null +++ b/docs/guides/test-and-debug/testing.md @@ -0,0 +1,1052 @@ +# Testing - Java SDK +You can test your application using unit tests or integration tests. +**Unit tests** only assess the logic written in your application's code. +**Integration tests** assess your application logic, database queries and +writes, and calls to your application's backend, if you have one. Unit tests +run on your development machine using the JVM, while integration tests +run on a physical or emulated Android device. You can run integration +tests by communicating with actual instances of Realm +or an App backend using Android's built-in instrumented tests. + +Android uses specific file paths and folder names in Android projects +for unit tests and instrumented tests: + +|Test Type|Path| +| --- | --- | +|Unit Tests|/app/src/test| +|Instrumented Tests|/app/src/androidTest| + +Because the SDK uses C++ code via Android Native for data +storage, unit testing requires you to entirely mock interactions with +Realm. Prefer integration tests for logic that requires +extensive interaction with the database. + +## Integration Tests +This section shows how to integration test an application that uses +the Realm SDK. It covers the following concepts in the test +environment: + +- acquiring an application context +- executing logic on a `Looper` thread +- how to delay test execution while asynchronous method calls complete + +### Application Context +To initialize the SDK, you'll need to provide an application or activity +[context](https://developer.android.com/reference/android/content/Context). +This isn't available by default in Android integration tests. However, +you can use Android's built-in testing [ActivityScenario](https://developer.android.com/reference/androidx/test/core/app/ActivityScenario) +class to start an activity in your tests. You can use any activity from +your application, or you can create an empty activity just for testing. +Call `ActivityScenario.launch()` with your activity class as a +parameter to start the simulated activity. + +Next, use the `ActivityScenario.onActivity()` method to run a lambda +on the simulated activity's main thread. In this lambda, you should call +the `Realm.init()` function to initialize the SDK with your activity +as a parameter. Additionally, you should save the parameter passed to +your lambda (the newly created instance of your activity) for future +use. + +Because the `onActivity()` method runs on a different thread, you +should block your test from executing further until this initial setup completes. + +The following example uses an `ActivityScenario`, an empty testing +activity, and a `CountDownLatch` to demonstrate how to set up an +environment where you can test your Realm application: + +#### Java + +```java +AtomicReference testActivity = new AtomicReference(); +ActivityScenario scenario = ActivityScenario.launch(BasicActivity.class); + +// create a latch to force blocking for an async call to initialize realm +CountDownLatch setupLatch = new CountDownLatch(1); + +scenario.onActivity(activity -> { + Realm.init(activity); + testActivity.set(activity); + setupLatch.countDown(); // unblock the latch await +}); + +// block until we have an activity to run tests on +try { + Assert.assertTrue(setupLatch.await(1, TimeUnit.SECONDS)); +} catch (InterruptedException e) { + Log.e("EXAMPLE", e.getMessage()); +} + +``` + +#### Kotlin + +```kotlin +var testActivity: Activity? = null +val scenario: ActivityScenario? = + ActivityScenario.launch(BasicActivity::class.java) + +// create a latch to force blocking for an async call to initialize realm +val setupLatch = CountDownLatch(1) + +scenario?.onActivity{ activity: BasicActivity -> + Realm.init(activity) + testActivity = activity + setupLatch.countDown() // unblock the latch await +} + +``` + +### Looper Thread +Realm functionality such as +Live objects and change notifications only +work on [Looper](https://developer.android.com/reference/android/os/Looper) threads. +Threads configured with a `Looper` object pass events over a message +loop coordinated by the `Looper`. Test functions normally don't have +a `Looper` object, and configuring one to work in your tests can be +very error-prone. + +Instead, you can use the [Activity.runOnUiThread()](https://developer.android.com/reference/android/app/Activity#runOnUiThread(java.lang.Runnable)) +method of your test activity to execute logic on a thread that already +has a `Looper` configured. Combine `Activity.runOnUiThread()` with +a `CountDownLatch` as described in the delay section to prevent your test from completing +and exiting before your logic has executed. Within the `runOnUiThread()` +call, you can interact with the SDK just like you normally would in your +application code: + +#### Java + +```java +testActivity.get().runOnUiThread(() -> { + // instantiate an app connection + String appID = YOUR_APP_ID; // replace this with your test application App ID + App app = new App(new AppConfiguration.Builder(appID).build()); + + // authenticate a user + Credentials credentials = Credentials.anonymous(); + app.loginAsync(credentials, it -> { + if (it.isSuccess()) { + Log.v("EXAMPLE", "Successfully authenticated."); + + Realm.getInstanceAsync(config, new Realm.Callback() { + @Override + public void onSuccess(@NonNull Realm realm) { + Log.v("EXAMPLE", "Successfully opened a realm."); + // read and write to realm here via transactions + testLatch.countDown(); + realm.executeTransaction(new Realm.Transaction() { + @Override + public void execute(@NonNull Realm realm) { + realm.createObjectFromJson(Frog.class, + "{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\", _id: 0 }"); + } + }); + realm.close(); + } + @Override + public void onError(@NonNull Throwable exception) { + Log.e("EXAMPLE", "Failed to open the realm: " + exception.getLocalizedMessage()); + } + }); + } else { + Log.e("EXAMPLE", "Failed login: " + it.getError().getErrorMessage()); + } + }); +}); + +``` + +#### Kotlin + +```kotlin +testActivity?.runOnUiThread { + // instantiate an app connection + val appID: String = YOUR_APP_ID // replace this with your App ID + val app = App(AppConfiguration.Builder(appID).build()) + + // authenticate a user + val credentials = Credentials.anonymous() + app.loginAsync(credentials) { + if (it.isSuccess) { + Log.v("EXAMPLE", "Successfully authenticated.") + + Realm.getInstanceAsync(config, object : Realm.Callback() { + override fun onSuccess(realm: Realm) { + Log.v("EXAMPLE", "Successfully opened a realm.") + // read and write to realm here via transactions + realm.executeTransaction { + realm.createObjectFromJson( + Frog::class.java, + "{ name: \"Doctor Cucumber\", age: 1, species: \"bullfrog\", owner: \"Wirt\", _id:0 }" + ) + } + testLatch.countDown() + realm.close() + } + override fun onError(exception: Throwable) { + Log.e("EXAMPLE", + "Failed to open the realm: " + exception.localizedMessage) + } + }) + } else { + Log.e("EXAMPLE", "Failed login: " + it.error.errorMessage) + } + } +} + +``` + +### Delay Test Execution While Async Calls Complete +Because the SDK uses asynchronous calls for common operations, tests need a way +to wait for those async calls to complete. Otherwise, your tests will +exit before your asynchronous (or multi-threaded) calls run. This example +uses Java's built-in [CountDownLatch](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/CountDownLatch.html). Follow these steps to use a `CountDownLatch` in your own tests: + +1. Instantiate a `CountDownLatch` with a count of 1. +2. After running the async logic your test needs to wait for, call that +`CountDownLatch` instance's `countDown()` method. +3. When you need to wait for async logic, add a `try`/`catch` block +that handles an `InterruptedException`. In that block, +call that `CountDownLatch` instance's `await()` method. +4. Pass a timeout interval and unit to `await()`, and wrap +the call in a `Assert.assertTrue()` assertion. If the logic takes +too long, the `await()` call times out, returning false and failing +the test. + +### Testing Backend +Applications that use an App backend should not connect to the +production backend for testing purposes for the following reasons: + +- you should always keep test users and production users separate +for security and privacy reasons +- tests often require a clean initial state, so there's a good chance +your tests will include a setup or teardown method that deletes all +users or large chunks of data + +You can use environments to manage separate +apps for testing and production. + +## Unit Tests +To unit test Realm applications that use Realm, +you must [mock](https://en.wikipedia.org/wiki/Mock_object) Realm (and your +application backend, if you use one). Use the following libraries to +mock SDK functionality: + +- [Robolectric](http://robolectric.org/) +- [PowerMock](https://powermock.github.io/) +- [Mockito](https://site.mockito.org/) + +To make these libraries available for unit testing in your Android project, +add the following to the `dependencies` block of your application +`build.gradle` file: + +``` + testImplementation "org.robolectric:robolectric:4.1" + testImplementation "org.mockito:mockito-core:3.3.3" + testImplementation "org.powermock:powermock-module-junit4:2.0.9" + testImplementation "org.powermock:powermock-module-junit4-rule:2.0.9" + testImplementation "org.powermock:powermock-api-mockito2:2.0.9" + testImplementation "org.powermock:powermock-classloading-xstream:2.0.9" +``` + +> Note: +> Mocking the SDK in unit tests requires Robolectric, Mockito, and +Powermock because the SDK uses Android Native C++ method calls to +interact with Realm. Because the frameworks required to +override these method calls can be delicate, you should use the +versions listed above to ensure that your mocking is successful. Some +recent version updates (particularly Robolectric version 4.2+) can +break compiliation of unit tests using the SDK. +> + +To configure your unit tests to use Robolectric, PowerMock, and Mockito +with the SDK, add the following annotations to each unit test class that +mocks the SDK: + +#### Java + +```java +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "jdk.internal.reflect.*", "androidx.*"}) +@SuppressStaticInitializationFor("io.realm.internal.Util") +@PrepareForTest({Realm.class, RealmConfiguration.class, RealmQuery.class, RealmResults.class, RealmCore.class, RealmLog.class}) + +``` + +#### Kotlin + +```kotlin +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28]) +@PowerMockIgnore( + "org.mockito.*", + "org.robolectric.*", + "android.*", + "jdk.internal.reflect.*", + "androidx.*" +) +@SuppressStaticInitializationFor("io.realm.internal.Util") +@PrepareForTest( + Realm::class, + RealmConfiguration::class, + RealmQuery::class, + RealmResults::class, + RealmCore::class, + RealmLog::class +) + +``` + +Then, bootstrap Powermock globally in the test class: + +#### Java + +```java +// bootstrap powermock +@Rule +public PowerMockRule rule = new PowerMockRule(); + +``` + +#### Kotlin + +```kotlin +// bootstrap powermock +@Rule +var rule = PowerMockRule() + +``` + +Next, mock the components of the SDK that might query native C++ code +so we don't hit the limitations of the test environment: + +#### Java + +```java +// set up realm SDK components to be mocked. The order of these matters +mockStatic(RealmCore.class); +mockStatic(RealmLog.class); +mockStatic(Realm.class); +mockStatic(RealmConfiguration.class); +Realm.init(RuntimeEnvironment.application); +// boilerplate to mock realm components -- this prevents us from hitting any +// native code +doNothing().when(RealmCore.class); +RealmCore.loadLibrary(any(Context.class)); + +``` + +#### Kotlin + +```kotlin +// set up realm SDK components to be mocked. The order of these matters +PowerMockito.mockStatic(RealmCore::class.java) +PowerMockito.mockStatic(RealmLog::class.java) +PowerMockito.mockStatic(Realm::class.java) +PowerMockito.mockStatic(RealmConfiguration::class.java) +Realm.init(RuntimeEnvironment.application) +PowerMockito.doNothing().`when`(RealmCore::class.java) +RealmCore.loadLibrary(ArgumentMatchers.any(Context::class.java)) + +``` + +Once you've completed the setup required for mocking, you can start +mocking components and wiring up behavior for your tests. You can also +configure PowerMockito to return specific objects when new objects of +a type are instantiated, so even code that references the default +realm in your application won't break your tests: + +#### Java + +```java +// create the mocked realm +final Realm mockRealm = mock(Realm.class); +final RealmConfiguration mockRealmConfig = mock(RealmConfiguration.class); +// use this mock realm config for all new realm configurations +whenNew(RealmConfiguration.class).withAnyArguments().thenReturn(mockRealmConfig); +// use this mock realm for all new default realms +when(Realm.getDefaultInstance()).thenReturn(mockRealm); + +``` + +#### Kotlin + +```kotlin +// create the mocked realm +val mockRealm = PowerMockito.mock(Realm::class.java) +val mockRealmConfig = PowerMockito.mock( + RealmConfiguration::class.java +) +// use this mock realm config for all new realm configurations +PowerMockito.whenNew(RealmConfiguration::class.java).withAnyArguments() + .thenReturn(mockRealmConfig) +// use this mock realm for all new default realms +PowerMockito.`when`(Realm.getDefaultInstance()).thenReturn(mockRealm) + +``` + +After mocking a realm, you'll have to configure data for your test +cases. See the full example below for some examples of how you can +provide testing data in unit tests. + +### Full Example +The following example shows a full JUnit `test` +example mocking Realm in unit tests. This example tests +an activity that performs some basic Realm operations. +The tests use mocking to simulate those operations when that activity is +started during a unit test: + +#### Java + +```java +package com.mongodb.realm.examples.java; + +import androidx.appcompat.app.AppCompatActivity; + +import android.os.Bundle; + +import android.os.AsyncTask; +import android.util.Log; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.mongodb.realm.examples.R; +import com.mongodb.realm.examples.model.java.Cat; + +import io.realm.Realm; +import io.realm.RealmResults; + +public class UnitTestActivity extends AppCompatActivity { + + public static final String TAG = UnitTestActivity.class.getName(); + private LinearLayout rootLayout = null; + + private Realm realm; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Realm.init(getApplicationContext()); + setContentView(R.layout.activity_unit_test); + rootLayout = findViewById(R.id.container); + rootLayout.removeAllViews(); + + // open the default Realm for the UI thread. + realm = Realm.getDefaultInstance(); + + // clean up from previous run + cleanUp(); + + // small operation that is ok to run on the main thread + basicCRUD(realm); + + // more complex operations can be executed on another thread. + AsyncTask foo = new AsyncTask() { + @Override + protected String doInBackground(Void... voids) { + String info = ""; + info += complexQuery(); + return info; + } + + @Override + protected void onPostExecute(String result) { + showStatus(result); + } + }; + + foo.execute(); + + findViewById(R.id.clean_up).setOnClickListener(view -> { + view.setEnabled(false); + Log.d("TAG", "clean up"); + cleanUp(); + view.setEnabled(true); + }); + } + + private void cleanUp() { + // delete all cats + realm.executeTransaction(r -> r.delete(Cat.class)); + } + + @Override + public void onDestroy() { + super.onDestroy(); + realm.close(); // remember to close realm when done. + } + + private void showStatus(String txt) { + Log.i(TAG, txt); + TextView tv = new TextView(this); + tv.setText(txt); + rootLayout.addView(tv); + } + + private void basicCRUD(Realm realm) { + showStatus("Perform basic Create/Read/Update/Delete (CRUD) operations..."); + + // all writes must be wrapped in a transaction to facilitate safe multi threading + realm.executeTransaction(r -> { + // add a cat + Cat cat = r.createObject(Cat.class); + cat.setName("John Young"); + }); + + // find the first cat (no query conditions) and read a field + final Cat cat = realm.where(Cat.class).findFirst(); + showStatus(cat.getName()); + + // update cat in a transaction + realm.executeTransaction(r -> { + cat.setName("John Senior"); + }); + + showStatus(cat.getName()); + + // add two more cats + realm.executeTransaction(r -> { + Cat jane = r.createObject(Cat.class); + jane.setName("Jane"); + + Cat doug = r.createObject(Cat.class); + doug.setName("Robert"); + }); + + RealmResults cats = realm.where(Cat.class).findAll(); + showStatus(String.format("Found %s cats", cats.size())); + for (Cat p : cats) { + showStatus("Found " + p.getName()); + } + } + + private String complexQuery() { + String status = "\n\nPerforming complex Query operation..."; + + Realm realm = Realm.getDefaultInstance(); + status += "\nNumber of cats in the DB: " + realm.where(Cat.class).count(); + + // find all cats where name begins with "J". + RealmResults results = realm.where(Cat.class) + .beginsWith("name", "J") + .findAll(); + status += "\nNumber of cats whose name begins with 'J': " + results.size(); + + realm.close(); + return status; + } +} + +``` + +```java +import android.content.Context; + +import com.mongodb.realm.examples.java.UnitTestActivity; +import com.mongodb.realm.examples.model.java.Cat; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PowerMockIgnore; +import org.powermock.core.classloader.annotations.PrepareForTest; +import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor; +import org.powermock.modules.junit4.rule.PowerMockRule; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Config; + +import java.util.Arrays; +import java.util.List; + +import io.realm.Realm; +import io.realm.RealmConfiguration; +import io.realm.RealmObject; +import io.realm.RealmQuery; +import io.realm.RealmResults; +import io.realm.internal.RealmCore; +import io.realm.log.RealmLog; + +import com.mongodb.realm.examples.R; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.powermock.api.mockito.PowerMockito.doNothing; +import static org.powermock.api.mockito.PowerMockito.mock; +import static org.powermock.api.mockito.PowerMockito.mockStatic; +import static org.powermock.api.mockito.PowerMockito.when; +import static org.powermock.api.mockito.PowerMockito.whenNew; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 28) +@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "jdk.internal.reflect.*", "androidx.*"}) +@SuppressStaticInitializationFor("io.realm.internal.Util") +@PrepareForTest({Realm.class, RealmConfiguration.class, RealmQuery.class, RealmResults.class, RealmCore.class, RealmLog.class}) +public class TestTest { + // bootstrap powermock + @Rule + public PowerMockRule rule = new PowerMockRule(); + + // mocked realm SDK components for tests + private Realm mockRealm; + private RealmResults cats; + + @Before + public void setup() throws Exception { + // set up realm SDK components to be mocked. The order of these matters + mockStatic(RealmCore.class); + mockStatic(RealmLog.class); + mockStatic(Realm.class); + mockStatic(RealmConfiguration.class); + Realm.init(RuntimeEnvironment.application); + // boilerplate to mock realm components -- this prevents us from hitting any + // native code + doNothing().when(RealmCore.class); + RealmCore.loadLibrary(any(Context.class)); + + // create the mocked realm + final Realm mockRealm = mock(Realm.class); + final RealmConfiguration mockRealmConfig = mock(RealmConfiguration.class); + // use this mock realm config for all new realm configurations + whenNew(RealmConfiguration.class).withAnyArguments().thenReturn(mockRealmConfig); + // use this mock realm for all new default realms + when(Realm.getDefaultInstance()).thenReturn(mockRealm); + + // any time we ask Realm to create a Cat, return a new instance. + when(mockRealm.createObject(Cat.class)).thenReturn(new Cat()); + + // set up test data + Cat p1 = new Cat(); + p1.setName("Enoch"); + Cat p2 = new Cat(); + p2.setName("Quincy Endicott"); + Cat p3 = new Cat(); + p3.setName("Sara"); + Cat p4 = new Cat(); + p4.setName("Jimmy Brown"); + List catList = Arrays.asList(p1, p2, p3, p4); + + // create a mocked RealmQuery + RealmQuery catQuery = mockRealmQuery(); + // when the RealmQuery performs findFirst, return the first record in the list. + when(catQuery.findFirst()).thenReturn(catList.get(0)); + // when the where clause is called on the Realm, return the mock query. + when(mockRealm.where(Cat.class)).thenReturn(catQuery); + // when the RealmQuery is filtered on any string and any integer, return the query + when(catQuery.equalTo(anyString(), anyInt())).thenReturn(catQuery); + // when a between query is performed with any string as the field and any int as the + // value, then return the catQuery itself + when(catQuery.between(anyString(), anyInt(), anyInt())).thenReturn(catQuery); + // When a beginsWith clause is performed with any string field and any string value + // return the same cat query + when(catQuery.beginsWith(anyString(), anyString())).thenReturn(catQuery); + + // RealmResults is final, must mock static and also place this in the PrepareForTest + // annotation array. + mockStatic(RealmResults.class); + // create a mock RealmResults + RealmResults cats = mockRealmResults(); + // the for(...) loop in Java needs an iterator, so we're giving it one that has items, + // since the mock RealmResults does not provide an implementation. Therefore, any time + // anyone asks for the RealmResults Iterator, give them a functioning iterator from the + // ArrayList of Cats we created above. This will allow the loop to execute. + when(cats.iterator()).thenReturn(catList.iterator()); + // Return the size of the mock list. + when(cats.size()).thenReturn(catList.size()); + + // when we ask Realm for all of the Cat instances, return the mock RealmResults + when(mockRealm.where(Cat.class).findAll()).thenReturn(cats); + // when we ask the RealmQuery for all of the Cat objects, return the mock RealmResults + when(catQuery.findAll()).thenReturn(cats); + + this.mockRealm = mockRealm; + this.cats = cats; + } + + @Test + public void shouldBeAbleToAccessActivityAndVerifyRealmInteractions() { + doCallRealMethod().when(mockRealm) + .executeTransaction(any(Realm.Transaction.class)); + + // create test activity -- onCreate method calls methods that + // query/write to realm + UnitTestActivity activity = Robolectric + .buildActivity(UnitTestActivity.class) + .create() + .start() + .resume() + .visible() + .get(); + + // click the clean up button + activity.findViewById(R.id.clean_up).performClick(); + + // verify that we queried for Cat instances five times in this run + // (2 in basicCrud(), 2 in complexQuery() and 1 in the button click) + verify(mockRealm, times(5)).where(Cat.class); + + // verify that the delete method was called. We also call delete at + // the start of the activity to ensure we start with a clean db. + verify(mockRealm, times(2)).delete(Cat.class); + + // call the destroy method so we can verify that the .close() method + // was called (below) + activity.onDestroy(); + + // verify that the realm got closed 2 separate times. Once in the + // AsyncTask, once in onDestroy + verify(mockRealm, times(2)).close(); + } + + @SuppressWarnings("unchecked") + private RealmQuery mockRealmQuery() { + return mock(RealmQuery.class); + } + + @SuppressWarnings("unchecked") + private RealmResults mockRealmResults() { + return mock(RealmResults.class); + } +} + +``` + +#### Kotlin + +```kotlin +package com.mongodb.realm.examples.kotlin + +import android.os.AsyncTask +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import com.mongodb.realm.examples.R +import com.mongodb.realm.examples.model.java.Cat +import io.realm.Realm + +class UnitTestActivity : AppCompatActivity() { + private var rootLayout: LinearLayout? = null + private var realm: Realm? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + Realm.init(applicationContext) + setContentView(R.layout.activity_unit_test) + rootLayout = findViewById(R.id.container) + rootLayout!!.removeAllViews() + + // open the default Realm for the UI thread. + realm = Realm.getDefaultInstance() + + // clean up from previous run + cleanUp() + + // small operation that is ok to run on the main thread + basicCRUD(realm) + + // more complex operations can be executed on another thread. + val foo: AsyncTask = object : AsyncTask() { + protected override fun doInBackground(vararg params: Void?): String? { + var info = "" + info += complexQuery() + return info + } + + override fun onPostExecute(result: String) { + showStatus(result) + } + } + foo.execute() + findViewById(R.id.clean_up).setOnClickListener { view: View -> + view.isEnabled = false + Log.d("TAG", "clean up") + cleanUp() + view.isEnabled = true + } + } + + private fun cleanUp() { + // delete all cats + realm!!.executeTransaction { r: Realm -> r.delete(Cat::class.java) } + } + + public override fun onDestroy() { + super.onDestroy() + realm!!.close() // remember to close realm when done. + } + + private fun showStatus(txt: String) { + Log.i(TAG, txt) + val tv = TextView(this) + tv.text = txt + rootLayout!!.addView(tv) + } + + private fun basicCRUD(realm: Realm?) { + showStatus("Perform basic Create/Read/Update/Delete (CRUD) operations...") + + // all writes must be wrapped in a transaction to facilitate safe multi threading + realm!!.executeTransaction { r: Realm -> + // add a cat + val cat = r.createObject(Cat::class.java) + cat.name = "John Young" + } + + // find the first cat (no query conditions) and read a field + val cat = realm.where(Cat::class.java).findFirst() + showStatus(cat!!.name) + + // update cat in a transaction + realm.executeTransaction { r: Realm? -> + cat.name = "John Senior" + } + showStatus(cat.name) + + // add two more cats + realm.executeTransaction { r: Realm -> + val jane = r.createObject(Cat::class.java) + jane.name = "Jane" + val doug = r.createObject(Cat::class.java) + doug.name = "Robert" + } + val cats = realm.where(Cat::class.java).findAll() + showStatus(String.format("Found %s cats", cats.size)) + for (p in cats) { + showStatus("Found " + p.name) + } + } + + private fun complexQuery(): String { + var status = "\n\nPerforming complex Query operation..." + val realm = Realm.getDefaultInstance() + status += """ + + Number of cats in the DB: ${realm.where(Cat::class.java).count()} + """.trimIndent() + + // find all cats where name begins with "J". + val results = realm.where(Cat::class.java) + .beginsWith("name", "J") + .findAll() + status += """ + + Number of cats whose name begins with 'J': ${results.size} + """.trimIndent() + realm.close() + return status + } + + companion object { + val TAG = UnitTestActivity::class.java.name + } +} + +``` + +```kotlin +import android.content.Context +import android.view.View +import com.mongodb.realm.examples.R +import com.mongodb.realm.examples.kotlin.UnitTestActivity +import com.mongodb.realm.examples.model.java.Cat +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmObject +import io.realm.RealmQuery +import io.realm.RealmResults +import io.realm.internal.RealmCore +import io.realm.log.RealmLog +import java.lang.Exception +import java.util.* +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mockito +import org.powermock.api.mockito.PowerMockito +import org.powermock.core.classloader.annotations.PowerMockIgnore +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.core.classloader.annotations.SuppressStaticInitializationFor +import org.powermock.modules.junit4.rule.PowerMockRule +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.RuntimeEnvironment +import org.robolectric.annotation.Config + +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [28]) +@PowerMockIgnore( + "org.mockito.*", + "org.robolectric.*", + "android.*", + "jdk.internal.reflect.*", + "androidx.*" +) +@SuppressStaticInitializationFor("io.realm.internal.Util") +@PrepareForTest( + Realm::class, + RealmConfiguration::class, + RealmQuery::class, + RealmResults::class, + RealmCore::class, + RealmLog::class +) +class TestTest { + // bootstrap powermock + @Rule + var rule = PowerMockRule() + + // mocked realm SDK components for tests + private var mockRealm: Realm? = null + private var cats: RealmResults? = null + @Before + @Throws(Exception::class) + fun setup() { + // set up realm SDK components to be mocked. The order of these matters + PowerMockito.mockStatic(RealmCore::class.java) + PowerMockito.mockStatic(RealmLog::class.java) + PowerMockito.mockStatic(Realm::class.java) + PowerMockito.mockStatic(RealmConfiguration::class.java) + Realm.init(RuntimeEnvironment.application) + PowerMockito.doNothing().`when`(RealmCore::class.java) + RealmCore.loadLibrary(ArgumentMatchers.any(Context::class.java)) + + // create the mocked realm + val mockRealm = PowerMockito.mock(Realm::class.java) + val mockRealmConfig = PowerMockito.mock( + RealmConfiguration::class.java + ) + // use this mock realm config for all new realm configurations + PowerMockito.whenNew(RealmConfiguration::class.java).withAnyArguments() + .thenReturn(mockRealmConfig) + // use this mock realm for all new default realms + PowerMockito.`when`(Realm.getDefaultInstance()).thenReturn(mockRealm) + + // any time we ask Realm to create a Cat, return a new instance. + PowerMockito.`when`(mockRealm.createObject(Cat::class.java)).thenReturn(Cat()) + + // set up test data + val p1 = Cat() + p1.name = "Enoch" + val p2 = Cat() + p2.name = "Quincy Endicott" + val p3 = Cat() + p3.name = "Sara" + val p4 = Cat() + p4.name = "Jimmy Brown" + val catList = Arrays.asList(p1, p2, p3, p4) + + // create a mocked RealmQuery + val catQuery = mockRealmQuery() + // when the RealmQuery performs findFirst, return the first record in the list. + PowerMockito.`when`(catQuery!!.findFirst()).thenReturn(catList[0]) + // when the where clause is called on the Realm, return the mock query. + PowerMockito.`when`(mockRealm.where(Cat::class.java)).thenReturn(catQuery) + // when the RealmQuery is filtered on any string and any integer, return the query + PowerMockito.`when`( + catQuery.equalTo( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyInt() + ) + ).thenReturn(catQuery) + // when a between query is performed with any string as the field and any int as the + // value, then return the catQuery itself + PowerMockito.`when`( + catQuery.between( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt() + ) + ).thenReturn(catQuery) + // When a beginsWith clause is performed with any string field and any string value + // return the same cat query + PowerMockito.`when`( + catQuery.beginsWith( + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString() + ) + ).thenReturn(catQuery) + + // RealmResults is final, must mock static and also place this in the PrepareForTest + // annotation array. + PowerMockito.mockStatic(RealmResults::class.java) + // create a mock RealmResults + val cats = mockRealmResults() + // the for(...) loop in Java needs an iterator, so we're giving it one that has items, + // since the mock RealmResults does not provide an implementation. Therefore, any time + // anyone asks for the RealmResults Iterator, give them a functioning iterator from the + // ArrayList of Cats we created above. This will allow the loop to execute. + PowerMockito.`when`>(cats!!.iterator()).thenReturn(catList.iterator()) + // Return the size of the mock list. + PowerMockito.`when`(cats.size).thenReturn(catList.size) + + // when we ask Realm for all of the Cat instances, return the mock RealmResults + PowerMockito.`when`(mockRealm.where(Cat::class.java).findAll()).thenReturn(cats) + // when we ask the RealmQuery for all of the Cat objects, return the mock RealmResults + PowerMockito.`when`(catQuery.findAll()).thenReturn(cats) + this.mockRealm = mockRealm + this.cats = cats + } + + @Test + fun shouldBeAbleToAccessActivityAndVerifyRealmInteractions() { + Mockito.doCallRealMethod().`when`(mockRealm)!! + .executeTransaction(ArgumentMatchers.any(Realm.Transaction::class.java)) + + // create test activity -- onCreate method calls methods that + // query/write to realm + val activity = Robolectric + .buildActivity(UnitTestActivity::class.java) + .create() + .start() + .resume() + .visible() + .get() + + // click the clean up button + activity.findViewById(R.id.clean_up).performClick() + + // verify that we queried for Cat instances five times in this run + // (2 in basicCrud(), 2 in complexQuery() and 1 in the button click) + Mockito.verify(mockRealm, Mockito.times(5))!!.where(Cat::class.java) + + // verify that the delete method was called. We also call delete at + // the start of the activity to ensure we start with a clean db. + Mockito.verify(mockRealm, Mockito.times(2))!!.delete(Cat::class.java) + + // call the destroy method so we can verify that the .close() method + // was called (below) + activity.onDestroy() + + // verify that the realm got closed 2 separate times. Once in the + // AsyncTask, once in onDestroy + Mockito.verify(mockRealm, Mockito.times(2))!!.close() + } + + private fun mockRealmQuery(): RealmQuery? { + @Suppress("UNCHECKED_CAST") + return PowerMockito.mock(RealmQuery::class.java) as RealmQuery + } + + private fun mockRealmResults(): RealmResults? { + @Suppress("UNCHECKED_CAST") + return PowerMockito.mock(RealmResults::class.java) as RealmResults + } +} + +``` + +> Seealso: +> See the [Unit Testing Example App](https://github.com/realm/realm-java/tree/master/examples/unitTestExample) +for an example of unit testing an application that uses +Realm. +> diff --git a/docs/guides/test-and-debug/troubleshooting.md b/docs/guides/test-and-debug/troubleshooting.md new file mode 100644 index 0000000000..6c51c84f9a --- /dev/null +++ b/docs/guides/test-and-debug/troubleshooting.md @@ -0,0 +1,199 @@ +# Troubleshooting - Java SDK +## Couldn't load "librealm-jni.so" +If your app uses native libraries that don't ship with support for +64-bit architectures, Android will fail to load Realm's +`librealm-jni.so` file on ARM64 devices. This happens because Android +cannot load 32-bit and 64-bit native libraries concurrently. Ideally, +all libraries could provide the same set of supported ABIs, but +sometimes that may not be doable when using a 3rd-party library. + +To work around this issue, you can exclude Realm's ARM64 library from +the APK file by adding the following code to the application's +`build.gradle`. You can refer to Mixing 32- and 64-bit Dependencies in Android for more information. + +```gradle +android { + //... + packagingOptions { + exclude "lib/arm64-v8a/librealm-jni.so" + } + //... +} +``` + +> Seealso: +> For more information, see [Mixing 32- and 64-bit Dependencies in Android](https://corbt.com/posts/2015/09/18/mixing-32-and-64bit-dependencies-in-android.html). +> + +## Network Calls to Mixpanel +Realm collects anonymous analytics when you run the +Realm bytecode transformer on your source code. This is +completely anonymous and helps us improve the product by flagging: + +- which version of the SDK you use +- which operating system you use +- if your application uses Kotlin + +Analytics do not run when your application runs on user devices - only +when you compile your source code. To opt out of analytics, you can set +the `REALM_DISABLE_ANALYTICS` environment variable to `true`. + +## Change Listeners in Android 12 with SDK Versions Below 10.5.1 +Due to a change in the Linux kernel, +object, collection, and realm notifications do not work in SDK versions below +10.5.1 on devices running certain early versions of +Android 12. + +This change effects Linux kernel versions beginning with `5.5`. +Linux kernel version `5.14-rc4` fixed the issue. The fix was +also backported to `LTS 5.10.56` and `LTS 5.13.8`. All mainline +and LTS Android 12 branches contain the fix or a backport of it. + +If you experience this issue, you can restore notification functionality +with the following fixes: + +- upgrade to a version of the SDK later than 10.5.1. +- upgrade to a version of Android 12 that uses a Linux kernel release +that contains the fix (kernel commit `3a34b13a88caeb2800ab44a4918f230041b37dd9`) +or the backport of the fix (kernel commit `4b20d2de0b367bca627b49efd8d2e9e01bb66753`). + +## Configurations Cannot be Different if Used to Open the Same File +Realm runs checks whenever you open a realm file to +avoid corruption. In order to avoid accidentally opening a realm +file with incompatible settings, the SDK uses Java's `equals()` method +to compare `RealmConfiguration` objects. This prevents the SDK from +opening a single realm file with different schemas, durability levels, +or writability settings. However, configurations that include lambda +functions, such as those passed to +`initialData()` +and +`compactOnLaunch()`, +can break this `equals()` comparison, since two different lambdas are +never considered equal using Java's built-in comparison. +To avoid this error when using lambdas, you can either: + +1. Store a single configuration statically in your application, so that +separate realm instances use the exact same +`RealmConfiguration` object and it passes the check. +2. Override the default equals check of the `RealmConfiguration`: `val config = RealmConfiguration.Builder() + .initialData(object: Realm.Transaction { + override fun execute(realm: Realm) { + TODO("Not yet implemented") + } + + override fun equals(other: Any?): Boolean { + return true + } + + override fun hashCode(): Int { + return 37 + } + }).build()` + +## Kapt Exceptions During Builds +If you experience an exception in the Kapt library with a description +like the following: + +``` +A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptWithoutKotlincTask$KaptExecutionWorkAction +``` + +This most likely means there is an issue with one of your model classes. +Possible causes include: + +- introducing a field type that is not supported by the SDK +- using a visibility type other than `open` or `public` for a realm object model class +- using a Realm annotation on an incompatible field + +If you experience this error, check any recent updates to your schema for +problems. + +## Installation Size +Once your app is built for release and split for distribution, the SDK +should only add about 800KB to your APK in most cases. The releases are +significantly larger because they include support for more architectures, +such as ARM7, ARMv7, ARM64, x86, and MIPS. The APK file contains all +supported architectures, but the Android installer only installs native +code for the device's architecture. This means that the installed app +is smaller than the size of the APK file. + +You can reduce the size of the Android APK itself by splitting the APK +into a version for each architecture. Use the Android Build Tool ABI +Split support by adding the following to your build.gradle: + +```gradle +android { + splits { + abi { + enable true + reset() + include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' + } + } +} +``` + +Select the architectures that you'd like to include to build a separate +APK for each. + +> Seealso: +> See the [Android Tools documentation about ABI Splits](https://developer.android.com/studio/build/configure-apk-splits.html) +for more information, or the [example on GitHub](https://github.com/realm/realm-java/tree/master/examples/gridViewExample). +> + +If you don't want to handle multiple APKs, you can restrict the number +of architectures supported in a single APK. This is done by adding +`abiFilters` to your build.gradle: + +```gradle +android { + defaultConfig { + ndk { + abiFilters 'armeabi-v7a', 'arm64-v8a', 'mips', 'x86', 'x86_64' + } + } +} +``` + +> Seealso: +> [Controlling APK Size When Using Native Libraries](https://medium.com/android-news/controlling-apk-size-when-using-native-libraries-45c6c0e5b70a). +> + +## Customize Dependencies Defined by the Realm Gradle Plugin +Realm uses a Gradle plugin because it makes it easier to set +up a large number of dependencies. Unfortunately this also makes it a +bit harder to ignore specific transitive dependencies. + +If you want to customize Realm beyond what is exposed by the +plugin, you can manually set up all the dependencies and ignore the +Gradle plugin. The following example demonstrates how to set up the SDK +for an Android application using Kotlin manually: + +```gradle +buildscript { + ext.kotlin_version = '1.5.21' + ext.realm_version = '10.18.0' + repositories { + jcenter() + mavenCentral() + } + dependencies { + classpath "io.realm:realm-transformer:$realm_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' + +import io.realm.transformer.RealmTransformer +android.registerTransform(new RealmTransformer(project)) + +dependencies { + api "io.realm:realm-annotations:$realm_version" + api "io.realm:realm-android-library:$realm_version" + api "io.realm:realm-android-kotlin-extensions:$realm_version" + kapt "io.realm:realm-annotations-processor:$realm_version" +} +``` diff --git a/docs/guides/troubleshooting.md b/docs/guides/troubleshooting.md new file mode 100644 index 0000000000..602b53840d --- /dev/null +++ b/docs/guides/troubleshooting.md @@ -0,0 +1,30 @@ +# Troubleshooting - Java SDK +## Use in System Apps on Custom Android ROMs +Realm SDKs use named pipes to support notifications and access to +the Realm file from multiple processes. While this is allowed by +default for normal user apps, it is disallowed for system apps. + +System apps are defined by setting `android:sharedUserId="android.uid.system"` +in the Android manifest. For system apps, you may see a security violation in +Logcat that looks something like this: + +```bash +05-24 14:08:08.984 6921 6921 W .realmsystemapp: type=1400 audit(0.0:99): avc: denied { write } for name="realm.testapp.com.realmsystemapp-Bfqpnjj4mUvxWtfMcOXBCA==" dev="vdc" ino=14660 scontext=u:r:system_app:s0 tcontext=u:object_r:apk_data_file:s0 tclass=dir permissive=0 +05-24 14:08:08.984 6921 6921 W .realmsystemapp: type=1400 audit(0.0:100): avc: denied { write } for name="realm.testapp.com.realmsystemapp-Bfqpnjj4mUvxWtfMcOXBCA==" dev="vdc" ino=14660 scontext=u:r:system_app:s0 tcontext=u:object_r:apk_data_file:s0 tclass=dir permissive=0 +``` + +To fix this, you need to adjust the SELinux security rules in the ROM. This can +be done by using the tool `audit2allow`. This tool ships as part of +[AOSP](https://source.android.com/). + +1. Pull the current policy from the device: `adb pull /sys/fs/selinux/policy`. +2. Copy the SELinux error inside a text file called `input.txt`. +3. Run the `audit2allow` tool: `audit2allow -p policy -i input.txt`. +4. The tool should output a rule you can add to your existing policy. +The rule allows you to access the Realm file from multiple processes. + +`audit2allow` is produced when compiling AOSP/ROM and only runs on +Linux. Check out the details in the [Android Source documentation](https://source.android.com/security/selinux/validate#using_audit2allow). +Also note that since Android Oreo, Google changed the way it configures +SELinux and the default security policies are now more modularized. More details +are in the [Android Source documentation](https://source.android.com/security/selinux/images/SELinux_Treble.pdf). diff --git a/examples/architectureComponentsExample/build.gradle b/examples/architectureComponentsExample/build.gradle new file mode 100644 index 0000000000..20b28d2df4 --- /dev/null +++ b/examples/architectureComponentsExample/build.gradle @@ -0,0 +1,40 @@ +apply plugin: 'com.android.application' +apply plugin: 'realm-android' + +android { + compileSdkVersion rootProject.sdkVersion + buildToolsVersion rootProject.buildTools + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { + applicationId 'io.realm.examples.arch' + targetSdkVersion rootProject.sdkVersion + minSdkVersion rootProject.minSdkVersion + versionCode 1 + versionName "1.0" + + vectorDrawables.useSupportLibrary = true + } + + buildTypes { + release { + minifyEnabled true + signingConfig signingConfigs.debug + } + debug { + minifyEnabled false + } + } +} + +dependencies { + implementation "android.arch.lifecycle:runtime:1.1.1" + implementation "android.arch.lifecycle:extensions:1.1.1" + annotationProcessor "android.arch.lifecycle:compiler:1.1.1" + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:recyclerview-v7:27.1.1' + implementation 'com.android.support:design:27.1.1' +} diff --git a/examples/newsreaderExample/lint.xml b/examples/architectureComponentsExample/lint.xml similarity index 62% rename from examples/newsreaderExample/lint.xml rename to examples/architectureComponentsExample/lint.xml index 1d3dbb0011..da1621f226 100644 --- a/examples/newsreaderExample/lint.xml +++ b/examples/architectureComponentsExample/lint.xml @@ -1,7 +1,9 @@ + + - + diff --git a/examples/architectureComponentsExample/src/main/AndroidManifest.xml b/examples/architectureComponentsExample/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6303608067 --- /dev/null +++ b/examples/architectureComponentsExample/src/main/AndroidManifest.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/ArchExampleActivity.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/ArchExampleActivity.java new file mode 100644 index 0000000000..740ecb7f69 --- /dev/null +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/ArchExampleActivity.java @@ -0,0 +1,95 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.arch; + +import android.os.Bundle; +import android.support.annotation.MainThread; +import android.support.design.widget.FloatingActionButton; +import android.support.v7.app.AppCompatActivity; + +public class ArchExampleActivity extends AppCompatActivity { + private FloatingActionButton backgroundJobStartStop; + + private BackgroundTask backgroundTask; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_arch_example); + setupViews(); + + backgroundTask = (BackgroundTask) getLastCustomNonConfigurationInstance(); + if (backgroundTask == null) { // this could also live inside a ViewModel, a singleton job queue, etc. + backgroundTask = new BackgroundTask(); + backgroundTask.start(); // this task will update items in Realm on a background thread. + } + updateJobButton(); + + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .add(R.id.container, PersonListFragment.create()) + .addToBackStack(null) + .commit(); + } + } + + @Override + public Object onRetainCustomNonConfigurationInstance() { + return backgroundTask; // retain background task through config changes without ViewModel. + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (isFinishing()) { + if(backgroundTask.isStarted()) { + backgroundTask.stop(); // make sure job is stopped when exiting the app + } + } + } + + @Override + public void onBackPressed() { + if (getSupportFragmentManager().getBackStackEntryCount() <= 1) { + finish(); + } else { + super.onBackPressed(); + } + } + + @MainThread + private void setupViews() { + backgroundJobStartStop = findViewById(R.id.backgroundJobStartStop); + backgroundJobStartStop.setOnClickListener(v -> { + if (!backgroundTask.isStarted()) { + backgroundTask.start(); + } else { + backgroundTask.stop(); + } + updateJobButton(); + }); + } + + private void updateJobButton() { + if (backgroundTask.isStarted()) { + backgroundJobStartStop.setImageResource(R.drawable.ic_stop_black_24dp); + } else { + backgroundJobStartStop.setImageResource(R.drawable.ic_play_arrow_black_24dp); + } + } +} diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/BackgroundTask.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/BackgroundTask.java new file mode 100644 index 0000000000..b99389a69b --- /dev/null +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/BackgroundTask.java @@ -0,0 +1,92 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.arch; + +import android.annotation.SuppressLint; +import android.os.SystemClock; +import android.support.annotation.MainThread; +import android.util.Log; + +import io.realm.Realm; +import io.realm.RealmResults; +import io.realm.examples.arch.model.Person; + + +public class BackgroundTask { + private static final Object lock = new Object(); + + private static final String TAG = "BackgroundTask"; + + private boolean isStarted; + + private volatile Thread thread; + + @MainThread + public boolean isStarted() { + return isStarted; + } + + @MainThread + public void start() { + synchronized (lock) { + if (isStarted) { + return; + } + thread = new IncrementThread(); + thread.start(); + isStarted = true; + Log.i(TAG, "Background job started."); + } + } + + @MainThread + public void stop() { + synchronized (lock) { + if (thread != null) { + thread.interrupt(); + thread = null; + } + isStarted = false; + } + } + + private static final class IncrementThread extends Thread { + IncrementThread() { + super("Aging thread"); + } + + @Override + @SuppressLint("NewApi") + public void run() { + try (Realm realm = Realm.getDefaultInstance()) { + final RealmResults persons = realm.where(Person.class).findAll(); + Realm.Transaction transaction = (Realm r) -> { + for (Person person : persons) { + person.setAge(person.getAge() + 1); // updates the Persons in the Realm. + } + }; + + while (!isInterrupted()) { + realm.executeTransaction(transaction); + SystemClock.sleep(1000L); + } + } + Log.i(TAG, "Background job stopped."); + } + } +} + diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/CustomApplication.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/CustomApplication.java new file mode 100644 index 0000000000..b3c8333f70 --- /dev/null +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/CustomApplication.java @@ -0,0 +1,58 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.arch; + +import android.app.Application; +import android.support.annotation.NonNull; + +import io.realm.Realm; +import io.realm.RealmConfiguration; +import io.realm.examples.arch.model.Person; + + +public class CustomApplication extends Application { + + @Override + public void onCreate() { + super.onCreate(); + + Realm.init(this); + Realm.setDefaultConfiguration(new RealmConfiguration.Builder() + .deleteRealmIfMigrationNeeded() + .initialData(new Realm.Transaction() { + @Override + public void execute(@NonNull Realm realm) { + Person person = realm.createObject(Person.class); + person.setName("Makoto Yamazaki"); + person.setAge(32); + + person = realm.createObject(Person.class); + person.setName("Christian Melchior"); + person.setAge(34); + + person = realm.createObject(Person.class); + person.setName("Chen Mulong"); + person.setAge(29); + + person = realm.createObject(Person.class); + person.setName("Nabil Hachicha"); + person.setAge(31); + } + }) + .build()); + } +} diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonFragment.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonFragment.java new file mode 100644 index 0000000000..194553cae3 --- /dev/null +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonFragment.java @@ -0,0 +1,85 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.examples.arch; + +import android.arch.lifecycle.ViewModel; +import android.arch.lifecycle.ViewModelProvider; +import android.arch.lifecycle.ViewModelProviders; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +public class PersonFragment extends Fragment { + private static final String ARG_PERSON_NAME = "personName"; + + public static PersonFragment create(String personName) { + PersonFragment personFragment = new PersonFragment(); + Bundle bundle = new Bundle(); + bundle.putString(ARG_PERSON_NAME, personName); + personFragment.setArguments(bundle); + return personFragment; + } + + private PersonViewModel personViewModel; + + private TextView name; + private TextView age; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + @SuppressWarnings("ConstantConditions") final String personName = getArguments().getString(ARG_PERSON_NAME); + personViewModel = ViewModelProviders.of(this, new ViewModelProvider.Factory() { + @NonNull + @Override + public T create(@NonNull Class modelClass) { + if (modelClass == PersonViewModel.class) { + PersonViewModel personViewModel = new PersonViewModel(); + personViewModel.setup(personName); // we use a Factory to ensure `setup` is called before use. + //noinspection unchecked + return (T) personViewModel; + } + //noinspection ConstantConditions + return null; + } + }).get(PersonViewModel.class); + + personViewModel.getPerson().observe(this, person -> { + if (person != null) { // null would mean the object was deleted. + name.setText(person.getName()); + age.setText(String.valueOf(person.getAge())); + } + }); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_person, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + name = view.findViewById(R.id.personName); + age = view.findViewById(R.id.personAge); + } +} diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonListFragment.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonListFragment.java new file mode 100644 index 0000000000..a114dfd84a --- /dev/null +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonListFragment.java @@ -0,0 +1,140 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.examples.arch; + +import android.arch.lifecycle.ViewModelProviders; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.AppCompatActivity; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import java.util.Collections; +import java.util.List; + +import io.realm.examples.arch.model.Person; +import io.realm.examples.arch.utils.ContextUtils; + +public class PersonListFragment extends Fragment { + public static PersonListFragment create() { + return new PersonListFragment(); + } + + private RecyclerView recyclerView; + private Adapter adapter; + + private PersonListViewModel personListViewModel; + private List personList = Collections.emptyList(); + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Fragments should start listening in `onCreate()` + // to ensure single observer instance, even if detached (for example in FragmentPagerAdapter). + personListViewModel = ViewModelProviders.of(this).get(PersonListViewModel.class); + personListViewModel.getPersons().observe(this, people -> { + personList = people; + if (adapter != null) { + adapter.updateItems(people); + } + }); + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.fragment_person_list, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + recyclerView = view.findViewById(R.id.recyclerView); + recyclerView.setHasFixedSize(true); + recyclerView.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.VERTICAL, false)); + adapter = new Adapter(personList); + recyclerView.setAdapter(adapter); + } + + static class Adapter extends RecyclerView.Adapter { + private List persons; + + public Adapter(List persons) { + this.persons = persons; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_person, parent, false)); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + holder.bind(persons.get(position)); + } + + @Override + public int getItemCount() { + return persons == null ? 0 : persons.size(); + } + + public void updateItems(List persons) { + this.persons = persons; + notifyDataSetChanged(); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + TextView name; + TextView age; + + Person person; + + private final View.OnClickListener onClick = (view) -> { + if (person == null) { + return; + } + AppCompatActivity activity = ContextUtils.findActivity(view.getContext()); + PersonFragment personFragment = PersonFragment.create(person.name); + activity.getSupportFragmentManager() + .beginTransaction() + .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE) + .replace(R.id.container, personFragment) + .addToBackStack(null) + .commit(); + }; + + public ViewHolder(View itemView) { + super(itemView); + name = itemView.findViewById(R.id.personName); + age = itemView.findViewById(R.id.personAge); + itemView.setOnClickListener(onClick); + } + + public void bind(Person person) { + this.person = person; + name.setText(person.getName()); + age.setText(String.valueOf(person.getAge())); + } + } + } +} diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonListViewModel.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonListViewModel.java new file mode 100644 index 0000000000..e45edc4bd5 --- /dev/null +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonListViewModel.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.examples.arch; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; + +import java.util.List; + +import io.realm.Realm; +import io.realm.examples.arch.livemodel.LiveRealmResults; +import io.realm.examples.arch.model.Person; + +public class PersonListViewModel extends ViewModel { + private final Realm realm; + private final LiveData> persons; + + public PersonListViewModel() { + realm = Realm.getDefaultInstance(); // Realm is bound to the lifecycle of the ViewModel, and stays alive as long as it is needed. + persons = new LiveRealmResults<>(realm.where(Person.class).sort("age").findAllAsync()); + } + + public LiveData> getPersons() { + return persons; + } + + @Override + protected void onCleared() { + realm.close(); // Realm is bound to the lifecycle of the ViewModel, and is destroyed when no longer needed. + super.onCleared(); + } +} diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonViewModel.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonViewModel.java new file mode 100644 index 0000000000..666a938742 --- /dev/null +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/PersonViewModel.java @@ -0,0 +1,53 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.arch; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; + +import io.realm.Realm; +import io.realm.examples.arch.livemodel.LiveRealmObject; +import io.realm.examples.arch.model.Person; + + +public class PersonViewModel extends ViewModel { + private final Realm realm; + + private LiveData livePerson; + + public PersonViewModel() { + realm = Realm.getDefaultInstance(); // Realm is bound to the lifecycle of the ViewModel, and stays alive as long as it is needed. + } + + public LiveData getPerson() { + return livePerson; + } + + @Override + protected void onCleared() { + realm.close(); // Realm is bound to the lifecycle of the ViewModel, and is destroyed when no longer needed. + super.onCleared(); + } + + public void setup(String personName) { + Person person = realm.where(Person.class).equalTo("name", personName).findFirst(); + if (person == null) { + throw new IllegalStateException("The person was not found, it shouldn't be deleted!"); + } + livePerson = new LiveRealmObject<>(person); + } +} diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/livemodel/LiveRealmObject.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/livemodel/LiveRealmObject.java new file mode 100644 index 0000000000..97690d596f --- /dev/null +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/livemodel/LiveRealmObject.java @@ -0,0 +1,105 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.arch.livemodel; + +import android.arch.lifecycle.LiveData; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; + +import io.realm.ObjectChangeSet; +import io.realm.RealmModel; +import io.realm.RealmObject; +import io.realm.RealmObjectChangeListener; + +/** + * This class represents a RealmObject wrapped inside a LiveData. + * + * It is expected that the provided RealmObject is a managed object, and exists in the Realm on creation. + * + * This allows observing the RealmObject in such a way, that the listener that will be automatically unsubscribed when the enclosing LifecycleOwner is killed. + * + * Realm will keep the managed RealmObject up-to-date whenever a change occurs on any thread, + * and when that happens, the observer will be notified. + * + * The object will be observed until it is invalidated - deleted, or all local Realm instances are closed. + * + * @param the type of the RealmModel + */ +public class LiveRealmObject extends LiveData { + // The listener will listen until the object is deleted. + // An invalidated object shouldn't be set in LiveData, null is set instead. + private RealmObjectChangeListener listener = new RealmObjectChangeListener() { + @Override + public void onChange(@NonNull T object, ObjectChangeSet objectChangeSet) { + if (!objectChangeSet.isDeleted()) { + setValue(object); + } else { + setValue(null); + } + } + }; + + /** + * Wraps the provided managed RealmObject as a LiveData. + * + * The provided object should not be null, should be managed, and should be valid. + * + * @param object the managed RealmModel to wrap as LiveData + */ + @MainThread + public LiveRealmObject(@NonNull T object) { + //noinspection ConstantConditions + if (object == null) { + throw new IllegalArgumentException("The object cannot be null!"); + } + if (!RealmObject.isManaged(object)) { + throw new IllegalArgumentException("LiveRealmObject only supports managed RealmModel instances!"); + } + if (!RealmObject.isValid(object)) { + throw new IllegalArgumentException("The provided RealmObject is no longer valid, and therefore cannot be observed for changes."); + } + setValue(object); + } + + // We should start observing and stop observing, depending on whether we have observers. + // Deleted objects can no longer be observed. + // We can also no longer observe the object if all local Realm instances on this thread (the UI thread) are closed. + + /** + * Starts observing the RealmObject, if it is still valid. + */ + @Override + protected void onActive() { + super.onActive(); + T object = getValue(); + if (object != null && RealmObject.isValid(object)) { + RealmObject.addChangeListener(object, listener); + } + } + + /** + * Stops observing the RealmObject. + */ + @Override + protected void onInactive() { + super.onInactive(); + T object = getValue(); + if (object != null && RealmObject.isValid(object)) { + RealmObject.removeChangeListener(object, listener); + } + } +} diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/livemodel/LiveRealmResults.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/livemodel/LiveRealmResults.java new file mode 100644 index 0000000000..8b54563cec --- /dev/null +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/livemodel/LiveRealmResults.java @@ -0,0 +1,94 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.arch.livemodel; + +import android.arch.lifecycle.LiveData; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; + +import java.util.List; + +import javax.annotation.Nullable; + +import io.realm.OrderedCollectionChangeSet; +import io.realm.OrderedRealmCollectionChangeListener; +import io.realm.RealmModel; +import io.realm.RealmResults; + +/** + * This class represents a RealmResults wrapped inside a LiveData. + * + * Realm will always keep the RealmResults up-to-date whenever a change occurs on any thread, + * and when that happens, the observer will be notified. + * + * The RealmResults will be observed until it is invalidated - meaning all local Realm instances on this thread are closed. + * + * @param the type of the RealmModel + */ +public class LiveRealmResults extends LiveData> { + private final RealmResults results; + + // The listener will notify the observers whenever a change occurs. + // The results are modified in change. This could be expanded to also return the change set in a pair. + private OrderedRealmCollectionChangeListener> listener = new OrderedRealmCollectionChangeListener>() { + @Override + public void onChange(@NonNull RealmResults results, @Nullable OrderedCollectionChangeSet changeSet) { + LiveRealmResults.this.setValue(results); + } + }; + + @MainThread + public LiveRealmResults(@NonNull RealmResults results) { + //noinspection ConstantConditions + if (results == null) { + throw new IllegalArgumentException("Results cannot be null!"); + } + if (!results.isValid()) { + throw new IllegalArgumentException("The provided RealmResults is no longer valid, the Realm instance it belongs to is closed. It can no longer be observed for changes."); + } + this.results = results; + if (results.isLoaded()) { + // we should not notify observers when results aren't ready yet (async query). + // however, synchronous query should be set explicitly. + setValue(results); + } + } + + // We should start observing and stop observing, depending on whether we have observers. + + /** + * Starts observing the RealmResults, if it is still valid. + */ + @Override + protected void onActive() { + super.onActive(); + if (results.isValid()) { // invalidated results can no longer be observed. + results.addChangeListener(listener); + } + } + + /** + * Stops observing the RealmResults. + */ + @Override + protected void onInactive() { + super.onInactive(); + if (results.isValid()) { + results.removeChangeListener(listener); + } + } +} diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/model/Person.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/model/Person.java new file mode 100644 index 0000000000..de97eb924b --- /dev/null +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/model/Person.java @@ -0,0 +1,43 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.arch.model; + +import io.realm.RealmObject; +import io.realm.annotations.Index; + +public class Person extends RealmObject { + @Index + public String name; + + private int age; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } +} diff --git a/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/utils/ContextUtils.java b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/utils/ContextUtils.java new file mode 100644 index 0000000000..1a4cf1cc26 --- /dev/null +++ b/examples/architectureComponentsExample/src/main/java/io/realm/examples/arch/utils/ContextUtils.java @@ -0,0 +1,52 @@ +/* + * Copyright 2018 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.examples.arch.utils; + +import android.app.Activity; +import android.content.Context; +import android.content.ContextWrapper; + +/** + * This is a helper class to look up an Activity inside a View's context chain in a reliable/safe manner. + */ +public class ContextUtils { + private ContextUtils() { + } + + /** + * Finds the Activity inside the hierarchy of the provided Context. + * + * @param context the context + * @param the expected type of the Activity + * @return the activity + * + * @throws IllegalArgumentException if the context has no Activity in its base context hierarchy + */ + public static T findActivity(Context context) { + if (context instanceof Activity) { + //noinspection unchecked + return (T) context; + } + while (context != null && context instanceof ContextWrapper) { + context = ((ContextWrapper) context).getBaseContext(); + if (context instanceof Activity) { + //noinspection unchecked + return (T) context; + } + } + throw new IllegalArgumentException("No activity found in context hierarchy."); + } +} diff --git a/examples/architectureComponentsExample/src/main/res/drawable/ic_play_arrow_black_24dp.xml b/examples/architectureComponentsExample/src/main/res/drawable/ic_play_arrow_black_24dp.xml new file mode 100644 index 0000000000..bf9b895aca --- /dev/null +++ b/examples/architectureComponentsExample/src/main/res/drawable/ic_play_arrow_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/examples/architectureComponentsExample/src/main/res/drawable/ic_stop_black_24dp.xml b/examples/architectureComponentsExample/src/main/res/drawable/ic_stop_black_24dp.xml new file mode 100644 index 0000000000..c428d728dd --- /dev/null +++ b/examples/architectureComponentsExample/src/main/res/drawable/ic_stop_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/examples/architectureComponentsExample/src/main/res/layout/activity_arch_example.xml b/examples/architectureComponentsExample/src/main/res/layout/activity_arch_example.xml new file mode 100644 index 0000000000..45bbf88ac8 --- /dev/null +++ b/examples/architectureComponentsExample/src/main/res/layout/activity_arch_example.xml @@ -0,0 +1,25 @@ + + + + + + diff --git a/examples/architectureComponentsExample/src/main/res/layout/fragment_person.xml b/examples/architectureComponentsExample/src/main/res/layout/fragment_person.xml new file mode 100644 index 0000000000..48d7965ee9 --- /dev/null +++ b/examples/architectureComponentsExample/src/main/res/layout/fragment_person.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/examples/architectureComponentsExample/src/main/res/layout/fragment_person_list.xml b/examples/architectureComponentsExample/src/main/res/layout/fragment_person_list.xml new file mode 100644 index 0000000000..a4eb4e4289 --- /dev/null +++ b/examples/architectureComponentsExample/src/main/res/layout/fragment_person_list.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/examples/architectureComponentsExample/src/main/res/layout/item_person.xml b/examples/architectureComponentsExample/src/main/res/layout/item_person.xml new file mode 100644 index 0000000000..4b1de1a9b2 --- /dev/null +++ b/examples/architectureComponentsExample/src/main/res/layout/item_person.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/newsreaderExample/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/architectureComponentsExample/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from examples/newsreaderExample/src/main/res/mipmap-hdpi/ic_launcher.png rename to examples/architectureComponentsExample/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/examples/newsreaderExample/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/architectureComponentsExample/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from examples/newsreaderExample/src/main/res/mipmap-mdpi/ic_launcher.png rename to examples/architectureComponentsExample/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/examples/newsreaderExample/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/architectureComponentsExample/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from examples/newsreaderExample/src/main/res/mipmap-xhdpi/ic_launcher.png rename to examples/architectureComponentsExample/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/examples/newsreaderExample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/architectureComponentsExample/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from examples/newsreaderExample/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to examples/architectureComponentsExample/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/examples/newsreaderExample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/architectureComponentsExample/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from examples/newsreaderExample/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to examples/architectureComponentsExample/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/examples/gridViewExample/src/main/res/values-w820dp/dimens.xml b/examples/architectureComponentsExample/src/main/res/values-w820dp/dimens.xml similarity index 100% rename from examples/gridViewExample/src/main/res/values-w820dp/dimens.xml rename to examples/architectureComponentsExample/src/main/res/values-w820dp/dimens.xml diff --git a/examples/gridViewExample/src/main/res/values/dimens.xml b/examples/architectureComponentsExample/src/main/res/values/dimens.xml similarity index 100% rename from examples/gridViewExample/src/main/res/values/dimens.xml rename to examples/architectureComponentsExample/src/main/res/values/dimens.xml diff --git a/examples/architectureComponentsExample/src/main/res/values/strings.xml b/examples/architectureComponentsExample/src/main/res/values/strings.xml new file mode 100644 index 0000000000..fcfdd7f1bb --- /dev/null +++ b/examples/architectureComponentsExample/src/main/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + Lifecycle example + + diff --git a/examples/architectureComponentsExample/src/main/res/values/styles.xml b/examples/architectureComponentsExample/src/main/res/values/styles.xml new file mode 100644 index 0000000000..54b991006c --- /dev/null +++ b/examples/architectureComponentsExample/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/examples/build.gradle b/examples/build.gradle index 64b863d9f8..31c6d956f1 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -1,27 +1,48 @@ -project.ext.sdkVersion = 24 -project.ext.buildTools = '24.0.0' +def projectDependencies = new Properties() +projectDependencies.load(new FileInputStream("${rootDir}/../dependencies.list")) +project.ext.sdkVersion = 29 +project.ext.minSdkVersion = 16 +project.ext.buildTools = projectDependencies.get("ANDROID_BUILD_TOOLS") +project.ext.kotlinVersion = projectDependencies.get('KOTLIN') // Don't cache SNAPSHOT (changing) dependencies. configurations.all { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } +static String getAppId (path) { + String build = new File(path).text + def matcher = build =~ 'applicationId.*' + def appId = matcher.size() > 0 ? matcher[0].trim() - 'applicationId' - ~/\s/ : '' + String myappId = appId.replaceAll('"', '') + myappId = myappId.replaceAll('\'', '') + return myappId +} + allprojects { def currentVersion = file("${rootDir}/../version.txt").text.trim() + def props = new Properties() + props.load(new FileInputStream("${rootDir}/../dependencies.list")) + props.each { key, val -> + project.ext.set(key, val) + } + buildscript { + ext { + kotlin_version = projectDependencies.get('KOTLIN') + } repositories { mavenLocal() - jcenter() + mavenCentral() maven { url 'https://jitpack.io' } + google() + jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.2.0' - classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.6' - classpath 'com.github.JakeWharton:sdk-manager-plugin:0ce4cdf08009d79223850a59959d9d6e774d0f77' - classpath 'com.novoda:gradle-android-command-plugin:1.5.0' - classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' + classpath "com.android.tools.build:gradle:${props.get("GRADLE_BUILD_TOOLS")}" classpath "io.realm:realm-gradle-plugin:${currentVersion}" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -30,6 +51,32 @@ allprojects { repositories { mavenLocal() + mavenCentral() + google() jcenter() } + + if (!project.name.startsWith("realm-examples") + && !project.name.startsWith("library") + && !project.name.startsWith("moduleExample")) { // exclude root and library project + ["Debug", "Release"].each { + task "monkey${it}"(dependsOn: "install${it}") { + doLast { + def numberOfEvents = 2000 + def appId = getAppId("${project.projectDir}/build.gradle") + def process = "adb shell monkey -p ${appId} --pct-syskeys 0 ${numberOfEvents}".execute([], project.rootDir) + + def sout = new StringBuilder(), serr = new StringBuilder() + process.consumeProcessOutput(sout, serr) + process.waitFor() + + if (process.exitValue() != 0 + || !sout?.toString()?.trim()?.contains("Events injected: ${numberOfEvents}")) { + // fail Gradle build + throw new GradleException("monkey failed for AppID: ${appId} \nExit code: ${process.exitValue()}\nStd out: ${sout}\nStd err: ${serr}") + } + } + } + } + } } diff --git a/examples/compatibilityExample/README.md b/examples/compatibilityExample/README.md new file mode 100644 index 0000000000..23925e91d4 --- /dev/null +++ b/examples/compatibilityExample/README.md @@ -0,0 +1,5 @@ +# Using this example + +This example is not really meant for use, but only as a compatibility check to ensure that we can +build projects without AndroidX and Java 8 features. + diff --git a/examples/compatibilityExample/build.gradle b/examples/compatibilityExample/build.gradle new file mode 100644 index 0000000000..b74adf0233 --- /dev/null +++ b/examples/compatibilityExample/build.gradle @@ -0,0 +1,60 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'realm-android' + +android { + compileSdkVersion rootProject.sdkVersion + buildToolsVersion rootProject.buildTools + + defaultConfig { + applicationId 'io.realm.examples.compatibility' + targetSdkVersion rootProject.sdkVersion + minSdkVersion rootProject.minSdkVersion + versionCode 1 + versionName "1.0" + } + + buildTypes { + // Configure server and App Id. + // The default server is https://realm-dev.mongodb.com/ . Go to that and copy the MongoDB + // Realm App Id. + // + // If you are running a local version of MongoDB Realm, modify endpoint accordingly. Most + // likely it is "http://localhost:9090" + def mongodbRealmUrl = "https://realm-dev.mongodb.com" + def appId = "my-app-id" + debug { + buildConfigField "String", "MONGODB_REALM_URL", "\"${mongodbRealmUrl}\"" + buildConfigField "String", "MONGODB_REALM_APP_ID", "\"${appId}\"" + } + release { + buildConfigField "String", "MONGODB_REALM_URL", "\"${mongodbRealmUrl}\"" + buildConfigField "String", "MONGODB_REALM_APP_ID", "\"${appId}\"" + minifyEnabled true + signingConfig signingConfigs.debug + } + } + + // Ensure that we can compile an app without Java 8 features + compileOptions { + sourceCompatibility 1.8 + targetCompatibility 1.8 + } +} + +realm { + syncEnabled = true +} + +dependencies { + implementation 'com.android.support:appcompat-v7:27.1.1' + implementation 'com.android.support:design:27.1.1' + implementation 'me.zhanghai.android.materialprogressbar:library:1.3.0' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'com.android.support.constraint:constraint-layout:1.1.3' +} + +if ((project.findProperty("android.useAndroidX") ?: false).toBoolean()) + throw new RuntimeException("Compatibility project should run without AndroidX") diff --git a/examples/compatibilityExample/gradle.properties b/examples/compatibilityExample/gradle.properties new file mode 100644 index 0000000000..3b465e0263 --- /dev/null +++ b/examples/compatibilityExample/gradle.properties @@ -0,0 +1,2 @@ +# Ensure that we do not use AndroidX for this project +android.useAndroidX=false diff --git a/examples/compatibilityExample/lint.xml b/examples/compatibilityExample/lint.xml new file mode 100644 index 0000000000..da1621f226 --- /dev/null +++ b/examples/compatibilityExample/lint.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/examples/compatibilityExample/src/main/AndroidManifest.xml b/examples/compatibilityExample/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..6e5500a498 --- /dev/null +++ b/examples/compatibilityExample/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/examples/compatibilityExample/src/main/java/io/realm/examples/compatibility/MyActivity.kt b/examples/compatibilityExample/src/main/java/io/realm/examples/compatibility/MyActivity.kt new file mode 100644 index 0000000000..8eee9d5c2e --- /dev/null +++ b/examples/compatibilityExample/src/main/java/io/realm/examples/compatibility/MyActivity.kt @@ -0,0 +1,11 @@ +package io.realm.examples.compatibility + +import android.support.v7.app.AppCompatActivity +import android.os.Bundle + +class MyActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_my_activty) + } +} diff --git a/examples/compatibilityExample/src/main/java/io/realm/examples/compatibility/MyApplication.kt b/examples/compatibilityExample/src/main/java/io/realm/examples/compatibility/MyApplication.kt new file mode 100644 index 0000000000..8e8f4276ed --- /dev/null +++ b/examples/compatibilityExample/src/main/java/io/realm/examples/compatibility/MyApplication.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2019 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.compatibility + +import android.app.Application + +import io.realm.Realm +import io.realm.log.LogLevel +import io.realm.log.RealmLog +import io.realm.mongodb.App +import io.realm.mongodb.AppConfiguration + +lateinit var APP: App + +class MyApplication : Application() { + + override fun onCreate() { + super.onCreate() + Realm.init(this) + APP = App(AppConfiguration.Builder(BuildConfig.MONGODB_REALM_APP_ID) + .baseUrl(BuildConfig.MONGODB_REALM_URL) + .appName(BuildConfig.VERSION_NAME) + .appVersion(BuildConfig.VERSION_CODE.toString()) + .build()) + + // Enable more logging in debug mode + if (BuildConfig.DEBUG) { + RealmLog.setLevel(LogLevel.DEBUG) + } + } +} diff --git a/examples/compatibilityExample/src/main/res/drawable-xxhdpi/ic_exit_to_app_white_24dp.png b/examples/compatibilityExample/src/main/res/drawable-xxhdpi/ic_exit_to_app_white_24dp.png new file mode 100644 index 0000000000..c04fe6e0e3 Binary files /dev/null and b/examples/compatibilityExample/src/main/res/drawable-xxhdpi/ic_exit_to_app_white_24dp.png differ diff --git a/examples/compatibilityExample/src/main/res/drawable-xxxhdpi/ic_exit_to_app_white_24dp.png b/examples/compatibilityExample/src/main/res/drawable-xxxhdpi/ic_exit_to_app_white_24dp.png new file mode 100644 index 0000000000..27a9d7b05a Binary files /dev/null and b/examples/compatibilityExample/src/main/res/drawable-xxxhdpi/ic_exit_to_app_white_24dp.png differ diff --git a/examples/compatibilityExample/src/main/res/drawable/logo.png b/examples/compatibilityExample/src/main/res/drawable/logo.png new file mode 100755 index 0000000000..91826a7567 Binary files /dev/null and b/examples/compatibilityExample/src/main/res/drawable/logo.png differ diff --git a/examples/compatibilityExample/src/main/res/layout/activity_my_activty.xml b/examples/compatibilityExample/src/main/res/layout/activity_my_activty.xml new file mode 100644 index 0000000000..ed72b63f86 --- /dev/null +++ b/examples/compatibilityExample/src/main/res/layout/activity_my_activty.xml @@ -0,0 +1,9 @@ + + + + diff --git a/examples/compatibilityExample/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/compatibilityExample/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000000..58303aff5b Binary files /dev/null and b/examples/compatibilityExample/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/compatibilityExample/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/compatibilityExample/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 0000000000..9b29caed3d Binary files /dev/null and b/examples/compatibilityExample/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/examples/compatibilityExample/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/compatibilityExample/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 0000000000..15527b160e Binary files /dev/null and b/examples/compatibilityExample/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/compatibilityExample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/compatibilityExample/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 0000000000..eb9ece04b2 Binary files /dev/null and b/examples/compatibilityExample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/newsreaderExample/src/main/res/values/dimens.xml b/examples/compatibilityExample/src/main/res/values/dimens.xml similarity index 100% rename from examples/newsreaderExample/src/main/res/values/dimens.xml rename to examples/compatibilityExample/src/main/res/values/dimens.xml diff --git a/examples/compatibilityExample/src/main/res/values/realm_colors.xml b/examples/compatibilityExample/src/main/res/values/realm_colors.xml new file mode 100644 index 0000000000..3d435a5c44 --- /dev/null +++ b/examples/compatibilityExample/src/main/res/values/realm_colors.xml @@ -0,0 +1,28 @@ + + + + #1C233F + #9A9BA5 + #b1b3bf + #EBEBF2 + + + #39477F + #59569E + #9A59A5 + #D34CA3 + #F25192 + #F77C88 + #FC9F95 + #FCC397 + + + #d64881 + #dadada + + + #EF5350 + #9CCC65 + #FFA726 + + \ No newline at end of file diff --git a/examples/compatibilityExample/src/main/res/values/strings.xml b/examples/compatibilityExample/src/main/res/values/strings.xml new file mode 100644 index 0000000000..f54b795456 --- /dev/null +++ b/examples/compatibilityExample/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Realm Compatibility Example + Realm Logo + diff --git a/examples/compatibilityExample/src/main/res/values/styles.xml b/examples/compatibilityExample/src/main/res/values/styles.xml new file mode 100644 index 0000000000..333a4944af --- /dev/null +++ b/examples/compatibilityExample/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/examples/coroutinesExample/build.gradle b/examples/coroutinesExample/build.gradle new file mode 100644 index 0000000000..4e909277c9 --- /dev/null +++ b/examples/coroutinesExample/build.gradle @@ -0,0 +1,86 @@ +apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply plugin: 'realm-android' + +android { + // androidx.lifecycle dependencies requires Android APIs 31 or later + compileSdkVersion 31 + buildToolsVersion rootProject.buildTools + + defaultConfig { + applicationId "io.realm.examples.coroutinesexample" + targetSdkVersion rootProject.sdkVersion + minSdkVersion rootProject.minSdkVersion + versionCode 1 + versionName "1.0" + multiDexEnabled true + + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + dataBinding true + } + + packagingOptions { + exclude 'META-INF/LICENSE' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8.toString() + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + + implementation "androidx.activity:activity-ktx:1.1.0" + + implementation "androidx.appcompat:appcompat:1.2.0" + + implementation "androidx.constraintlayout:constraintlayout:2.0.4" + implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0" + + implementation "androidx.fragment:fragment-ktx:1.2.5" + + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.0" + implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.4.0" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.4.0" + implementation "androidx.lifecycle:lifecycle-common-java8:2.4.0" + + implementation "androidx.legacy:legacy-support-v4:1.0.0" + + implementation "androidx.multidex:multidex:2.0.1" + + implementation "androidx.recyclerview:recyclerview:1.1.0" + + implementation "com.dropbox.mobile.store:store4:4.0.5" + + implementation "com.google.android.material:material:1.2.1" + + implementation "com.squareup.retrofit2:adapter-rxjava2:2.3.0" + implementation "com.squareup.retrofit2:retrofit:2.8.1" + implementation "com.squareup.retrofit2:converter-moshi:2.8.1" + implementation "com.squareup.okhttp3:logging-interceptor:4.9.0" + + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx2:1.4.0" + + testImplementation 'junit:junit:4.13' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' +} diff --git a/examples/coroutinesExample/proguard-rules.pro b/examples/coroutinesExample/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/examples/coroutinesExample/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/examples/newsreaderExample/src/main/AndroidManifest.xml b/examples/coroutinesExample/src/main/AndroidManifest.xml similarity index 55% rename from examples/newsreaderExample/src/main/AndroidManifest.xml rename to examples/coroutinesExample/src/main/AndroidManifest.xml index c45071ebec..c60aa9b2af 100644 --- a/examples/newsreaderExample/src/main/AndroidManifest.xml +++ b/examples/coroutinesExample/src/main/AndroidManifest.xml @@ -1,26 +1,25 @@ - + - + android:theme="@style/AppTheme"> + - + - + - diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/MainActivity.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/MainActivity.kt new file mode 100644 index 0000000000..7c0fceb194 --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/MainActivity.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.fragment.app.commit +import io.realm.examples.coroutinesexample.ui.details.DetailsFragment +import io.realm.examples.coroutinesexample.ui.main.MainFragment +import kotlin.time.ExperimentalTime + +@ExperimentalTime +class MainActivity : AppCompatActivity(), MainFragment.OnItemClicked { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_main) + + if (savedInstanceState == null) { + showMainFragment() + } + } + + override fun onAttachFragment(fragment: Fragment) { + when (fragment) { + is MainFragment -> fragment.onItemclickedCallback = this + } + } + + override fun onBackPressed() { + val detailsFragment = supportFragmentManager.findFragmentByTag(DetailsFragment.TAG) + if (detailsFragment != null) { + supportFragmentManager.popBackStackImmediate() + } else { + super.onBackPressed() + } + } + + override fun onItemClicked(id: String) { + val mainFragment = supportFragmentManager.findFragmentByTag(MainFragment.TAG) + val detailsFragment = DetailsFragment.instantiate(DetailsFragment.ArgsBundle(id)) + + supportFragmentManager.commit { + setCustomAnimations(R.anim.fragment_open_enter, R.anim.fragment_open_exit) + add(R.id.container, detailsFragment, DetailsFragment.TAG) + hide(requireNotNull(mainFragment)) + addToBackStack(null) + } + } + + private fun showMainFragment() { + supportFragmentManager.commit { + replace(R.id.container, MainFragment.newInstance(), MainFragment.TAG) + } + } +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/MainApplication.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/MainApplication.kt new file mode 100644 index 0000000000..b572f53a93 --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/MainApplication.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample + +import androidx.multidex.MultiDexApplication +import io.realm.Realm +import io.realm.examples.coroutinesexample.data.newsreader.local.repository.NewsReaderRepository +import io.realm.examples.coroutinesexample.di.DependencyGraph + +const val TAG = "--- CoroutinesExample" + +class MainApplication : MultiDexApplication() { + + override fun onCreate() { + super.onCreate() + Realm.init(this) + + repository = DependencyGraph.provideNewsReaderRepository() + } + + companion object { + lateinit var repository: NewsReaderRepository + } +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/local/RealmNYTDao.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/local/RealmNYTDao.kt new file mode 100644 index 0000000000..b73ecea339 --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/local/RealmNYTDao.kt @@ -0,0 +1,213 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.data.newsreader.local + +import io.realm.Realm +import io.realm.RealmConfiguration +import io.realm.RealmList +import io.realm.RealmResults +import io.realm.examples.coroutinesexample.data.newsreader.network.model.NYTMultimedium +import io.realm.examples.coroutinesexample.data.newsreader.network.model.NYTimesArticle +import io.realm.kotlin.executeTransactionAwait +import io.realm.kotlin.toFlow +import io.realm.kotlin.where +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext +import java.io.Closeable +import java.util.concurrent.Executors + +/** + * Data Access Object interface used to gain access to Realm. + * + * It implements [Closeable] to allow proper Realm instance housekeeping linked to handling the + * Android activity/fragment lifecycle. + */ +interface RealmNYTDao : Closeable { + suspend fun insertArticles(articles: List) + suspend fun updateArticle(id: String) + suspend fun deleteArticles(section: String) + suspend fun deleteAllArticles() + fun getArticlesBlocking(section: String): RealmResults + fun getArticles(section: String): Flow> + fun getArticleBlocking(id: String): RealmNYTimesArticle? + fun getArticle(id: String): Flow + fun countArticles(section: String): Long +} + +class RealmNYTDaoImpl( + private val realmConfiguration: RealmConfiguration +) : RealmNYTDao { + + /** + * Dispatcher used to run suspendable functions that run Realm transactions. This is needed to + * confine Realm instances within the same thread as long as the coroutine is running to avoid + * accessing said instances from different threads and thus (potentially) triggering a thread + * violation. + */ + private val monoThreadDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher() + + /** + * [Realm] instance used to fire queries. It must not be used for other than firing queries and + * has to be closed when no longer in use. + */ + private val realm = Realm.getInstance(realmConfiguration) + + override suspend fun insertArticles(articles: List) { + withContext(monoThreadDispatcher) { + runCloseableTransaction(realmConfiguration) { transactionRealm -> + transactionRealm.insertOrUpdate(articles) + } + } + } + + override suspend fun updateArticle(id: String) { + withContext(monoThreadDispatcher) { + runCloseableTransaction(realmConfiguration) { transactionRealm -> + val article = transactionRealm.where() + .equalTo(RealmNYTimesArticle.COLUMN_URL, id) + .findFirst() + checkNotNull(article).read = true + } + } + } + + override suspend fun deleteArticles(section: String) { + withContext(monoThreadDispatcher) { + runCloseableTransaction(realmConfiguration) { transactionRealm -> + transactionRealm.deleteAll() + } + } + } + + override suspend fun deleteAllArticles() { + withContext(monoThreadDispatcher) { + runCloseableTransaction(realmConfiguration) { transactionRealm -> + transactionRealm.deleteAll() + } + } + } + + override fun getArticlesBlocking(section: String): RealmResults { + return realm.where() + .equalTo(RealmNYTimesArticle.COLUMN_API_SECTION, section) + .findAll() + } + + override fun getArticles(section: String): Flow> { + return realm.where() + .equalTo(RealmNYTimesArticle.COLUMN_API_SECTION, section) + .findAllAsync() + .toFlow() + } + + override fun getArticleBlocking(id: String): RealmNYTimesArticle? { + return realm.where() + .equalTo(RealmNYTimesArticle.COLUMN_URL, id) + .findFirst() + } + + override fun getArticle(id: String): Flow { + return realm.where() + .equalTo(RealmNYTimesArticle.COLUMN_URL, id) + .findFirstAsync() + .toFlow() + } + + override fun countArticles(section: String): Long { + return realm.where() + .equalTo(RealmNYTimesArticle.COLUMN_API_SECTION, section) + .count() + } + + override fun close() { + realm.close() + } +} + +/** + * Inserts a [List] of [NYTimesArticle]s after they have been mapped to [RealmNYTimesArticle] + * instances. + */ +suspend fun RealmNYTDao.insertArticles(apiSection: String, articles: List) { + val realmArticles = articles.toRealmArticles(apiSection) + insertArticles(realmArticles) +} + +private fun List.toRealmArticles(apiQuerySection: String): List { + val timestamp = System.currentTimeMillis() + return map { article -> + RealmNYTimesArticle().apply { + updateTime = timestamp + apiSection = apiQuerySection + section = article.section + subsection = article.subsection + title = article.title + abstractText = article.abstractText + url = article.url + uri = article.uri + byline = article.byline + itemType = article.itemType + updatedDate = article.updatedDate + createDate = article.createDate + publishedDate = article.publishedDate + materialTypeFacet = article.materialTypeFacet + kicker = article.kicker + desFacet = RealmList().apply { addAll(article.desFacet ?: listOf()) } + orgFacet = RealmList().apply { addAll(article.orgFacet ?: listOf()) } + perFacet = RealmList().apply { addAll(article.perFacet ?: listOf()) } + geoFacet = RealmList().apply { addAll(article.geoFacet ?: listOf()) } + orgFacet = RealmList().apply { addAll(article.orgFacet ?: listOf()) } + perFacet = RealmList().apply { addAll(article.perFacet ?: listOf()) } + geoFacet = RealmList().apply { addAll(article.geoFacet ?: listOf()) } + multimedia = article.multimedia.toRealmMultimediumRealmList() + shortUrl = article.shortUrl + } + } +} + +private fun List?.toRealmMultimediumRealmList(): RealmList { + return RealmList().also { realmList -> + realmList.addAll( + this?.map { multimedium -> + multimedium.toRealmMultimedium() + } ?: listOf() + ) + } +} + +private fun NYTMultimedium.toRealmMultimedium(): RealmNYTMultimedium { + return RealmNYTMultimedium().also { + it.url = url + it.format = format + it.height = height + it.width = width + it.type = type + it.subtype = subtype + it.caption = caption + it.copyright = copyright + } +} + +private suspend fun runCloseableTransaction( + realmConfiguration: RealmConfiguration, + transaction: (realm: Realm) -> Unit +) { + Realm.getInstance(realmConfiguration).use { realmInstance -> + realmInstance.executeTransactionAwait(transaction = transaction) + } +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/local/RealmNYTimes.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/local/RealmNYTimes.kt new file mode 100644 index 0000000000..4f7c442af5 --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/local/RealmNYTimes.kt @@ -0,0 +1,74 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.data.newsreader.local + +import io.realm.RealmList +import io.realm.RealmObject +import io.realm.annotations.LinkingObjects +import io.realm.annotations.PrimaryKey +import io.realm.annotations.RealmClass +import java.time.LocalDateTime +import java.util.* + +open class RealmNYTimesArticle : RealmObject() { + var read: Boolean = false + var updateTime: Long = 0 + var apiSection: String = "" + var section: String = "" + var subsection: String? = null + var title: String = "" + var abstractText: String? = null + + @PrimaryKey + var url: String = UUID.randomUUID().toString() + + var uri: String? = null + var byline: String? = null + var itemType: String? = null + var updatedDate: String? = null + var createDate: String? = null + var publishedDate: String? = null + var materialTypeFacet: String? = null + var kicker: String? = null + var desFacet: RealmList = RealmList() + var orgFacet: RealmList = RealmList() + var perFacet: RealmList = RealmList() + var geoFacet: RealmList = RealmList() + var multimedia: RealmList = RealmList() + var shortUrl: String? = null + + companion object { + const val EMBEDDED_MULTIMEDIA = "multimedia" + const val COLUMN_URL = "url" + const val COLUMN_API_SECTION = "apiSection" + } +} + +@RealmClass(embedded = true) +open class RealmNYTMultimedium : RealmObject() { + var url: String? = null + var format: String? = null + var height: Int = 0 + var width: Int = 0 + var type: String? = null + var subtype: String? = null + var caption: String? = null + var copyright: String? = null + + @LinkingObjects(RealmNYTimesArticle.EMBEDDED_MULTIMEDIA) + val parent = RealmNYTimesArticle() +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/local/repository/NewsReaderRepository.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/local/repository/NewsReaderRepository.kt new file mode 100644 index 0000000000..64d6f57101 --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/local/repository/NewsReaderRepository.kt @@ -0,0 +1,122 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.data.newsreader.local.repository + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.dropbox.android.external.store4.* +import io.realm.examples.coroutinesexample.data.newsreader.local.RealmNYTDao +import io.realm.examples.coroutinesexample.data.newsreader.local.RealmNYTimesArticle +import io.realm.examples.coroutinesexample.ui.main.NewsReaderState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch + +private const val THIRTY_MINUTES = 30 * 60 * 1000 + +class NewsReaderRepository( + private val dao: RealmNYTDao, + private val store: Store> +) { + + private val _newsReaderState = MutableLiveData() + val newsReaderState: LiveData + get() = _newsReaderState + + private val sectionRefreshJobs = mutableMapOf() + + fun getTopStories(scope: CoroutineScope, apiSection: String, refresh: Boolean = false) { + scope.launch { + if (refresh) { + store.fresh(apiSection) + } else { + getFromStream(scope, apiSection) + } + } + } + + fun getStory(id: String): Flow { + return dao.getArticle(id) + } + + fun updateArticle(scope: CoroutineScope, id: String) { + scope.launch { + dao.updateArticle(id) + } + } + + fun close() { + dao.close() + sectionRefreshJobs.values.forEach { it.cancel() } + sectionRefreshJobs.clear() + } + + private fun getFromStream(scope: CoroutineScope, apiSection: String) { + store.stream(StoreRequest.cached( + key = apiSection, + refresh = false + )).onEach { response -> + val origin = response.origin.toString() + when (response) { + is StoreResponse.Loading -> NewsReaderState.Loading(origin) + is StoreResponse.Data -> getNewsReaderState(response, store, apiSection, origin) + is StoreResponse.NoNewData -> NewsReaderState.NoNewData(origin) + is StoreResponse.Error.Exception -> NewsReaderState.ErrorException(origin, response.error) + is StoreResponse.Error.Message -> NewsReaderState.ErrorMessage(origin, response.message) + }.also { + _newsReaderState.postValue(it) + } + }.launchIn( + scope + ).also { job -> + scope.launch { + sectionRefreshJobs.values.forEach { it.cancelAndJoin() } + sectionRefreshJobs.clear() + sectionRefreshJobs[apiSection] = job + } + } + } + + private suspend fun getNewsReaderState( + response: StoreResponse.Data>, + store: Store>, + apiSection: String, + origin: String + ): NewsReaderState { + val data = response.value + return if (data.isNotEmpty()) { + data.first() + .let { firstElement -> + val now = System.currentTimeMillis() + val entryExpired = (firstElement.updateTime + THIRTY_MINUTES) < now + + if (!(response.origin == ResponseOrigin.Fetcher || !entryExpired)) { + store.fresh(apiSection) + NewsReaderState.Loading(origin) + } else { + NewsReaderState.Data(origin, response.value) + } + } + } else { + NewsReaderState.Data(origin, response.value) + } + } +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/network/NYTimesApiClient.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/network/NYTimesApiClient.kt new file mode 100644 index 0000000000..c88d245275 --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/network/NYTimesApiClient.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.data.newsreader.network + +import io.realm.examples.coroutinesexample.data.newsreader.network.model.NYTimesResponse +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory +import retrofit2.converter.moshi.MoshiConverterFactory + +private const val API_KEY = "YUPmyj0Q09Fm2VlCHmD9FU7rpCcI5dUD" + +interface NYTimesApiClient { + suspend fun getTopStories(section: String): NYTimesResponse +} + +class NYTimesApiClientImpl : NYTimesApiClient { + + private val service: NYTimesService + + init { + val okHttpClient = OkHttpClient.Builder() + .addInterceptor( + HttpLoggingInterceptor() + .apply { setLevel(HttpLoggingInterceptor.Level.BASIC) } + ) + .addInterceptor { chain -> + val original = chain.request() + val originalHttpUrl = original.url + + val url = originalHttpUrl.newBuilder() + .addEncodedQueryParameter("api-key", API_KEY) + .build() + + val requestBuilder: Request.Builder = original.newBuilder() + .url(url) + + val request: Request = requestBuilder.build() + chain.proceed(request) + } + .build() + + service = Retrofit.Builder() + .client(okHttpClient) + .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) + .addConverterFactory(MoshiConverterFactory.create()) + .baseUrl("https://api.nytimes.com/") + .build() + .create(NYTimesService::class.java) + } + + override suspend fun getTopStories(section: String): NYTimesResponse { + return service.topStories(section) + } +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/network/NYTimesService.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/network/NYTimesService.kt new file mode 100644 index 0000000000..97eec6c08a --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/network/NYTimesService.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.data.newsreader.network + +import io.realm.examples.coroutinesexample.data.newsreader.network.model.NYTimesResponse +import retrofit2.http.GET +import retrofit2.http.Path + +interface NYTimesService { + @GET("svc/topstories/v2/{section}.json") + suspend fun topStories(@Path("section") section: String): NYTimesResponse +} + +val sectionsToNames = mapOf( + "Home" to "home", + "World" to "world", + "National" to "national", + "Politics" to "politics", + "NY Region" to "nyregion", + "Business" to "business", + "Opinion" to "opinion", + "Technology" to "technology", + "Science" to "science", + "Health" to "health", + "Sports" to "sports", + "Arts" to "arts", + "Fashion" to "fashion", + "Dining" to "dining", + "Travel" to "travel", + "Magazine" to "magazine", + "Real Estate" to "realestate" +) diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/network/model/NYTimes.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/network/model/NYTimes.kt new file mode 100644 index 0000000000..893a8eb7b8 --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/data/newsreader/network/model/NYTimes.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.data.newsreader.network.model + +import com.squareup.moshi.Json + +data class NYTimesResponse( + val status: String, + val copyright: String, + val section: String, + @field:Json(name = "last_updated") val lastUpdated: String, + @field:Json(name = "num_results") val numResults: Int, + val results: List +) + +data class NYTimesArticle( + val section: String, + val subsection: String, + val title: String, + @field:Json(name = "abstract") val abstractText: String?, + val url: String, + val uri: String, + val byline: String, + @field:Json(name = "item_type") val itemType: String?, + @field:Json(name = "updated_date") val updatedDate: String?, + @field:Json(name = "created_date") val createDate: String?, + @field:Json(name = "published_date") val publishedDate: String?, + @field:Json(name = "material_type_facet") val materialTypeFacet: String?, + val kicker: String, + @field:Json(name = "des_facet") val desFacet: List?, + @field:Json(name = "org_facet") val orgFacet: List?, + @field:Json(name = "per_facet") val perFacet: List?, + @field:Json(name = "geo_facet") val geoFacet: List?, + val multimedia: List, + @field:Json(name = "short_url") val shortUrl: String? +) + +data class NYTMultimedium( + val url: String, + val format: String, + val height: Int, + val width: Int, + val type: String, + val subtype: String, + val caption: String, + val copyright: String +) diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/di/DependencyGraph.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/di/DependencyGraph.kt new file mode 100644 index 0000000000..180b3c15e9 --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/di/DependencyGraph.kt @@ -0,0 +1,83 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.di + +import com.dropbox.android.external.store4.Fetcher +import com.dropbox.android.external.store4.SourceOfTruth +import com.dropbox.android.external.store4.Store +import com.dropbox.android.external.store4.StoreBuilder +import io.realm.RealmConfiguration +import io.realm.examples.coroutinesexample.data.newsreader.local.RealmNYTDao +import io.realm.examples.coroutinesexample.data.newsreader.local.RealmNYTDaoImpl +import io.realm.examples.coroutinesexample.data.newsreader.local.RealmNYTimesArticle +import io.realm.examples.coroutinesexample.data.newsreader.local.insertArticles +import io.realm.examples.coroutinesexample.data.newsreader.local.repository.NewsReaderRepository +import io.realm.examples.coroutinesexample.data.newsreader.network.NYTimesApiClient +import io.realm.examples.coroutinesexample.data.newsreader.network.NYTimesApiClientImpl +import io.realm.examples.coroutinesexample.data.newsreader.network.model.NYTimesArticle +import io.realm.examples.coroutinesexample.util.NewsReaderFlowFactory +import kotlinx.coroutines.flow.map + +/** + * Homemade, simple DI solution - ideally, we should use a proper DI framework instead. + */ +object DependencyGraph { + + // Repository dependencies + fun provideNewsReaderRepository(): NewsReaderRepository = + NewsReaderRepository(provideRealmDao(), provideStore()) + + private fun provideStore(): Store> = StoreBuilder.from( + fetcher = provideFetcher(provideApiClient()), + sourceOfTruth = provideSourceOfTruth(provideRealmDao()) + ).build() + + private fun provideFetcher(nytApiClient: NYTimesApiClient): Fetcher> = + Fetcher.of { apiSection -> + nytApiClient.getTopStories(apiSection).results + } + + private fun provideSourceOfTruth(realmDao: RealmNYTDao): SourceOfTruth, List> = + SourceOfTruth.of( + reader = { apiSection -> + realmDao.getArticles(apiSection) + .map { articles -> + if (articles.isEmpty()) null + else articles + } + }, + writer = { apiSection, articles -> + realmDao.insertArticles(apiSection, articles) + }, + delete = { apiSection -> + realmDao.deleteArticles(apiSection) + }, + deleteAll = { + realmDao.deleteAllArticles() + } + ) + + // Database dependencies + private fun provideRealmDao(): RealmNYTDao = RealmNYTDaoImpl(provideRealmConfig()) + + private fun provideRealmConfig(): RealmConfiguration = RealmConfiguration.Builder() + .flowFactory(NewsReaderFlowFactory()) + .build() + + // Network dependencies + private fun provideApiClient(): NYTimesApiClient = NYTimesApiClientImpl() +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/details/DetailsFragment.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/details/DetailsFragment.kt new file mode 100644 index 0000000000..227ef78d3a --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/details/DetailsFragment.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.ui.details + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import io.realm.examples.coroutinesexample.databinding.FragmentDetailsBinding +import kotlin.time.ExperimentalTime + +@ExperimentalTime +class DetailsFragment : Fragment() { + + private val viewModel: DetailsViewModel by viewModels() + + private lateinit var binding: FragmentDetailsBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return FragmentDetailsBinding.inflate(inflater, container, false) + .also { binding -> + this.binding = binding + binding.lifecycleOwner = viewLifecycleOwner + binding.viewModel = viewModel + setupLiveData() + }.root + } + + override fun onActivityCreated(savedInstanceState: Bundle?) { + super.onActivityCreated(savedInstanceState) + + val id = requireNotNull(requireArguments().getString(ARG_ID)) + viewModel.loadDetails(id) + } + + private fun setupLiveData() { + viewModel.read.observe(viewLifecycleOwner, Observer { + setRead() + }) + } + + private fun setRead() { + with(binding.read) { + animate().alpha(1.0f) + } + } + + data class ArgsBundle(val id: String) + + companion object { + + const val TAG = "DetailsFragment" + + private const val ARG_ID = "id" + + fun instantiate(argsBundle: ArgsBundle): DetailsFragment { + return DetailsFragment().apply { + arguments = Bundle().apply { + putString(ARG_ID, argsBundle.id) + } + } + } + } +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/details/DetailsViewModel.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/details/DetailsViewModel.kt new file mode 100644 index 0000000000..45fb32ee00 --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/details/DetailsViewModel.kt @@ -0,0 +1,79 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.ui.details + +import androidx.lifecycle.* +import io.realm.examples.coroutinesexample.data.newsreader.local.RealmNYTimesArticle +import io.realm.examples.coroutinesexample.di.DependencyGraph +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.isActive +import kotlin.time.ExperimentalTime +import kotlin.time.seconds + +@ExperimentalTime +class DetailsViewModel : ViewModel() { + + private val repository = DependencyGraph.provideNewsReaderRepository() + + private val article = MutableLiveData() + + private val _read = MutableLiveData() + val read: LiveData + get() = _read + + val date = article.map { it.updatedDate.toString() } + val title = article.map { it.title } + val articleText = article.map { it.abstractText } + + override fun onCleared() { + repository.close() + } + + fun loadDetails(id: String) { + repository.getStory(id) + .onEach { realmArticle -> + checkNotNull(realmArticle) + .also { + if (article.value == null) { + article.postValue(it) + + if (!it.read) { + markAsRead(it) + } else { + markAsRead(it, true) + } + } + } + }.launchIn(viewModelScope) + } + + private fun markAsRead(article: RealmNYTimesArticle, immediately: Boolean = false) { + if (immediately) { + _read.postValue(true) + } else { + flow { + delay(2.seconds) + repository.updateArticle(viewModelScope, article.url) + _read.postValue(true) + }.launchIn(viewModelScope) + } + } +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/main/MainAdapter.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/main/MainAdapter.kt new file mode 100644 index 0000000000..9348960709 --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/main/MainAdapter.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.ui.main + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import io.realm.examples.coroutinesexample.R +import io.realm.examples.coroutinesexample.data.newsreader.local.RealmNYTimesArticle + +class MainAdapter( + private val onClick: (String) -> Unit +) : ListAdapter(DIFF_CALLBACK) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder = + LayoutInflater.from(parent.context) + .inflate(R.layout.item_article, parent, false) + .let { view -> ArticleViewHolder(view) } + + override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) { + with(holder.title) { + val article = getItem(position) + + text = article.title + isEnabled = !article.read + } + } + + inner class ArticleViewHolder(view: View) : RecyclerView.ViewHolder(view) { + + val title: TextView = view.findViewById(R.id.title) + + init { + view.setOnClickListener { + onClick.invoke(getItem(adapterPosition).url) + } + } + } + + companion object { + val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: RealmNYTimesArticle, newItem: RealmNYTimesArticle): Boolean = + oldItem == newItem + + override fun areContentsTheSame(oldItem: RealmNYTimesArticle, newItem: RealmNYTimesArticle): Boolean = + oldItem.read == newItem.read + && oldItem.title == newItem.title + && oldItem.abstractText == newItem.abstractText + } + } +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/main/MainFragment.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/main/MainFragment.kt new file mode 100644 index 0000000000..e5e5fb5e79 --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/main/MainFragment.kt @@ -0,0 +1,195 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.ui.main + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.SpinnerAdapter +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import io.realm.examples.coroutinesexample.R +import io.realm.examples.coroutinesexample.data.newsreader.local.RealmNYTimesArticle +import io.realm.examples.coroutinesexample.data.newsreader.network.sectionsToNames +import io.realm.examples.coroutinesexample.databinding.FragmentMainBinding +import java.util.* +import kotlin.Comparator + +class MainFragment : Fragment() { + + interface OnItemClicked { + fun onItemClicked(id: String) + } + + internal lateinit var onItemclickedCallback: OnItemClicked + + private val viewModel: MainViewModel by viewModels() + private val newsReaderAdapter = MainAdapter { id -> + onItemclickedCallback.onItemClicked(id) + } + + private lateinit var binding: FragmentMainBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = FragmentMainBinding.inflate(inflater, container, false) + .also { binding -> + binding.lifecycleOwner = viewLifecycleOwner + this.binding = binding + setupSpinner() + setupRecyclerView() + setupLiveData() + }.root + + private fun setupSpinner() { + with(binding.spinner) { + adapter = ArrayAdapter( + context, + android.R.layout.simple_spinner_dropdown_item, + sectionsToNames.keys.sortedWith( + Comparator { o1, o2 -> + if (o1.toLowerCase(Locale.ROOT) == "home") return@Comparator -1 + if (o2.toLowerCase(Locale.ROOT) == "home") return@Comparator 1 + return@Comparator o1.compareTo(o2, ignoreCase = true) + } + ) + ) + onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + viewModel.getTopStories(getApiSection(adapter, position)) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + // No-op + } + } + } + } + + private fun setupRecyclerView() { + with(binding.list) { + layoutManager = LinearLayoutManager(context) + adapter = newsReaderAdapter + } + + with(binding.refresh) { + setOnRefreshListener { + with(binding.spinner) { + viewModel.getTopStories(getApiSection(adapter, selectedItemPosition), true) + } + } + } + } + + private fun setupLiveData() { + viewModel.newsReaderState.observe(viewLifecycleOwner, Observer { viewState -> + when (viewState) { + is NewsReaderState.Loading -> { + Log.d(TAG, "--- origin: ${viewState.origin}, loading") + RealmStateHelper.loading(binding) + } + is NewsReaderState.Data -> { + Log.d(TAG, "--- origin: ${viewState.origin}, elements: ${viewState.data.size}") + RealmStateHelper.data(binding, viewState.data, newsReaderAdapter) + } + is NewsReaderState.NoNewData -> { + Log.d(TAG, "--- origin: ${viewState.origin}, no new data") + RealmStateHelper.noNewData(binding) + } + is NewsReaderState.ErrorException -> { + val stacktrace = viewState.throwable.cause?.stackTrace?.joinToString { "$it\n" } + Log.e(TAG, "--- error (exception): ${viewState.throwable.message} - ${viewState.throwable.cause?.message}: $stacktrace") + RealmStateHelper.error(binding) + } + is NewsReaderState.ErrorMessage -> { + Log.e(TAG, "--- error (message): ${viewState.message}") + RealmStateHelper.error(binding) + } + } + }) + } + + private fun getApiSection(adapter: SpinnerAdapter, position: Int): String { + val apiSection = adapter.getItem(position) as String + return requireNotNull(sectionsToNames[apiSection]) + } + + companion object { + + const val TAG = "MainFragment" + + fun newInstance() = MainFragment() + } +} + +sealed class NewsReaderState { + + abstract val origin: String + + data class Loading(override val origin: String) : NewsReaderState() + data class Data(override val origin: String, val data: List) : NewsReaderState() + data class NoNewData(override val origin: String) : NewsReaderState() + data class ErrorException(override val origin: String, val throwable: Throwable) : NewsReaderState() + data class ErrorMessage(override val origin: String, val message: String) : NewsReaderState() +} + +private object RealmStateHelper { + fun loading(binding: FragmentMainBinding) { + if (!binding.refresh.isRefreshing) { + binding.refresh.setRefreshing(true) + } + } + + fun data( + binding: FragmentMainBinding, + data: List, + newsReaderAdapter: MainAdapter + ) { + hideLoadingSpinner(binding) + newsReaderAdapter.submitList(data) + } + + fun noNewData(binding: FragmentMainBinding) { + hideLoadingSpinner(binding) + } + + fun error(binding: FragmentMainBinding) { + hideLoadingSpinner(binding) + Toast.makeText(binding.root.context, R.string.error_generic, Toast.LENGTH_SHORT).show() + } + + private fun hideLoadingSpinner(binding: FragmentMainBinding) { + if (binding.refresh.isRefreshing) { + binding.refresh.setRefreshing(false) + } + } + + private fun showLoadingSpinner(binding: FragmentMainBinding) { + if (!binding.refresh.isRefreshing) { + binding.refresh.setRefreshing(true) + } + } +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/main/MainViewModel.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/main/MainViewModel.kt new file mode 100644 index 0000000000..aab018325b --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/ui/main/MainViewModel.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.ui.main + +import android.util.Log +import androidx.lifecycle.LiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import io.realm.examples.coroutinesexample.TAG +import io.realm.examples.coroutinesexample.di.DependencyGraph + +class MainViewModel : ViewModel() { + + private val repository = DependencyGraph.provideNewsReaderRepository() + + val newsReaderState: LiveData + get() = repository.newsReaderState + + override fun onCleared() { + repository.close() + } + + fun getTopStories(apiSection: String, refresh: Boolean = false) { + Log.d(TAG, "------ apiSection: $apiSection - refresh '$refresh'") + repository.getTopStories(viewModelScope, apiSection, refresh) + } +} diff --git a/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/util/NewsReaderFlowFactory.kt b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/util/NewsReaderFlowFactory.kt new file mode 100644 index 0000000000..8b00787f2a --- /dev/null +++ b/examples/coroutinesExample/src/main/java/io/realm/examples/coroutinesexample/util/NewsReaderFlowFactory.kt @@ -0,0 +1,64 @@ +/* + * Copyright 2020 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.coroutinesexample.util + +import io.realm.DynamicRealm +import io.realm.Realm +import io.realm.RealmResults +import io.realm.coroutines.RealmFlowFactory +import io.realm.rx.CollectionChange +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.drop + +/** + * Similar to [io.realm.coroutines.RealmFlowFactory] but it will not emit the current value + * immediately. This is needed by Store to function properly or else it will receive updates with + * empty [RealmResults] that will make it think existing values for the current key are present. + * + * There is no need to override the methods for [io.realm.RealmModel] since the internal factory + * does check whether or not an object is loaded before the first emission. + */ +class NewsReaderFlowFactory : RealmFlowFactory(true) { + + override fun from( + realm: Realm, + results: RealmResults + ): Flow> = + super.from(realm, results) + .drop(1) + + override fun from( + dynamicRealm: DynamicRealm, + results: RealmResults + ): Flow> = + super.from(dynamicRealm, results) + .drop(1) + + override fun changesetFrom( + realm: Realm, + results: RealmResults + ): Flow>> = + super.changesetFrom(realm, results) + .drop(1) + + override fun changesetFrom( + dynamicRealm: DynamicRealm, + results: RealmResults + ): Flow>> = + super.changesetFrom(dynamicRealm, results) + .drop(1) +} diff --git a/examples/coroutinesExample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/coroutinesExample/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..7706ab9e6d --- /dev/null +++ b/examples/coroutinesExample/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + diff --git a/examples/coroutinesExample/src/main/res/drawable/ic_launcher_background.xml b/examples/coroutinesExample/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..07d5da9cbf --- /dev/null +++ b/examples/coroutinesExample/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/coroutinesExample/src/main/res/layout/activity_main.xml b/examples/coroutinesExample/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..0b160e16e8 --- /dev/null +++ b/examples/coroutinesExample/src/main/res/layout/activity_main.xml @@ -0,0 +1,22 @@ + + + diff --git a/examples/coroutinesExample/src/main/res/layout/fragment_details.xml b/examples/coroutinesExample/src/main/res/layout/fragment_details.xml new file mode 100644 index 0000000000..e2f0d9aa5a --- /dev/null +++ b/examples/coroutinesExample/src/main/res/layout/fragment_details.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/coroutinesExample/src/main/res/layout/fragment_main.xml b/examples/coroutinesExample/src/main/res/layout/fragment_main.xml new file mode 100644 index 0000000000..3b85af837d --- /dev/null +++ b/examples/coroutinesExample/src/main/res/layout/fragment_main.xml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/coroutinesExample/src/main/res/layout/item_article.xml b/examples/coroutinesExample/src/main/res/layout/item_article.xml new file mode 100644 index 0000000000..37708b31bb --- /dev/null +++ b/examples/coroutinesExample/src/main/res/layout/item_article.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/examples/coroutinesExample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/examples/coroutinesExample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000000..6b78462d61 --- /dev/null +++ b/examples/coroutinesExample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/coroutinesExample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/examples/coroutinesExample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000000..6b78462d61 --- /dev/null +++ b/examples/coroutinesExample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/coroutinesExample/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/coroutinesExample/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000..a571e60098 Binary files /dev/null and b/examples/coroutinesExample/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/coroutinesExample/src/main/res/mipmap-hdpi/ic_launcher_round.png b/examples/coroutinesExample/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000..61da551c55 Binary files /dev/null and b/examples/coroutinesExample/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/examples/coroutinesExample/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/coroutinesExample/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000..c41dd28531 Binary files /dev/null and b/examples/coroutinesExample/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/examples/coroutinesExample/src/main/res/mipmap-mdpi/ic_launcher_round.png b/examples/coroutinesExample/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000..db5080a752 Binary files /dev/null and b/examples/coroutinesExample/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/examples/coroutinesExample/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/coroutinesExample/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000..6dba46dab1 Binary files /dev/null and b/examples/coroutinesExample/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/coroutinesExample/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/examples/coroutinesExample/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..da31a871c8 Binary files /dev/null and b/examples/coroutinesExample/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/examples/coroutinesExample/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/coroutinesExample/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000..15ac681720 Binary files /dev/null and b/examples/coroutinesExample/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/coroutinesExample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/examples/coroutinesExample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..b216f2d313 Binary files /dev/null and b/examples/coroutinesExample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/examples/coroutinesExample/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/coroutinesExample/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000..f25a419744 Binary files /dev/null and b/examples/coroutinesExample/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/examples/coroutinesExample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/examples/coroutinesExample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000..e96783ccce Binary files /dev/null and b/examples/coroutinesExample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/examples/coroutinesExample/src/main/res/values/colors.xml b/examples/coroutinesExample/src/main/res/values/colors.xml new file mode 100644 index 0000000000..030098fe0f --- /dev/null +++ b/examples/coroutinesExample/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #6200EE + #3700B3 + #03DAC5 + diff --git a/examples/coroutinesExample/src/main/res/values/dimens.xml b/examples/coroutinesExample/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..e8c0a99498 --- /dev/null +++ b/examples/coroutinesExample/src/main/res/values/dimens.xml @@ -0,0 +1,8 @@ + + + 8dp + 16dp + 8dp + 40dp + 16dp + diff --git a/examples/coroutinesExample/src/main/res/values/strings.xml b/examples/coroutinesExample/src/main/res/values/strings.xml new file mode 100644 index 0000000000..084c24188e --- /dev/null +++ b/examples/coroutinesExample/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + Coroutines Example + An error has occurred. Check Logcat for more details. + Read + diff --git a/examples/coroutinesExample/src/main/res/values/styles.xml b/examples/coroutinesExample/src/main/res/values/styles.xml new file mode 100644 index 0000000000..879324ef70 --- /dev/null +++ b/examples/coroutinesExample/src/main/res/values/styles.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/examples/newsreaderExample/src/main/res/values-v21/styles.xml b/examples/coroutinesExample/src/main/res/values/themes.xml similarity index 50% rename from examples/newsreaderExample/src/main/res/values-v21/styles.xml rename to examples/coroutinesExample/src/main/res/values/themes.xml index dbbdd40f49..de62580a83 100644 --- a/examples/newsreaderExample/src/main/res/values-v21/styles.xml +++ b/examples/coroutinesExample/src/main/res/values/themes.xml @@ -3,7 +3,9 @@ + + + + diff --git a/examples/multiprocessExample/.gitignore b/examples/multiprocessExample/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/examples/multiprocessExample/.gitignore @@ -0,0 +1 @@ +/build diff --git a/examples/multiprocessExample/build.gradle b/examples/multiprocessExample/build.gradle new file mode 100644 index 0000000000..155832dcbf --- /dev/null +++ b/examples/multiprocessExample/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.application' +apply plugin: 'realm-android' + +android { + compileSdkVersion rootProject.sdkVersion + buildToolsVersion rootProject.buildTools + + defaultConfig { + applicationId "io.realm.examples.realmmultiprocessexample" + targetSdkVersion rootProject.sdkVersion + minSdkVersion rootProject.minSdkVersion + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled true + signingConfig signingConfigs.debug + } + debug { + minifyEnabled true + } + } +} + +dependencies { + implementation 'com.android.support:appcompat-v7:27.1.1' +} diff --git a/examples/multiprocessExample/proguard-rules.pro b/examples/multiprocessExample/proguard-rules.pro new file mode 100644 index 0000000000..8456b3daec --- /dev/null +++ b/examples/multiprocessExample/proguard-rules.pro @@ -0,0 +1,17 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /home/cc/.android-sdk/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/examples/multiprocessExample/src/main/AndroidManifest.xml b/examples/multiprocessExample/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..91862ee505 --- /dev/null +++ b/examples/multiprocessExample/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + diff --git a/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/AnotherProcessService.java b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/AnotherProcessService.java new file mode 100644 index 0000000000..72c56433d5 --- /dev/null +++ b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/AnotherProcessService.java @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.realmmultiprocessexample; + +import android.app.Service; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; + +import io.realm.Realm; + +public class AnotherProcessService extends Service { + + Handler handler; + + @Override + public void onCreate() { + super.onCreate(); + + handler = new Handler(Looper.myLooper()); + final Runnable runnable = new Runnable() { + @Override + public void run() { + Realm realm = Realm.getDefaultInstance(); + realm.beginTransaction(); + realm.copyToRealmOrUpdate(Utils.createStandaloneProcessInfo(AnotherProcessService.this)); + realm.commitTransaction(); + realm.close(); + handler.postDelayed(this, 1000); + } + }; + handler.postDelayed(runnable, 1000); + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + @Override + public IBinder onBind(Intent intent) { + throw new UnsupportedOperationException("Not yet implemented"); + } +} diff --git a/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/MainActivity.java b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/MainActivity.java new file mode 100644 index 0000000000..6c279d4a99 --- /dev/null +++ b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/MainActivity.java @@ -0,0 +1,90 @@ +/* + * Copyright 2017 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.examples.realmmultiprocessexample; + +import android.content.Intent; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.view.View; +import android.widget.TextView; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Locale; + +import io.realm.Realm; +import io.realm.RealmChangeListener; +import io.realm.RealmResults; +import io.realm.examples.realmmultiprocessexample.models.ProcessInfo; + +public class MainActivity extends AppCompatActivity { + + private TextView textView; + private Realm realm; + private RealmResults processInfoResults; + private DateFormat dateFormat = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss", Locale.ENGLISH); + + private RealmChangeListener> listener = + new RealmChangeListener>() { + @Override + public void onChange(RealmResults results) { + StringBuilder stringBuilder = new StringBuilder(); + + for (ProcessInfo processInfo : results) { + stringBuilder.append(processInfo.getName()); + stringBuilder.append("\npid: "); + stringBuilder.append(processInfo.getPid()); + stringBuilder.append("\nlast response time: "); + stringBuilder.append(dateFormat.format(processInfo.getLastResponseDate())); + stringBuilder.append("\n------\n"); + } + textView.setText(stringBuilder.toString()); + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + textView = (TextView) findViewById(R.id.textView); + + if (realm == null) { + realm = Realm.getDefaultInstance(); + processInfoResults = realm.where(ProcessInfo.class).findAllAsync(); + processInfoResults.addChangeListener(listener); + } + + realm.beginTransaction(); + realm.copyToRealmOrUpdate(Utils.createStandaloneProcessInfo(this)); + realm.commitTransaction(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (realm != null) { + realm.close(); + realm = null; + processInfoResults = null; + } + } + + public void onStartButton(View button) { + Intent intent = new Intent(MainActivity.this, AnotherProcessService.class); + startService(intent); + button.setEnabled(false); + } +} diff --git a/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/MyApplication.java b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/MyApplication.java new file mode 100644 index 0000000000..7d55aa6831 --- /dev/null +++ b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/MyApplication.java @@ -0,0 +1,36 @@ +/* + * Copyright 2017 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.realmmultiprocessexample; + +import android.app.Application; + +import io.realm.Realm; +import io.realm.RealmConfiguration; + +public class MyApplication extends Application { + @Override + public void onCreate() { + super.onCreate(); + Realm.init(this); + RealmConfiguration configuration = new RealmConfiguration.Builder() + .deleteRealmIfMigrationNeeded() + .allowWritesOnUiThread(true) + .allowQueriesOnUiThread(true) + .build(); + Realm.setDefaultConfiguration(configuration); + } +} diff --git a/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/Utils.java b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/Utils.java new file mode 100644 index 0000000000..68baaee8f5 --- /dev/null +++ b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/Utils.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.realm.examples.realmmultiprocessexample; + +import android.app.ActivityManager; +import android.app.ActivityManager.RunningAppProcessInfo; +import android.content.Context; +import android.os.Process; + +import java.util.Date; +import java.util.List; + +import io.realm.examples.realmmultiprocessexample.models.ProcessInfo; + +public class Utils { + + public static String getMyProcessName(Context context) { + String processName = ""; + ActivityManager am = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE); + List infoList = am.getRunningAppProcesses(); + if (infoList == null) { + throw new RuntimeException("getRunningAppProcesses() returns 'null'."); + } + for (RunningAppProcessInfo info : infoList) { + try { + if (info.pid == Process.myPid()) { + processName = info.processName; + break; + } + } catch (Exception ignored) { + } + } + return processName; + } + + public static ProcessInfo createStandaloneProcessInfo(Context context) { + ProcessInfo processInfo = new ProcessInfo(); + processInfo.setName(getMyProcessName(context)); + processInfo.setPid(android.os.Process.myPid()); + processInfo.setLastResponseDate(new Date()); + + return processInfo; + } +} diff --git a/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/models/ProcessInfo.java b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/models/ProcessInfo.java new file mode 100644 index 0000000000..7df0bdff41 --- /dev/null +++ b/examples/multiprocessExample/src/main/java/io/realm/examples/realmmultiprocessexample/models/ProcessInfo.java @@ -0,0 +1,54 @@ +/* + * Copyright 2017 Realm Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.realm.examples.realmmultiprocessexample.models; + +import java.util.Date; + +import io.realm.RealmObject; +import io.realm.annotations.PrimaryKey; +import io.realm.annotations.Required; + +public class ProcessInfo extends RealmObject { + @PrimaryKey + private String name; + private int pid; + @Required + private Date lastResponseDate; + + public int getPid() { + return pid; + } + + public void setPid(int pid) { + this.pid = pid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getLastResponseDate() { + return lastResponseDate; + } + + public void setLastResponseDate(Date lastResponseDate) { + this.lastResponseDate = lastResponseDate; + } +} diff --git a/examples/multiprocessExample/src/main/res/layout/activity_main.xml b/examples/multiprocessExample/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..79f8daf590 --- /dev/null +++ b/examples/multiprocessExample/src/main/res/layout/activity_main.xml @@ -0,0 +1,26 @@ + + + +