├── demo
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ └── styles.xml
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.png
│ │ │ └── ic_launcher_round.png
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── layout
│ │ │ └── activity_main.xml
│ │ ├── drawable-v24
│ │ │ └── ic_launcher_foreground.xml
│ │ └── drawable
│ │ │ └── ic_launcher_background.xml
│ │ ├── kotlin
│ │ └── com
│ │ │ └── otalistudios
│ │ │ └── firestore
│ │ │ └── demo
│ │ │ ├── MainActivity.kt
│ │ │ └── User.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle.kts
├── compiler
├── .gitignore
├── src
│ └── main
│ │ ├── resources
│ │ └── META-INF
│ │ │ ├── gradle
│ │ │ └── incremental.annotation.processors
│ │ │ └── services
│ │ │ └── javax.annotation.processing.Processor
│ │ └── kotlin
│ │ └── com
│ │ └── otaliastudios
│ │ └── firestore
│ │ └── compiler
│ │ ├── Property.kt
│ │ ├── shared.kt
│ │ ├── Types.kt
│ │ └── Processor.kt
└── build.gradle.kts
├── firestore
├── .gitignore
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── kotlin
│ │ └── com
│ │ └── otaliastudios
│ │ └── firestore
│ │ ├── FirestoreClass.kt
│ │ ├── parcel
│ │ ├── TimestampParceler.kt
│ │ ├── DocumentReferenceParceler.kt
│ │ ├── FieldValueParceler.kt
│ │ ├── FirestoreParceler.kt
│ │ └── api.kt
│ │ ├── config.kt
│ │ ├── cache.kt
│ │ ├── metadata.kt
│ │ ├── FirestoreFieldDelegate.kt
│ │ ├── FirestoreLogger.kt
│ │ ├── batchWrite.kt
│ │ ├── snapshots.kt
│ │ ├── FirestoreDocument.kt
│ │ ├── FirestoreList.kt
│ │ └── FirestoreMap.kt
├── proguard-rules.pro
└── build.gradle.kts
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle.kts
├── .github
├── workflows
│ ├── build.yml
│ └── deploy.yml
└── FUNDING.yml
├── CHANGELOG.md
├── gradle.properties
├── gradlew.bat
├── gradlew
├── README.md
└── LICENSE
/demo/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/compiler/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/firestore/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Firestore
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natario1/Firestore/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 |
--------------------------------------------------------------------------------
/compiler/src/main/resources/META-INF/gradle/incremental.annotation.processors:
--------------------------------------------------------------------------------
1 | com.otaliastudios.firestore.compiler.Processor,isolating
--------------------------------------------------------------------------------
/compiler/src/main/resources/META-INF/services/javax.annotation.processing.Processor:
--------------------------------------------------------------------------------
1 | com.otaliastudios.firestore.compiler.Processor
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natario1/Firestore/HEAD/demo/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natario1/Firestore/HEAD/demo/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natario1/Firestore/HEAD/demo/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natario1/Firestore/HEAD/demo/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natario1/Firestore/HEAD/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | */
4 |
5 | include(":compiler", ":firestore", ":demo")
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natario1/Firestore/HEAD/demo/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natario1/Firestore/HEAD/demo/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natario1/Firestore/HEAD/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natario1/Firestore/HEAD/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/natario1/Firestore/HEAD/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/firestore/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
7 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/compiler/src/main/kotlin/com/otaliastudios/firestore/compiler/Property.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore.compiler
2 |
3 | data class Property(
4 | val name: String,
5 | val type: String,
6 | val isNullable: Boolean,
7 | val isBindable: Boolean = false)
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sat May 19 15:36:51 CEST 2018
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-all.zip
7 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/demo/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/firestore/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Seems that keepnames is not enough.
2 | -keep class * implements com.otaliastudios.firestore.FirestoreMetadata { *; }
3 | -keep class * extends com.otaliastudios.firestore.FirestoreMap { *; }
4 | -keep class * extends com.otaliastudios.firestore.FirestoreList { *; }
5 | -keep class * extends com.otaliastudios.firestore.FirestoreDocument { *; }
6 |
--------------------------------------------------------------------------------
/demo/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/demo/src/main/kotlin/com/otalistudios/firestore/demo/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.otalistudios.firestore.demo
2 |
3 | import androidx.appcompat.app.AppCompatActivity
4 | import android.os.Bundle
5 |
6 | class MainActivity : AppCompatActivity() {
7 |
8 | override fun onCreate(savedInstanceState: Bundle?) {
9 | super.onCreate(savedInstanceState)
10 | setContentView(R.layout.activity_main)
11 |
12 | // Simple test to ensure that User class is processed
13 | UserMetadataImpl()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreClass.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | */
4 |
5 | package com.otaliastudios.firestore
6 |
7 | import androidx.annotation.Keep
8 |
9 | /**
10 | * Identifies [FirestoreDocument], [FirestoreMap]s and [FirestoreList]s
11 | * so that they can be processed by the annotation processor.
12 | */
13 | @Keep
14 | @Target(AnnotationTarget.CLASS)
15 | @Retention(AnnotationRetention.RUNTIME)
16 | public annotation class FirestoreClass
17 | // Keep in sync with compiler!
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
2 | # Renaming ? Change the README badge.
3 | name: Build
4 | on:
5 | push:
6 | branches:
7 | - master
8 | pull_request:
9 | jobs:
10 | ANDROID_BASE_CHECKS:
11 | name: Base Checks
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 | - uses: actions/setup-java@v1
16 | with:
17 | java-version: 1.8
18 | - name: Perform base checks
19 | run: ./gradlew demo:assembleDebug publishToDirectory
--------------------------------------------------------------------------------
/compiler/src/main/kotlin/com/otaliastudios/firestore/compiler/shared.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | */
4 |
5 | @file:Suppress("PackageDirectoryMismatch")
6 | package com.otaliastudios.firestore
7 |
8 | @Target(AnnotationTarget.CLASS)
9 | @Retention(AnnotationRetention.RUNTIME)
10 | annotation class FirestoreClass
11 |
12 | @Suppress("unused")
13 | interface FirestoreMetadata {
14 | fun create(key: String): T?
15 | fun isNullable(key: String): Boolean
16 | fun getBindableResource(key: String): Int?
17 | fun createInnerType(): T?
18 |
19 | companion object {
20 | const val SUFFIX = "MetadataImpl"
21 | }
22 | }
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/workflow-syntax-for-github-actions
2 | name: Deploy
3 | on:
4 | release:
5 | types: [published]
6 | jobs:
7 | BINTRAY_UPLOAD:
8 | name: Bintray Upload
9 | runs-on: ubuntu-latest
10 | env:
11 | BINTRAY_USER: ${{ secrets.BINTRAY_USER }}
12 | BINTRAY_KEY: ${{ secrets.BINTRAY_KEY }}
13 | BINTRAY_REPO: ${{ secrets.BINTRAY_REPO }}
14 | steps:
15 | - uses: actions/checkout@v2
16 | - uses: actions/setup-java@v1
17 | with:
18 | java-version: 1.8
19 | - name: Publish to bintray
20 | run: ./gradlew publishToBintray
21 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [natario1]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/TimestampParceler.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore.parcel
2 |
3 | import android.os.Parcel
4 | import com.google.firebase.Timestamp
5 | import com.otaliastudios.firestore.parcel.FirestoreParceler
6 |
7 | /**
8 | * Parcels a possibly null Timestamp.
9 | */
10 | public object TimestampParceler: FirestoreParceler {
11 |
12 | override fun create(parcel: Parcel): Timestamp {
13 | return Timestamp(parcel.readLong(), parcel.readInt())
14 | }
15 |
16 | override fun write(data: Timestamp, parcel: Parcel, flags: Int) {
17 | parcel.writeLong(data.seconds)
18 | parcel.writeInt(data.nanoseconds)
19 | }
20 | }
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/config.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore
2 |
3 | import android.util.LruCache
4 | import com.google.firebase.firestore.ktx.firestore
5 | import com.google.firebase.ktx.Firebase
6 | import com.otaliastudios.firestore.parcel.DocumentReferenceParceler
7 | import com.otaliastudios.firestore.parcel.FieldValueParceler
8 | import com.otaliastudios.firestore.parcel.TimestampParceler
9 | import com.otaliastudios.firestore.parcel.registerParceler
10 |
11 | // TODO make it configurable (FirebaseApp)
12 | internal val FIRESTORE by lazy {
13 | Firebase.firestore.also {
14 | // One time setup
15 | registerParceler(DocumentReferenceParceler)
16 | registerParceler(TimestampParceler)
17 | registerParceler(FieldValueParceler)
18 | }
19 | }
--------------------------------------------------------------------------------
/demo/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/demo/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.kts.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/DocumentReferenceParceler.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore.parcel
2 |
3 | import android.os.Parcel
4 | import com.google.firebase.firestore.DocumentReference
5 | import com.google.firebase.firestore.FirebaseFirestore
6 | import com.otaliastudios.firestore.parcel.FirestoreParceler
7 |
8 | /**
9 | * Parcels a possibly null DocumentReference.
10 | * Note that it uses [FirebaseFirestore.getInstance] to do so.
11 | */
12 | public object DocumentReferenceParceler: FirestoreParceler {
13 |
14 | override fun create(parcel: Parcel): DocumentReference {
15 | return FirebaseFirestore.getInstance().document(parcel.readString()!!)
16 | }
17 |
18 | override fun write(data: DocumentReference, parcel: Parcel, flags: Int) {
19 | parcel.writeString(data.path)
20 | }
21 | }
--------------------------------------------------------------------------------
/demo/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Starting from v0.7.0, you can [support development](https://github.com/sponsors/natario1) through the GitHub Sponsors program.
2 | Companies can share a tiny part of their revenue and get private support hours in return. Thanks!
3 |
4 | ## v0.7.0
5 |
6 | - New: Upgrade to Kotlin 1.4, Firestore 21.6.0 ([#12][12])
7 | - Breaking change: `FirestoreParcelers.add` is now `registerParceler` or `FirestoreParceler.register` ([#12][12])
8 | - Breaking change: `FirestoreDocument.CacheState` is now `FirestoreCacheState` ([#12][12])
9 | - Breaking change: parcelers moved to `com.otaliastudios.firestore.parcel.*` package ([#12][12])
10 | - Fix: do not crash exception when metadata is not present ([#12][12])
11 |
12 |
13 |
14 | [natario1]: https://github.com/natario1
15 |
16 | [12]: https://github.com/natario1/Firestore/pull/12
17 |
--------------------------------------------------------------------------------
/demo/src/main/kotlin/com/otalistudios/firestore/demo/User.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("unused")
2 |
3 | package com.otalistudios.firestore.demo
4 |
5 | import com.otaliastudios.firestore.FirestoreClass
6 | import com.otaliastudios.firestore.FirestoreDocument
7 | import com.otaliastudios.firestore.FirestoreList
8 | import com.otaliastudios.firestore.FirestoreMap
9 |
10 | @FirestoreClass
11 | class User : FirestoreDocument() {
12 | var type: Int by this
13 | var imageUrl: String? by this("image_url")
14 | var messages: Messages by this
15 |
16 | @FirestoreClass
17 | class Messages : FirestoreList()
18 |
19 | @FirestoreClass
20 | class Message : FirestoreMap() {
21 | var from: String by this
22 | var to: String by this
23 | var text: String? by this()
24 | }
25 |
26 | init {
27 | // Default values
28 | type = 1
29 | }
30 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | #
4 |
5 | # Project-wide Gradle settings.
6 | # IDE (e.g. Android Studio) users:
7 | # Gradle settings configured through the IDE *will override*
8 | # any settings specified in this file.
9 | # For more details on how to configure your build environment visit
10 | # http://www.gradle.org/docs/current/userguide/build_environment.html
11 | # Specifies the JVM arguments used for the daemon process.
12 | # The setting is particularly useful for tweaking memory settings.
13 | org.gradle.jvmargs=-Xmx3072m
14 |
15 | android.enableJetifier=true
16 | android.useAndroidX=true
17 |
18 | # When configured, Gradle will run in incubating parallel mode.
19 | # This option should only be used with decoupled projects. More details, visit
20 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
21 | # org.gradle.parallel=true
22 |
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FieldValueParceler.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore.parcel
2 |
3 | import android.os.Parcel
4 | import com.google.firebase.firestore.FieldValue
5 | import com.otaliastudios.firestore.parcel.FirestoreParceler
6 |
7 | /**
8 | * Parcels a [FieldValue].
9 | */
10 | public object FieldValueParceler: FirestoreParceler {
11 |
12 | override fun create(parcel: Parcel): FieldValue {
13 | return when (val what = parcel.readString()) {
14 | "delete" -> FieldValue.delete()
15 | "timestamp" -> FieldValue.serverTimestamp()
16 | else -> throw RuntimeException("Unknown FieldValue value: $what")
17 | }
18 | }
19 |
20 | override fun write(data: FieldValue, parcel: Parcel, flags: Int) {
21 | if (data == FieldValue.delete()) {
22 | parcel.writeString("delete")
23 | } else if (data == FieldValue.serverTimestamp()) {
24 | parcel.writeString("timestamp")
25 | } else throw RuntimeException("Cant parcel this FieldValue: $this")
26 | }
27 | }
--------------------------------------------------------------------------------
/compiler/src/main/kotlin/com/otaliastudios/firestore/compiler/Types.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore.compiler
2 |
3 | object Types {
4 | const val FIRESTORE_CLASS = "com.otaliastudios.firestore.FirestoreClass"
5 | const val FIRESTORE_DOCUMENT = "com.otaliastudios.firestore.FirestoreDocument"
6 | const val FIRESTORE_MAP = "com.otaliastudios.firestore.FirestoreMap"
7 | const val FIRESTORE_LIST = "com.otaliastudios.firestore.FirestoreList"
8 | const val DATABINDING_BR = "androidx.databinding.library.baseAdapters.BR"
9 |
10 | const val TIMESTAMP = "com.google.firebase.Timestamp"
11 |
12 | const val KOTLIN_INT = "kotlin.Int"
13 | const val KOTLIN_FLOAT = "kotlin.Float"
14 | const val KOTLIN_DOUBLE = "kotlin.Double"
15 | const val KOTLIN_STRING = "kotlin.String"
16 | const val KOTLIN_BOOLEAN = "kotlin.Boolean"
17 |
18 | const val FIRESTORE_EXCLUDE = "com.google.firebase.firestore.Exclude"
19 | const val NULLABLE = "org.jetbrains.annotations.Nullable"
20 | const val SUPPRESS = "kotlin.Suppress"
21 | const val BINDABLE = "androidx.databinding.Bindable"
22 | }
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/cache.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore
2 |
3 | import android.util.LruCache
4 | import kotlin.reflect.KClass
5 |
6 | public object FirestoreCache {
7 | // TODO make size configurable
8 | private val cache = LruCache(100)
9 |
10 | @Suppress("UNCHECKED_CAST")
11 | public fun get(id: String, type: KClass): T? {
12 | val cached = cache[id] ?: return null
13 | require(type.isInstance(cached)) { "Cached object is not of the given type: $type / ${cached::class}"}
14 | return cached as T
15 | }
16 |
17 | @Suppress("unused")
18 | public inline operator fun get(id: String): T? {
19 | return get(id, T::class)
20 | }
21 |
22 | internal operator fun set(id: String, value: T) {
23 | cache.put(id, value)
24 | }
25 |
26 | internal fun remove(id: String) {
27 | cache.remove(id)
28 | }
29 | }
30 |
31 | public enum class FirestoreCacheState {
32 | FRESH, CACHED_EQUAL, CACHED_CHANGED
33 | }
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/FirestoreParceler.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore.parcel
2 |
3 | import android.os.Parcel
4 | import kotlin.reflect.KClass
5 |
6 | public interface FirestoreParceler {
7 |
8 | /**
9 | * Writes the [T] instance state to the [parcel].
10 | */
11 | public fun write(data: T, parcel: Parcel, flags: Int)
12 |
13 | /**
14 | * Reads the [T] instance state from the [parcel], constructs the new [T] instance and returns it.
15 | */
16 | public fun create(parcel: Parcel): T
17 |
18 | public companion object {
19 | /**
20 | * Registers a new [FirestoreParceler].
21 | */
22 | @Suppress("unused")
23 | public fun register(klass: KClass, parceler: FirestoreParceler) {
24 | registerParceler(klass, parceler)
25 | }
26 |
27 | /**
28 | * Registers a new [FirestoreParceler].
29 | */
30 | @Suppress("unused")
31 | public inline fun register(parceler: FirestoreParceler) {
32 | registerParceler(T::class, parceler)
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/demo/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | */
4 |
5 | plugins {
6 | id("com.android.application")
7 | id("kotlin-android")
8 | id("kotlin-kapt")
9 | }
10 |
11 | android {
12 | setCompileSdkVersion(rootProject.extra["compileSdkVersion"] as Int)
13 |
14 | defaultConfig {
15 | applicationId = "com.otalistudios.firestore.demo"
16 | setMinSdkVersion(21)
17 | setTargetSdkVersion(rootProject.extra["targetSdkVersion"] as Int)
18 | versionCode = 1
19 | versionName = "1.0"
20 | }
21 |
22 | sourceSets {
23 | getByName("main").java.srcDir("src/main/kotlin")
24 | getByName("test").java.srcDir("src/test/kotlin")
25 | }
26 |
27 | buildTypes {
28 | getByName("debug").isMinifyEnabled = false
29 | getByName("release").isMinifyEnabled = false
30 | }
31 | }
32 |
33 | dependencies {
34 |
35 | implementation(project(":firestore"))
36 | kapt(project(":compiler"))
37 | implementation("androidx.appcompat:appcompat:1.2.0")
38 | implementation("androidx.core:core-ktx:1.3.1")
39 | implementation("androidx.constraintlayout:constraintlayout:2.0.1")
40 | }
41 |
--------------------------------------------------------------------------------
/compiler/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | */
4 |
5 | import com.otaliastudios.tools.publisher.common.License
6 | import com.otaliastudios.tools.publisher.common.Release
7 |
8 | plugins {
9 | id("kotlin")
10 | id("com.otaliastudios.tools.publisher")
11 | }
12 |
13 | java {
14 | sourceCompatibility = JavaVersion.VERSION_1_8
15 | targetCompatibility = JavaVersion.VERSION_1_8
16 | }
17 |
18 | dependencies {
19 | api("com.squareup:kotlinpoet:1.5.0")
20 | api("com.squareup:kotlinpoet-metadata:1.5.0")
21 | api("com.squareup:kotlinpoet-metadata-specs:1.5.0")
22 | }
23 |
24 | publisher {
25 | project.artifact = "firestore-compiler"
26 | project.description = property("libDescription") as String
27 | project.group = property("libGroup") as String
28 | project.url = property("githubUrl") as String
29 | project.vcsUrl = property("githubGit") as String
30 | project.addLicense(License.APACHE_2_0)
31 | release.version = property("libVersion") as String
32 | release.setSources(Release.SOURCES_AUTO)
33 | release.setDocs(Release.DOCS_AUTO)
34 | bintray {
35 | auth.user = "BINTRAY_USER"
36 | auth.key = "BINTRAY_KEY"
37 | auth.repo = "BINTRAY_REPO"
38 | }
39 | directory {
40 | directory = "../build/maven"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/metadata.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | */
4 |
5 | package com.otaliastudios.firestore
6 |
7 | import androidx.annotation.Keep
8 | import kotlin.reflect.KClass
9 |
10 | // Keep in sync with compiler!
11 | // And also proguard rules
12 | @Keep
13 | public interface FirestoreMetadata {
14 | public fun create(key: String): T?
15 | public fun isNullable(key: String): Boolean
16 | public fun getBindableResource(key: String): Int?
17 | public fun createInnerType(): T?
18 |
19 | public companion object {
20 | public const val SUFFIX: String = "MetadataImpl"
21 | }
22 | }
23 |
24 | private val log = FirestoreLogger("Metadata")
25 |
26 | private val METADATA
27 | = mutableMapOf()
28 |
29 | internal val KClass<*>.metadata : FirestoreMetadata? get() {
30 | val name = java.name
31 | if (!METADATA.containsKey(name)) {
32 | try {
33 | val classPackage = java.`package`!!.name
34 | val className = java.simpleName
35 | val metadata = Class.forName("$classPackage.$className${FirestoreMetadata.SUFFIX}")
36 | METADATA[name] = metadata.newInstance() as FirestoreMetadata
37 | } catch (e: Exception) {
38 | log.w(e) { "Error while fetching class metadata." }
39 | }
40 | }
41 | return METADATA[name]
42 | }
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreFieldDelegate.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore
2 |
3 | import kotlin.properties.ReadWriteProperty
4 | import kotlin.reflect.KProperty
5 |
6 | /**
7 | * Provides delegation (by this() or by this(name)) to [FirestoreMap] fields.
8 | */
9 | internal class FirestoreFieldDelegate(private val field: String? = null)
10 | : ReadWriteProperty, ValueType> {
11 |
12 | private val KProperty<*>.field get() = this@FirestoreFieldDelegate.field ?: this.name
13 |
14 | override fun setValue(thisRef: FirestoreMap, property: KProperty<*>, value: ValueType) {
15 | thisRef[property.field] = value
16 | }
17 |
18 | override fun getValue(thisRef: FirestoreMap, property: KProperty<*>): ValueType {
19 | @Suppress("UNCHECKED_CAST")
20 | var what = thisRef[property.field] as ValueType
21 | if (what == null) {
22 | val metadata = this::class.metadata
23 | if (metadata != null && !metadata.isNullable(property.field)) {
24 | what = metadata.create(property.field)!!
25 | thisRef[property.field] = what
26 | // We don't want this to be dirty now! It was just retrieved, not really set.
27 | // If we leave it dirty, it would not be updated on next mergeValues().
28 | thisRef.clearDirt(property.field)
29 | }
30 | }
31 | return what
32 | }
33 |
34 | }
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreLogger.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("unused")
2 |
3 | package com.otaliastudios.firestore
4 |
5 | import android.util.Log
6 |
7 | /**
8 | * Lazy logger used across the library.
9 | * Use [setLevel] to configure the verbosity.
10 | */
11 | public class FirestoreLogger internal constructor(private val tag: String) {
12 |
13 | public companion object {
14 | public const val VERBOSE: Int = Log.VERBOSE
15 | public const val INFO: Int = Log.INFO
16 | public const val WARN: Int = Log.WARN
17 | public const val ERROR: Int = Log.ERROR
18 |
19 | private var level = WARN
20 |
21 | @JvmStatic
22 | public fun setLevel(level: Int) {
23 | this.level = level
24 | }
25 | }
26 |
27 | internal fun w(message: () -> String) {
28 | if (level <= WARN) Log.w(tag, message())
29 | }
30 |
31 | internal fun w(throwable: Throwable, message: () -> String) {
32 | if (level <= WARN) Log.w(tag, message(), throwable)
33 | }
34 |
35 | internal fun e(message: () -> String) {
36 | if (level <= ERROR) Log.e(tag, message())
37 | }
38 |
39 | internal fun e(throwable: Throwable, message: () -> String) {
40 | if (level <= ERROR) Log.e(tag, message(), throwable)
41 | }
42 |
43 | internal fun i(message: () -> String) {
44 | if (level <= INFO) Log.i(tag, message())
45 | }
46 |
47 | internal fun i(throwable: Throwable, message: () -> String) {
48 | if (level <= INFO) Log.i(tag, message(), throwable)
49 | }
50 |
51 | internal fun v(message: () -> String) {
52 | if (level <= VERBOSE) Log.v(tag, message())
53 | }
54 |
55 | internal fun v(throwable: Throwable, message: () -> String) {
56 | if (level <= VERBOSE) Log.v(tag, message(), throwable)
57 | }
58 | }
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/batchWrite.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore
2 |
3 | import com.google.android.gms.tasks.Task
4 | import com.google.android.gms.tasks.Tasks
5 | import com.google.firebase.firestore.WriteBatch
6 |
7 |
8 | /**
9 | * Initiates a batch write operation.
10 | */
11 | @Suppress("unused")
12 | public fun batchWrite(updates: FirestoreBatchWriter.() -> Unit): Task {
13 | return FirestoreBatchWrite().run {
14 | perform(updates)
15 | commit()
16 | }
17 | }
18 |
19 | public class FirestoreBatchWriter internal constructor(private val batch: WriteBatch) {
20 |
21 | internal val ops = mutableListOf()
22 |
23 | /**
24 | * Adds a document save to the batch operation.
25 | */
26 | @Suppress("unused")
27 | public fun save(document: FirestoreDocument) {
28 | ops.add(document.save(batch))
29 | }
30 |
31 |
32 | /**
33 | * Adds a document deletion to the batch operation.
34 | */
35 | @Suppress("unused")
36 | public fun delete(document: FirestoreDocument) {
37 | ops.add(document.delete(batch))
38 | }
39 | }
40 |
41 | private class FirestoreBatchWrite {
42 |
43 | private val batch = FIRESTORE.batch()
44 | private val writer = FirestoreBatchWriter(batch)
45 |
46 | fun perform(action: FirestoreBatchWriter.() -> Unit): FirestoreBatchWrite {
47 | action(writer)
48 | return this
49 | }
50 |
51 | fun commit(): Task {
52 | return batch.commit().addOnSuccessListener {
53 | writer.ops.forEach { it.notifySuccess() }
54 | }.addOnFailureListener {
55 | writer.ops.forEach { it.notifyFailure() }
56 | }.onSuccessTask {
57 | Tasks.forResult(Unit)
58 | }
59 | }
60 | }
61 |
62 | internal interface FirestoreBatchOp {
63 | fun notifySuccess()
64 | fun notifyFailure()
65 | }
--------------------------------------------------------------------------------
/demo/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/firestore/build.gradle.kts:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | */
4 |
5 | import com.otaliastudios.tools.publisher.common.License
6 | import com.otaliastudios.tools.publisher.common.Release
7 |
8 | plugins {
9 | id("com.android.library")
10 | id("kotlin-android")
11 | id("com.otaliastudios.tools.publisher")
12 | }
13 |
14 | android {
15 | setCompileSdkVersion(property("compileSdkVersion") as Int)
16 | defaultConfig {
17 | setMinSdkVersion(property("minSdkVersion") as Int)
18 | setTargetSdkVersion(property("targetSdkVersion") as Int)
19 | versionName = property("libVersion") as String
20 | }
21 |
22 | buildFeatures {
23 | dataBinding = true
24 | }
25 |
26 | sourceSets {
27 | getByName("main").java.srcDirs("src/main/kotlin")
28 | getByName("test").java.srcDirs("src/test/kotlin")
29 | }
30 |
31 | compileOptions {
32 | sourceCompatibility = JavaVersion.VERSION_1_8
33 | targetCompatibility = JavaVersion.VERSION_1_8
34 | }
35 |
36 | buildTypes {
37 | getByName("release").consumerProguardFile("proguard-rules.pro")
38 | }
39 |
40 | kotlinOptions {
41 | // Until the explicitApi() works in the Kotlin block...
42 | // https://youtrack.jetbrains.com/issue/KT-37652
43 | freeCompilerArgs += listOf("-Xexplicit-api=strict")
44 | }
45 | }
46 |
47 | dependencies {
48 | api("com.google.firebase:firebase-firestore-ktx:21.6.0")
49 | }
50 |
51 | publisher {
52 | project.artifact = "firestore"
53 | project.description = property("libDescription") as String
54 | project.group = property("libGroup") as String
55 | project.url = property("githubUrl") as String
56 | project.vcsUrl = property("githubGit") as String
57 | project.addLicense(License.APACHE_2_0)
58 | release.setSources(Release.SOURCES_AUTO)
59 | release.setDocs(Release.DOCS_AUTO)
60 | bintray {
61 | auth.user = "BINTRAY_USER"
62 | auth.key = "BINTRAY_KEY"
63 | auth.repo = "BINTRAY_REPO"
64 | }
65 | directory {
66 | directory = "../build/maven"
67 | }
68 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/parcel/api.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore.parcel
2 |
3 | import android.os.Parcel
4 | import com.otaliastudios.firestore.FirestoreLogger
5 | import kotlin.reflect.KClass
6 |
7 | private val PARCELERS = mutableMapOf>()
8 |
9 | /**
10 | * Registers a new [FirestoreParceler].
11 | */
12 | public fun registerParceler(klass: KClass, parceler: FirestoreParceler) {
13 | PARCELERS[klass.java.name] = parceler
14 | }
15 |
16 | /**
17 | * Registers a new [FirestoreParceler].
18 | */
19 | @Suppress("unused")
20 | public inline fun registerParceler(parceler: FirestoreParceler) {
21 | registerParceler(T::class, parceler)
22 | }
23 |
24 | private val log = FirestoreLogger("Parcelers")
25 | private const val NULL = 0
26 | private const val PARCELER = 1
27 | private const val VALUE = 2
28 |
29 | internal fun Parcel.writeValue(value: Any?, tag: String) {
30 | if (value == null) {
31 | log.v { "$tag writeParcel: value is null." }
32 | writeInt(NULL)
33 | return
34 | }
35 | val klass = value::class.java.name
36 | if (PARCELERS.containsKey(klass)) {
37 | log.v { "$tag writeParcel: value $value will be written with parceler for class $klass." }
38 | writeInt(PARCELER)
39 | writeString(klass)
40 | @Suppress("UNCHECKED_CAST")
41 | val parceler = PARCELERS[klass] as FirestoreParceler
42 | parceler.write(value, this, 0)
43 | return
44 | }
45 | try {
46 | log.v { "$tag writeParcel: value $value will be written with writeValue()." }
47 | writeInt(VALUE)
48 | writeValue(value)
49 | } catch (e: Exception) {
50 | log.e(e) { "Could not write value $value. You need to add a FirestoreParceler." }
51 | throw e
52 | }
53 | }
54 |
55 | internal fun Parcel.readValue(loader: ClassLoader, tag: String): Any? {
56 | val what = readInt()
57 | if (what == NULL) {
58 | log.v { "$tag readParcel: value is null." }
59 | return null
60 | }
61 | if (what == PARCELER) {
62 | val klass = readString()!!
63 | log.v { "$tag readParcel: value will be read by parceler $klass." }
64 | @Suppress("UNCHECKED_CAST")
65 | val parceler = PARCELERS[klass] as FirestoreParceler
66 | return parceler.create(this)
67 | }
68 | if (what == VALUE) {
69 | val read = readValue(loader)
70 | log.v { "$tag readParcel: value was read by readValue: $read." }
71 | return read
72 | }
73 | val e = IllegalStateException("$tag Error while reading parcel. Unexpected control int: $what")
74 | log.e(e) { e.message!! }
75 | throw e
76 | }
77 |
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/snapshots.kt:
--------------------------------------------------------------------------------
1 | package com.otaliastudios.firestore
2 |
3 | import com.google.firebase.firestore.DocumentSnapshot
4 | import kotlin.reflect.KClass
5 |
6 | private val log = FirestoreLogger("Snapshots")
7 |
8 | /**
9 | * Converts the given [DocumentSnapshot] to a [FirestoreDocument].
10 | * The [cache] boolean tells whether we should inspect the cache before allocating a new object.
11 | */
12 | @Suppress("unused")
13 | public inline fun DocumentSnapshot.toFirestoreDocument(cache: Boolean = true): T {
14 | return toFirestoreDocument(T::class, cache)
15 | }
16 |
17 | /**
18 | * Converts the given [DocumentSnapshot] to a [FirestoreDocument].
19 | * The [cache] boolean tells whether we should inspect the cache before allocating a new object.
20 | */
21 | @Suppress("UNCHECKED_CAST")
22 | public fun DocumentSnapshot.toFirestoreDocument(type: KClass, cache: Boolean = true): T {
23 | var needsCacheState = false
24 | val result = if (cache) {
25 | val cached = FirestoreCache.get(reference.id, type)
26 | if (cached == null) {
27 | log.i { "Id ${reference.id} asked for cache. Cache miss." }
28 | val new = type.java.newInstance()
29 | new.clearDirt() // Clear dirtyness from init().
30 | FirestoreCache[reference.id] = new
31 | new.cacheState = FirestoreCacheState.FRESH
32 | new
33 | } else {
34 | if (metadata.isFromCache) {
35 | log.i { "Id ${reference.id} asked for cache. Was found. Using CACHED_EQUAL because metadata.isFromCache." }
36 | cached.cacheState = FirestoreCacheState.CACHED_EQUAL
37 | } else {
38 | log.i { "Id ${reference.id} asked for cache. Was found. We'll see if something changed." }
39 | needsCacheState = true
40 | }
41 | cached
42 | }
43 | } else {
44 | log.i { "Id ${reference.id} created with no cache." }
45 | val new = type.java.newInstance()
46 | new.clearDirt() // Clear dirtyness from init().
47 | FirestoreCache[reference.id] = new
48 | new.cacheState = FirestoreCacheState.FRESH
49 | new
50 | }
51 | result.id = reference.id
52 | result.collection = reference.parent.path
53 | val changed = result.mergeValues(data!!, needsCacheState, reference.id)
54 | if (needsCacheState) {
55 | result.cacheState = if (changed) {
56 | log.v { "Id ${reference.id} Setting cache state to CACHED_CHANGED." }
57 | FirestoreCacheState.CACHED_CHANGED
58 | } else {
59 | log.v { "Id ${reference.id} Setting cache state to CACHED_EQUAL." }
60 | FirestoreCacheState.CACHED_EQUAL
61 | }
62 | }
63 | return result
64 | }
65 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/demo/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreDocument.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | */
4 |
5 | package com.otaliastudios.firestore
6 |
7 | import android.os.Bundle
8 | import androidx.annotation.Keep
9 | import com.google.android.gms.tasks.Task
10 | import com.google.android.gms.tasks.Tasks
11 | import com.google.firebase.Timestamp
12 | import com.google.firebase.firestore.*
13 | import com.otaliastudios.firestore.parcel.DocumentReferenceParceler
14 | import com.otaliastudios.firestore.parcel.FieldValueParceler
15 | import com.otaliastudios.firestore.parcel.TimestampParceler
16 | import kotlin.reflect.KClass
17 |
18 | /**
19 | * The base document class.
20 | */
21 | @Keep
22 | public abstract class FirestoreDocument(
23 | @get:Exclude public var collection: String? = null,
24 | @get:Exclude public var id: String? = null,
25 | source: Map? = null
26 | ) : FirestoreMap(source = source) {
27 |
28 | @Suppress("MemberVisibilityCanBePrivate", "RedundantModalityModifier")
29 | @Exclude
30 | public final fun isNew(): Boolean {
31 | return createdAt == null
32 | }
33 |
34 | private fun requireReference(): DocumentReference {
35 | return if (id == null) {
36 | FIRESTORE.collection(collection!!).document()
37 | } else {
38 | FIRESTORE.collection(collection!!).document(id!!)
39 | }
40 | }
41 |
42 | @Exclude
43 | public fun getReference(): DocumentReference {
44 | if (id == null) throw IllegalStateException("Cant return reference for unsaved data.")
45 | return requireReference()
46 | }
47 |
48 | @get:Keep @set:Keep
49 | public var createdAt: Timestamp? by this
50 |
51 | @get:Keep @set:Keep
52 | public var updatedAt: Timestamp? by this
53 |
54 | internal var cacheState: FirestoreCacheState = FirestoreCacheState.FRESH
55 |
56 | @Suppress("unused")
57 | @Exclude
58 | public fun getCacheState(): FirestoreCacheState = cacheState
59 |
60 | @Suppress("unused")
61 | @Exclude
62 | public fun delete(): Task {
63 | @Suppress("UNCHECKED_CAST")
64 | return getReference().delete() as Task
65 | }
66 |
67 | @Exclude
68 | internal fun delete(batch: WriteBatch): FirestoreBatchOp {
69 | batch.delete(getReference())
70 | return object: FirestoreBatchOp {
71 | override fun notifyFailure() {}
72 | override fun notifySuccess() {}
73 | }
74 | }
75 |
76 | @Suppress("unused")
77 | @Exclude
78 | public fun save(): Task {
79 | return when {
80 | isNew() -> create()
81 | else -> update()
82 | }
83 | }
84 |
85 | @Exclude
86 | internal fun save(batch: WriteBatch): FirestoreBatchOp {
87 | return when {
88 | isNew() -> create(batch)
89 | else -> update(batch)
90 | }
91 | }
92 |
93 | private fun update(): Task {
94 | if (isNew()) throw IllegalStateException("Can not update a new object. Please call create().")
95 | val map = mutableMapOf()
96 | flattenValues(map, prefix = "", dirtyOnly = true)
97 | map["updatedAt"] = FieldValue.serverTimestamp()
98 | return getReference().update(map).onSuccessTask {
99 | updatedAt = Timestamp.now()
100 | clearDirt()
101 | @Suppress("UNCHECKED_CAST")
102 | Tasks.forResult(this as T)
103 | }
104 | }
105 |
106 | private fun create(): Task {
107 | if (!isNew()) throw IllegalStateException("Can not create an existing object.")
108 | val reference = requireReference()
109 | // Collect all values. Can't use 'this': we can't be read by Firestore unless we have fields declared.
110 | // Same for FirestoreMap and FirestoreList. Each one need to return a Firestore-readable Map or List or whatever else.
111 | // Can't use the [flatten] API or this won't work with . fields.
112 | val map = collectValues(dirtyOnly = false).toMutableMap()
113 | map["createdAt"] = FieldValue.serverTimestamp()
114 | map["updatedAt"] = FieldValue.serverTimestamp()
115 | // Add to cache NOW, then eventually revert.
116 | // This is because when reference.set() succeeds, any query listener is notified
117 | // before our onSuccessTask() is called. So a new item is created.
118 | FirestoreCache[reference.id] = this
119 | return reference.set(map).addOnFailureListener {
120 | FirestoreCache.remove(reference.id)
121 | }.onSuccessTask {
122 | id = reference.id
123 | createdAt = Timestamp.now()
124 | updatedAt = createdAt
125 | clearDirt()
126 | @Suppress("UNCHECKED_CAST")
127 | Tasks.forResult(this as T)
128 | }
129 | }
130 |
131 | private fun update(batch: WriteBatch): FirestoreBatchOp {
132 | if (isNew()) throw IllegalStateException("Can not update a new object. Please call create().")
133 | val map = mutableMapOf()
134 | flattenValues(map, prefix = "", dirtyOnly = true)
135 | map["updatedAt"] = FieldValue.serverTimestamp()
136 | batch.update(getReference(), map)
137 | return object: FirestoreBatchOp {
138 | override fun notifyFailure() {}
139 | override fun notifySuccess() {
140 | updatedAt = Timestamp.now()
141 | clearDirt()
142 | }
143 | }
144 | }
145 |
146 | private fun create(batch: WriteBatch): FirestoreBatchOp {
147 | if (!isNew()) throw IllegalStateException("Can not create an existing object.")
148 | val reference = requireReference()
149 | val map = collectValues(dirtyOnly = false).toMutableMap()
150 | map["createdAt"] = FieldValue.serverTimestamp()
151 | map["updatedAt"] = FieldValue.serverTimestamp()
152 | batch.set(reference, map)
153 | FirestoreCache[reference.id] = this
154 | return object: FirestoreBatchOp {
155 | override fun notifyFailure() {
156 | FirestoreCache.remove(reference.id)
157 | }
158 |
159 | override fun notifySuccess() {
160 | id = reference.id
161 | createdAt = Timestamp.now()
162 | updatedAt = createdAt
163 | clearDirt()
164 | }
165 | }
166 | }
167 |
168 | @Suppress("unused")
169 | @Exclude
170 | public fun trySave(vararg updates: Pair): Task {
171 | if (isNew()) throw IllegalStateException("Can not trySave a new object. Please call save() first.")
172 | val reference = requireReference()
173 | val values = updates.toMap().toMutableMap()
174 | values["updatedAt"] = FieldValue.serverTimestamp()
175 | if (isNew()) values["createdAt"] = FieldValue.serverTimestamp()
176 | return reference.update(values).continueWith {
177 | if (it.exception != null) {
178 | throw it.exception!!
179 | } else {
180 | id = reference.id
181 | values.forEach { (key, value) ->
182 | set(key, value)
183 | clearDirt(key)
184 | }
185 | updatedAt = Timestamp.now()
186 | createdAt = createdAt ?: updatedAt
187 | clearDirt("updatedAt")
188 | clearDirt("createdAt")
189 | @Suppress("UNCHECKED_CAST")
190 | this@FirestoreDocument as T
191 | }
192 | }
193 | }
194 |
195 | override fun onWriteToBundle(bundle: Bundle) {
196 | super.onWriteToBundle(bundle)
197 | bundle.putString("id", id)
198 | bundle.putString("collection", collection)
199 | }
200 |
201 | override fun onReadFromBundle(bundle: Bundle) {
202 | super.onReadFromBundle(bundle)
203 | id = bundle.getString("id", null)
204 | collection = bundle.getString("collection", null)
205 | }
206 |
207 | override fun equals(other: Any?): Boolean {
208 | if (this === other) return true
209 | return other is FirestoreDocument &&
210 | other.id == this.id &&
211 | other.collection == this.collection &&
212 | super.equals(other)
213 | }
214 |
215 | public companion object {
216 | @Deprecated(message = "Use FirestoreCache directly.", replaceWith = ReplaceWith("FirestoreCache.get"))
217 | public fun getCached(id: String, type: KClass): T?
218 | = FirestoreCache.get(id, type)
219 |
220 | @Suppress("unused")
221 | @Deprecated(message = "Use FirestoreCache directly.", replaceWith = ReplaceWith("FirestoreCache.get"))
222 | public inline fun getCached(id: String): T?
223 | = FirestoreCache[id]
224 | }
225 | }
226 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/natario1/Firestore/actions)
2 | [](https://github.com/natario1/Firestore/releases)
3 | [](https://github.com/natario1/Firestore/issues)
4 |
5 | *Need support, consulting, or have any other business-related question? Feel free to get in touch.*
6 |
7 | *Like the project, make profit from it, or simply want to thank back? Please consider [sponsoring me](https://github.com/sponsors/natario1)!*
8 |
9 | # Firestore
10 |
11 | The lightweight, efficient wrapper for Firestore model data, written in Kotlin, with data-binding and Parcelable support.
12 |
13 | ```groovy
14 | implementation 'com.otaliastudios:firestore:0.7.0'
15 | kapt 'com.otaliastudios:firestore-compiler:0.7.0'
16 | ```
17 |
18 | - Efficient and lightweight
19 | - Compiler to avoid reflection
20 | - Built-in Parcelable implementation
21 | - Built-in Data binding support
22 | - Built-in equals & hashcode
23 | - Written in Kotlin using map / list delegation
24 | - Full replacement for `data class` and `Parcelize`
25 |
26 | # We know about your schema
27 |
28 | The key classes here are `FirestoreDocument` (for documents), `FirestoreMap` and `FirestoreList` (for inner maps and lists).
29 | They use Kotlin delegation so you can declare expected fields using the `by this` syntax:
30 |
31 | ```kotlin
32 | @FirestoreClass
33 | class User : FirestoreDocument() {
34 | var type: Int by this
35 | var imageUrl: String? by this
36 | var messages: Messages by this
37 |
38 | @FirestoreClass
39 | class Messages : FirestoreList()
40 |
41 | @FirestoreClass
42 | class Message : FirestoreMap() {
43 | var from: String by this
44 | var to: String by this
45 | var text: String? by this
46 | }
47 | }
48 | ```
49 |
50 | The compiler will inspect your hierarchy and at runtime, it will know how to instantiate fields
51 | and inner maps or lists, as long as you provide a no arguments constructor for them. This is valid:
52 |
53 | ```kotlin
54 | val user = User()
55 | val message = Message()
56 | user.messages.add(message) // We didn't have to instantiate Messages()
57 | ```
58 |
59 | Fiels are instantiated automatically and **lazily**, when requested.
60 | The map and list implementations parsed by the compiler are also used when retrieving the document
61 | from the network, which makes it much more efficient than using reflection to find setters.
62 |
63 | ```kotlin
64 | val user: User = documentSnapshot.toFirestoreDocument()
65 | val lastMessage = user.messages.last()
66 | ```
67 |
68 | ## Specify default values
69 |
70 | The fields that are marked as not nullable, will be instantiated using their no arguments constructor.
71 | This means that, for example, `Int` defaults to `0`. To specify different defaults, simply use an init block:
72 |
73 | ```kotlin
74 | @FirestoreClass
75 | class User : FirestoreDocument() {
76 | var type: Int by this
77 |
78 | init {
79 | type = UserType.ADMIN
80 | }
81 | }
82 | ```
83 |
84 | # We cache your documents
85 |
86 | Similar to what Firestore-UI does, we keep a static `LruCache` of your documents based on their path.
87 | This means that if you run a query for 50 documents and 20 were already cached, we will reuse their
88 | instance instead of creating new ones.
89 |
90 | ```kotlin
91 | val user1: User = documentSnapshot.toFirestoreDocument()
92 | val user2: User = documentSnapshot.toFirestoreDocument()
93 | assert(user1 === user2)
94 | ```
95 |
96 | Of course, the fields will be updated to reflect the new network data.
97 |
98 | # We keep some basic fields
99 |
100 | Each object will keep (and save to network) some extra fields that are commonly used, plus have
101 | useful functions to inspect their state with respect to the database.
102 |
103 | ```kotlin
104 | val isNew: Boolean = user.isNew() // Whether this object was saved to / comes from network
105 | val createdAt: Timestamp? = user.createdAt // When this object was saved to network for the first time. Null if new
106 | val updatedAt: Timestamp? = user.updatedAt // When this object was saved to network for the last time. Null if new
107 | val reference: DocumentReference = user.getReference() // Throws if new
108 | ```
109 |
110 | We also have built-in reliable implementations for `equals()` and `hashcode()`.
111 |
112 | # We know your dirty values
113 |
114 | When you update some fields, either some declared field (`user.imageUrl = "url"`) or using the backing
115 | map implementation (`user["imageUrl"] = "url"`), the document will remember that this specific field was changed
116 | with respect to the original values.
117 |
118 | Next call to `user.save()` will internally use something like `reference.update(mapOf("imageUrl" to "url"))`,
119 | instead of saving the whole object to the database. This is managed automatically and you don't have to worry about it.
120 |
121 | This even works with inner fields and maps!
122 |
123 | ```kotlin
124 | user.family.father.name = "John"
125 | user.save()
126 |
127 | // This will not send the whole User, not the whole Family and not even the whole Father.
128 | // It will call reference.update("user.family.father.name", "John") as it should.
129 | ```
130 |
131 | # Built-in Data Binding support
132 |
133 | The `FirestoreDocument` and `FirestoreMap` classes extend the `BaseObservable` class from the official data binding lib.
134 | Thanks to the compiler, all declared fields will automatically call `notifyPropertyChanged()` for you,
135 | which hugely reduce the work needed to implement databinding and two-way databinding.
136 |
137 |
138 | In fact, all you have to do is add `@get:Bindable` to your fields:
139 |
140 | ```kotlin
141 | @FirestoreClass
142 | class Message : FirestoreDocument() {
143 | @get:Bindable var text: String by this
144 | @get:Bindable var comment: String by this
145 | }
146 | ```
147 |
148 | You can now use `message.text` and `message.comment` in XML layouts and they will be updated
149 | when the data model changes.
150 |
151 | # We know how to `Parcel`
152 |
153 | Documents, maps and lists implement the `Parcelable` interface.
154 |
155 | ## Local metadata
156 |
157 | If your object holds metadata that should not by saved to network (for instance, fields that are not marked with `by this`),
158 | they can be saved and restored to the `Parcel` overriding the class callbacks:
159 |
160 | ```kotlin
161 | @FirestoreClass
162 | class Message : FirestoreDocument() {
163 | var text: String by this
164 | var comment: String by this
165 |
166 | var hasBeenRead = false
167 |
168 | override fun onWriteToBundle(bundle: Bundle) {
169 | bundle.putBoolean("hasBeenRead", hasBeenRead)
170 | }
171 |
172 | override fun onReadFromBundle(bundle: Bundle) {
173 | hasBeenRead = bundle.getBoolean("hasBeenRead")
174 | }
175 | }
176 | ```
177 |
178 | ## Special types
179 |
180 | We offer built in parcelers for `DocumentReference`, `Timestamp` and `FieldValue` types.
181 | If your types do not implement parcelable directly, either have them implement it or register
182 | a parceler using `FirestoreDocument.registerParceler()`:
183 |
184 | ```kotlin
185 | class App : Application() {
186 |
187 | override fun onCreate() {
188 | registerParceler(GeoPointParceler)
189 | registerParceler(WhateverParceler)
190 | }
191 |
192 | object GeoPointParceler : FirestoreDocument.Parceler() {
193 | // ...
194 | }
195 |
196 | object WhateverParceler : FirestoreDocument.Parceler() {
197 | // ...
198 | }
199 | }
200 | ```
201 |
202 | # Finally, we know how to update
203 |
204 | The document class exposes `delete()`, `save()` and `trySave()` methods. They all return a `Task>` object
205 | from the Google gms library, which you should be used to. This lets you add success and failure callbacks,
206 | as well as chain operations with complex dependencies.
207 |
208 | ## Delete
209 |
210 | ```kotlin
211 | user.delete().addOnSuccessListener {
212 | // User was deleted!
213 | }.addOnFailureListener {
214 | // Something went wrong
215 | }
216 | ```
217 |
218 | The delete operation will throw an exception is the object `isNew()`.
219 |
220 | ## Save
221 |
222 | The `save()` method will check if the object is new or comes from server.
223 |
224 | - For a new object, internally this will call `reference.set()` to create your object
225 | - For a backend object, this will call `reference.update()`. As stated above in the dirty fields chapter,
226 | we only update exactly what was changed programmatically.
227 |
228 | ```kotlin
229 | user.family.father = John()
230 | user.type = User.TYPE_CHILD
231 | user.save().addOnSuccessListener {
232 | // User was saved!
233 | }.addOnFailureListener {
234 | // Something went wrong
235 | }
236 | ```
237 |
238 | ## Try Save
239 |
240 | The `trySave()` method follows an opposite procedure.
241 | It will first try to update the fields you specify to network, and if the call succeeds, it will update
242 | the data document too.
243 |
244 | ```kotlin
245 | user.family.father = null
246 | user.trySave(
247 | "family.father" to John(),
248 | "type" to User.TYPE_CHILD
249 | ).addOnSuccessListener {
250 | // User was saved!
251 | assert(user.family.father is John)
252 | }.addOnFailureListener {
253 | // Something went wrong.
254 | assert(user.family.father == null)
255 | }
256 | ```
257 |
258 |
259 |
--------------------------------------------------------------------------------
/compiler/src/main/kotlin/com/otaliastudios/firestore/compiler/Processor.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | */
4 |
5 | package com.otaliastudios.firestore.compiler
6 |
7 | import com.otaliastudios.firestore.FirestoreClass
8 | import com.otaliastudios.firestore.FirestoreMetadata
9 | import com.otaliastudios.firestore.compiler.Types.BINDABLE
10 | import com.otaliastudios.firestore.compiler.Types.FIRESTORE_CLASS
11 | import com.otaliastudios.firestore.compiler.Types.FIRESTORE_DOCUMENT
12 | import com.otaliastudios.firestore.compiler.Types.FIRESTORE_EXCLUDE
13 | import com.otaliastudios.firestore.compiler.Types.FIRESTORE_LIST
14 | import com.otaliastudios.firestore.compiler.Types.FIRESTORE_MAP
15 | import com.otaliastudios.firestore.compiler.Types.KOTLIN_BOOLEAN
16 | import com.otaliastudios.firestore.compiler.Types.KOTLIN_DOUBLE
17 | import com.otaliastudios.firestore.compiler.Types.KOTLIN_FLOAT
18 | import com.otaliastudios.firestore.compiler.Types.KOTLIN_INT
19 | import com.otaliastudios.firestore.compiler.Types.KOTLIN_STRING
20 | import com.otaliastudios.firestore.compiler.Types.NULLABLE
21 | import com.otaliastudios.firestore.compiler.Types.SUPPRESS
22 | import com.otaliastudios.firestore.compiler.Types.TIMESTAMP
23 | import com.squareup.kotlinpoet.*
24 | import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview
25 | import com.squareup.kotlinpoet.metadata.specs.toTypeSpec
26 | import javax.annotation.processing.*
27 | import javax.lang.model.SourceVersion
28 | import javax.lang.model.element.*
29 | import javax.lang.model.type.DeclaredType
30 | import javax.tools.Diagnostic
31 |
32 | @KotlinPoetMetadataPreview
33 | @SupportedAnnotationTypes(FIRESTORE_CLASS)
34 | @SupportedSourceVersion(SourceVersion.RELEASE_8)
35 | class Processor : AbstractProcessor() {
36 |
37 | override fun process(set: Set, environment: RoundEnvironment): Boolean {
38 | collect(environment).forEach {
39 | val properties = inspect(it)
40 | write(it, properties)
41 | }
42 | return true
43 | }
44 |
45 | private fun collect(environment: RoundEnvironment): List {
46 | return environment
47 | .getElementsAnnotatedWith(FirestoreClass::class.java)
48 | .mapNotNull {
49 | if (it.kind != ElementKind.CLASS) {
50 | logError("FirestoreClass should only annotate classes.", it)
51 | null
52 | } else {
53 | it as TypeElement
54 | }
55 | }
56 | }
57 |
58 | private fun inspect(element: TypeElement): List {
59 | logInfo("Inspecting ${element.simpleName}")
60 |
61 | // Init properties. If document, add extra timestamps.
62 | val properties = mutableListOf()
63 | if (element.isFirestoreDocument) {
64 | logInfo("It's a document! Adding timestamp fields.")
65 | properties.add(Property("createdAt", TIMESTAMP, isNullable = true))
66 | properties.add(Property("updatedAt", TIMESTAMP, isNullable = true))
67 | }
68 |
69 | // Inspect properties.
70 | val spec = element.toTypeSpec()
71 | spec.propertySpecs.filter {
72 | // The properties we want to support are those delegated with by this(),
73 | // the rest is probably class state that should not be in firestore.
74 | it.delegated
75 | true
76 | }.filter {
77 | logInfo("INSPECTING! Property:${it.name} Initializer:${it.initializer} Getter:${it.getter} ReceiverType:${it.receiverType}")
78 | false
79 | }.forEach { property ->
80 | logInfo("Inspecting property ${property.name} of type ${property.type}.")
81 | val annotationsSpecs = property.annotations + (property.getter?.annotations ?: listOf())
82 | val annotations = annotationsSpecs.map { it.className.canonicalName }
83 | if (annotations.contains(FIRESTORE_EXCLUDE)) return@forEach
84 | val isNullable = property.type.isNullable || annotations.contains(NULLABLE)
85 | val isBindable = annotations.contains(BINDABLE)
86 | properties.add(Property(
87 | name = property.name,
88 | type = property.type.copy(nullable = false, annotations = listOf()).toString(),
89 | isBindable = isBindable,
90 | isNullable = isNullable)
91 | )
92 | }
93 | return properties
94 | }
95 |
96 | private fun write(element: TypeElement, properties: List) {
97 | // Collect file info
98 | val packageName = element.packageName
99 | val innerType = element.firestoreAncestor.typeArguments.first().toString()
100 | val inputClass = element.simpleName.toString()
101 | val outputClass = "$inputClass${FirestoreMetadata.SUFFIX}"
102 | logInfo("Writing... package=$packageName, innerType=$innerType")
103 |
104 | // Create function
105 | val createFunction = FunSpec.builder("create").apply {
106 | addModifiers(KModifier.OVERRIDE, KModifier.PUBLIC)
107 | addParameter("key", String::class)
108 | val type = TypeVariableName.invoke("T")
109 | addTypeVariable(type)
110 | returns(type.copy(nullable = true))
111 | val codeBuilder = CodeBlock.builder().beginControlFlow("return when (key)")
112 | properties.forEach {
113 | codeBuilder.addStatement("\"${it.name}\" -> ${getNewInstanceStatement(it.type)}")
114 | }
115 | codeBuilder.addStatement("else -> null")
116 | codeBuilder.endControlFlow()
117 | addCode(codeBuilder.build())
118 | }
119 |
120 | // Create inner type function
121 | val createInnerTypeFunction = FunSpec.builder("createInnerType").apply {
122 | addModifiers(KModifier.OVERRIDE, KModifier.PUBLIC)
123 | val type = TypeVariableName.invoke("T")
124 | addTypeVariable(type)
125 | returns(type.copy(nullable = true))
126 | addStatement("return ${getNewInstanceStatement(innerType)}")
127 | }
128 |
129 | // Nullable function
130 | val isNullableFunction = FunSpec.builder("isNullable").apply {
131 | addModifiers(KModifier.OVERRIDE, KModifier.PUBLIC)
132 | addParameter("key", String::class)
133 | returns(Boolean::class)
134 |
135 | val codeBuilder = CodeBlock.builder().beginControlFlow("return when (key)")
136 | properties.forEach {
137 | codeBuilder.addStatement("\"${it.name}\" -> ${it.isNullable}")
138 | }
139 | codeBuilder.addStatement("else -> false")
140 | codeBuilder.endControlFlow()
141 | addCode(codeBuilder.build())
142 | }
143 |
144 | // Get bindable resource function
145 | val getBindableResourceFunction = FunSpec.builder("getBindableResource").apply {
146 | addModifiers(KModifier.OVERRIDE, KModifier.PUBLIC)
147 | addParameter("key", String::class)
148 | returns(Int::class.asTypeName().copy(nullable = true))
149 | val codeBuilder = CodeBlock.builder().beginControlFlow("return when (key)")
150 | properties.filter { it.isBindable }.forEach {
151 | codeBuilder.addStatement("\"${it.name}\" -> ${Types.DATABINDING_BR}.${it.name}")
152 | }
153 | codeBuilder.addStatement("else -> null")
154 | codeBuilder.endControlFlow()
155 | addCode(codeBuilder.build())
156 | }
157 |
158 | // Merge them together
159 | val classBuilder = TypeSpec.classBuilder(outputClass).apply {
160 | addSuperinterface(FirestoreMetadata::class)
161 | addFunction(createFunction.build())
162 | addFunction(isNullableFunction.build())
163 | addFunction(getBindableResourceFunction.build())
164 | addFunction(createInnerTypeFunction.build())
165 | @Suppress("UNCHECKED_CAST")
166 | val suppress = Class.forName(SUPPRESS) as Class
167 | addAnnotation(AnnotationSpec.builder(suppress)
168 | .addMember("\"UNCHECKED_CAST\"")
169 | .addMember("\"UNUSED_EXPRESSION\"").build())
170 | }
171 |
172 | // Write file
173 | // app/build/generated/source/kapt/debug/my/package/Class_Metadata.kt
174 | FileSpec.builder(packageName, outputClass)
175 | .addComment("Generated file.")
176 | .addType(classBuilder.build())
177 | .build()
178 | .writeTo(processingEnv.filer)
179 | }
180 |
181 | private val Element.packageName: String
182 | get() {
183 | var element = this
184 | while (element.kind != ElementKind.PACKAGE) {
185 | element = element.enclosingElement
186 | }
187 | return (element as PackageElement).toString()
188 | }
189 |
190 | private val TypeElement.firestoreAncestor: DeclaredType
191 | get() {
192 | var element = this
193 | var declaredType: DeclaredType?
194 | while (true) {
195 | declaredType = element.superclass as DeclaredType
196 | element = declaredType.asElement() as TypeElement
197 | val name = element.qualifiedName.toString()
198 | if (name == FIRESTORE_MAP || name == FIRESTORE_LIST) {
199 | break
200 | }
201 | }
202 | logInfo("Ancestor final type: ${element.simpleName}")
203 | return declaredType!!
204 | }
205 |
206 | private val TypeElement.isFirestoreDocument: Boolean
207 | get() {
208 | var isDocument: Boolean
209 | var element = this
210 | while (true) {
211 | val name = element.qualifiedName.toString()
212 | isDocument = name == FIRESTORE_DOCUMENT
213 | if (isDocument || name == FIRESTORE_LIST || name == FIRESTORE_MAP) {
214 | // TODO also break if Any
215 | break
216 | }
217 | element = (element.superclass as DeclaredType).asElement() as TypeElement
218 | }
219 | return isDocument
220 | }
221 |
222 | private fun getNewInstanceStatement(type: String): String {
223 | return when (type) {
224 | KOTLIN_INT -> "0 as T"
225 | KOTLIN_FLOAT -> "0F as T"
226 | KOTLIN_DOUBLE -> "0.0 as T"
227 | KOTLIN_STRING -> "\"\" as T"
228 | KOTLIN_BOOLEAN -> "false as T"
229 | TIMESTAMP -> "$type(0L, 0) as T"
230 | else -> return "$type::class.java.getConstructor().newInstance() as T"
231 | }
232 | }
233 |
234 | @Suppress("SameParameterValue")
235 | private fun logError(message: String, element: Element? = null) {
236 | processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, message, element)
237 | }
238 |
239 | private fun logInfo(message: String) {
240 | processingEnv.messager.printMessage(Diagnostic.Kind.NOTE, message + "\n ")
241 | }
242 | }
243 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreList.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | */
4 |
5 | package com.otaliastudios.firestore
6 |
7 | import android.os.Bundle
8 | import android.os.Parcel
9 | import android.os.Parcelable
10 | import androidx.annotation.Keep
11 | import com.google.firebase.firestore.Exclude
12 | import com.otaliastudios.firestore.parcel.readValue
13 | import com.otaliastudios.firestore.parcel.writeValue
14 |
15 | /**
16 | * Creates a [FirestoreList] for the given values.
17 | */
18 | @Suppress("unused")
19 | public fun firestoreListOf(vararg values: T): FirestoreList {
20 | return FirestoreList(values.asList())
21 | }
22 |
23 | /**
24 | * A [FirestoreList] can be used to represent firestore lists.
25 | * Implements list methods by delegates to a mutable list under the hood.
26 | *
27 | * Whenever an item is inserted, removed, or when a inner Map/List is changed,
28 | * this list will be marked as dirty.
29 | */
30 | @Keep
31 | public open class FirestoreList @JvmOverloads constructor(
32 | source: List? = null
33 | ) : Iterable, Parcelable {
34 |
35 | private val log = FirestoreLogger("FirestoreList")
36 | private val data: MutableList = mutableListOf()
37 |
38 | @get:Exclude
39 | public val size: Int get() = data.size
40 |
41 | init {
42 | if (source != null) {
43 | mergeValues(source, false, "")
44 | }
45 | }
46 |
47 | override fun iterator(): Iterator {
48 | return data.iterator()
49 | }
50 |
51 | private var isDirty = false
52 |
53 | @Suppress("UNCHECKED_CAST")
54 | internal fun flattenValues(map: MutableMap, prefix: String, dirtyOnly: Boolean) {
55 | if (dirtyOnly && !isDirty()) return
56 | val list = mutableListOf()
57 | forEach {
58 | if (it is FirestoreMap<*>) {
59 | val cleanMap = mutableMapOf()
60 | it.flattenValues(cleanMap, "", dirtyOnly)
61 | list.add(cleanMap as T)
62 | } else if (it is FirestoreList<*>) {
63 | val cleanMap = mutableMapOf()
64 | it.flattenValues(cleanMap, "", dirtyOnly)
65 | list.add(cleanMap as T)
66 | } else {
67 | list.add(it)
68 | }
69 | }
70 | map[prefix] = list
71 | }
72 |
73 | @Suppress("UNCHECKED_CAST")
74 | internal fun collectValues(dirtyOnly: Boolean): List {
75 | if (dirtyOnly && !isDirty()) return listOf()
76 | val list = mutableListOf()
77 | forEach {
78 | if (it is FirestoreMap<*>) {
79 | list.add(it.collectValues(dirtyOnly) as T)
80 | } else if (it is FirestoreList<*>) {
81 | list.add(it.collectValues(dirtyOnly) as T)
82 | } else {
83 | list.add(it)
84 | }
85 | }
86 | return list
87 | }
88 |
89 | internal fun isDirty(): Boolean {
90 | if (isDirty) return true
91 | for (it in this) {
92 | if (it is FirestoreList<*> && it.isDirty()) return true
93 | if (it is FirestoreMap<*> && it.isDirty()) return true
94 | }
95 | return false
96 | }
97 |
98 | internal fun clearDirt() {
99 | isDirty = false
100 | for (it in this) {
101 | if (it is FirestoreList<*>) it.clearDirt()
102 | if (it is FirestoreMap<*>) it.clearDirt()
103 | }
104 | }
105 |
106 | private fun createFirestoreMap(): FirestoreMap {
107 | val map = onCreateFirestoreMap()
108 | map.clearDirt()
109 | return map
110 | }
111 |
112 | private fun createFirestoreList(): FirestoreList {
113 | val list = onCreateFirestoreList()
114 | list.clearDirt()
115 | return list
116 | }
117 |
118 | protected open fun onCreateFirestoreMap(): FirestoreMap {
119 | val metadata = this::class.metadata
120 | return metadata?.createInnerType>() ?: FirestoreMap()
121 | }
122 |
123 | protected open fun onCreateFirestoreList(): FirestoreList {
124 | val metadata = this::class.metadata
125 | return metadata?.createInnerType>() ?: FirestoreList()
126 | }
127 |
128 | @Suppress("UNCHECKED_CAST")
129 | internal fun mergeValues(values: List, checkChanges: Boolean, tag: String): Boolean {
130 | var changed = size != values.size
131 | val copy = if (checkChanges) data.toList() else listOf()
132 | data.clear()
133 | for (value in values) {
134 | if (value is Map<*, *> && value.keys.all { it is String }) {
135 | val child = createFirestoreMap() as T
136 | data.add(child)
137 | child as FirestoreMap
138 | value as Map
139 | val childChanged = child.mergeValues(value, checkChanges && !changed, tag)
140 | changed = changed || childChanged
141 | } else if (value is List<*>) {
142 | val child = createFirestoreList() as T
143 | data.add(child)
144 | child as FirestoreList
145 | value as List
146 | val childChanged = child.mergeValues(value, checkChanges && !changed, tag)
147 | changed = changed || childChanged
148 | } else {
149 | if (checkChanges && !changed) {
150 | val index = data.size
151 | val itemChanged = index > copy.lastIndex || value != copy[index]
152 | changed = changed || itemChanged
153 | }
154 | data.add(value)
155 | }
156 | }
157 | return checkChanges && changed
158 | }
159 |
160 | override fun equals(other: Any?): Boolean {
161 | if (this === other) return true
162 | return other is FirestoreList<*> &&
163 | other.data.size == data.size &&
164 | other.data.containsAll(data) &&
165 | other.isDirty == isDirty
166 | }
167 |
168 | override fun hashCode(): Int {
169 | var result = data.hashCode()
170 | result = 31 * result + isDirty.hashCode()
171 | return result
172 | }
173 |
174 | // Parcelable stuff.
175 |
176 | override fun describeContents(): Int = 0
177 |
178 | override fun writeToParcel(parcel: Parcel, flags: Int) {
179 | val hashcode = hashCode()
180 | parcel.writeInt(hashcode)
181 |
182 | // Write class name
183 | log.i { "List $hashcode: writing class ${this::class.java.name}" }
184 | parcel.writeString(this::class.java.name)
185 |
186 | // Write size and dirtiness
187 | log.v { "List $hashcode: writing dirty $isDirty and size $size" }
188 | parcel.writeInt(if (isDirty) 1 else 0)
189 | parcel.writeInt(size)
190 |
191 | // Write actual data
192 | for (value in data) {
193 | log.v { "List $hashcode: writing value $value" }
194 | parcel.writeValue(value, hashcode.toString())
195 | }
196 |
197 | // Extra bundle
198 | val bundle = Bundle()
199 | onWriteToBundle(bundle)
200 | log.v { "List $hashcode: writing extra bundle. Size is ${bundle.size()}" }
201 | parcel.writeBundle(bundle)
202 | }
203 |
204 | public companion object {
205 |
206 | private val LOG = FirestoreLogger("FirestoreList")
207 |
208 | @Suppress("unused")
209 | @JvmField
210 | public val CREATOR: Parcelable.Creator> = object : Parcelable.ClassLoaderCreator> {
211 |
212 | override fun createFromParcel(source: Parcel): FirestoreList {
213 | // This should never be called by the framework.
214 | LOG.e { "List: received call to createFromParcel without classLoader." }
215 | return createFromParcel(source, FirestoreList::class.java.classLoader!!)
216 | }
217 |
218 | override fun createFromParcel(parcel: Parcel, loader: ClassLoader): FirestoreList {
219 | val hashcode = parcel.readInt()
220 | // Read class name
221 | val klass = Class.forName(parcel.readString()!!)
222 | LOG.i { "List $hashcode: read class ${klass.simpleName}" }
223 | @Suppress("UNCHECKED_CAST")
224 | val dataList = klass.newInstance() as FirestoreList
225 |
226 | // Read dirtyness and size
227 | dataList.isDirty = parcel.readInt() == 1
228 | val count = parcel.readInt()
229 | LOG.v { "List $hashcode: read dirtyness ${dataList.isDirty} and size $count" }
230 |
231 | // Read actual data
232 | repeat(count) {
233 | LOG.v { "List $hashcode: reading value..." }
234 | dataList.data.add(parcel.readValue(loader, hashcode.toString())!!)
235 | }
236 |
237 | // Extra bundle
238 | LOG.v { "List $hashcode: reading extra bundle." }
239 | val bundle = parcel.readBundle(loader)!!
240 | LOG.v { "List $hashcode: read extra bundle, size ${bundle.size()}" }
241 | dataList.onReadFromBundle(bundle)
242 | return dataList
243 | }
244 |
245 | override fun newArray(size: Int): Array?> {
246 | return Array(size) { null }
247 | }
248 | }
249 | }
250 |
251 | protected open fun onWriteToBundle(bundle: Bundle) {}
252 |
253 | protected open fun onReadFromBundle(bundle: Bundle) {}
254 |
255 | // Dirtyness stuff.
256 |
257 | public fun add(element: T): Boolean {
258 | data.add(element)
259 | isDirty = true
260 | return true
261 | }
262 |
263 | public fun add(index: Int, element: T) {
264 | data.add(index, element)
265 | isDirty = true
266 | }
267 |
268 | public fun remove(element: T): Boolean {
269 | val result = data.remove(element)
270 | isDirty = true
271 | return result
272 | }
273 |
274 | public operator fun set(index: Int, element: T): T {
275 | val value = data.set(index, element)
276 | isDirty = true
277 | return value
278 | }
279 |
280 | // ObservableArrayList stuff.
281 | /*
282 | private var registry: ListChangeRegistry = ListChangeRegistry()
283 |
284 | override fun addOnListChangedCallback(callback: ObservableList.OnListChangedCallback>?) {
285 | registry.add(callback)
286 | }
287 |
288 | override fun removeOnListChangedCallback(callback: ObservableList.OnListChangedCallback>?) {
289 | registry.remove(callback)
290 | }
291 |
292 | override fun clear() {
293 | val oldSize = size
294 | data.clear()
295 | if (oldSize != 0) {
296 | registry.notifyRemoved(this, 0, oldSize)
297 | isDirty = true
298 | }
299 | }
300 |
301 | override fun add(element: T): Boolean {
302 | data.add(element)
303 | registry.notifyInserted(this, size - 1, 1)
304 | isDirty = true
305 | return true
306 | }
307 |
308 | override fun add(index: Int, element: T) {
309 | data.add(index, element)
310 | registry.notifyInserted(this, index, 1)
311 | isDirty = true
312 | }
313 |
314 | override fun addAll(elements: Collection): Boolean {
315 | val oldSize = size
316 | val added = data.addAll(elements)
317 | if (added) {
318 | registry.notifyInserted(this, oldSize, size - oldSize)
319 | isDirty = true
320 | }
321 | return added
322 | }
323 |
324 | override fun addAll(index: Int, elements: Collection): Boolean {
325 | val added = data.addAll(index, elements)
326 | if (added) {
327 | registry.notifyInserted(this, index, elements.size)
328 | isDirty = true
329 | }
330 | return added
331 | }
332 |
333 | override fun remove(element: T): Boolean {
334 | val index = indexOf(element)
335 | if (index >= 0) {
336 | removeAt(index)
337 | return true
338 | } else {
339 | return false
340 | }
341 | }
342 |
343 | override fun removeAt(index: Int): T {
344 | val value = data.removeAt(index)
345 | registry.notifyRemoved(this, index, 1)
346 | isDirty = true
347 | return value
348 | }
349 |
350 | override fun set(index: Int, element: T): T {
351 | val value = data.set(index, element)
352 | registry.notifyChanged(this, index, 1)
353 | isDirty = true
354 | return value
355 | } */
356 | }
--------------------------------------------------------------------------------
/firestore/src/main/kotlin/com/otaliastudios/firestore/FirestoreMap.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2018 Otalia Studios. Author: Mattia Iavarone.
3 | */
4 |
5 | package com.otaliastudios.firestore
6 |
7 | import android.os.Bundle
8 | import android.os.Parcel
9 | import android.os.Parcelable
10 | import androidx.annotation.Keep
11 | import androidx.databinding.BaseObservable
12 | import com.google.firebase.firestore.Exclude
13 | import com.otaliastudios.firestore.parcel.readValue
14 | import com.otaliastudios.firestore.parcel.writeValue
15 | import kotlin.properties.ReadWriteProperty
16 | import kotlin.reflect.KProperty
17 |
18 | /**
19 | * Creates a [FirestoreMap] for the given key-value pairs.
20 | */
21 | @Suppress("unused")
22 | public fun firestoreMapOf(vararg pairs: Pair): FirestoreMap {
23 | return FirestoreMap(pairs.toMap())
24 | }
25 |
26 | /**
27 | * A [FirestoreMap] can be used to represent firestore JSON-like structures.
28 | * Implements map methods by delegating to a mutable map under the hood.
29 | */
30 | @Keep
31 | public open class FirestoreMap(source: Map? = null) : BaseObservable(), Parcelable {
32 |
33 | private val log = FirestoreLogger("FirestoreMap")
34 | private val data: MutableMap = mutableMapOf()
35 | private val dirty: MutableSet = mutableSetOf()
36 |
37 | @get:Exclude
38 | public val keys: Set get() = data.keys
39 |
40 | @get:Exclude
41 | public val size: Int get() = data.size
42 |
43 | init {
44 | if (source != null) {
45 | mergeValues(source, false, "Initialization")
46 | }
47 | }
48 |
49 | internal fun isDirty(): Boolean {
50 | return keys.any { isDirty(it) }
51 | }
52 |
53 | internal fun isDirty(key: String): Boolean {
54 | if (dirty.contains(key)) return true
55 | if (key.contains('.')) {
56 | val first = key.split('.')[0]
57 | val second = key.removePrefix("$first.")
58 | val data = get(first)
59 | return when (data) {
60 | null -> false
61 | is FirestoreMap<*> -> data.isDirty(second)
62 | else -> throw IllegalArgumentException("Accessing with dot notation, but it is not a FirestoreMap.")
63 | }
64 | }
65 | val what = get(key)
66 | if (what is FirestoreList<*>) {
67 | return what.isDirty()
68 | } else {
69 | return false
70 | }
71 | }
72 |
73 | internal fun clearDirt() {
74 | for (key in keys) {
75 | clearDirt(key)
76 | }
77 | }
78 |
79 | internal fun clearDirt(key: String) {
80 | if (dirty.contains(key)) {
81 | dirty.remove(key)
82 | } else {
83 | val value = get(key)
84 | if (value is FirestoreMap<*>) {
85 | value.clearDirt()
86 | } else if (value is FirestoreList<*>) {
87 | value.clearDirt()
88 | }
89 | }
90 | }
91 |
92 |
93 | private fun createFirestoreMap(key: String): FirestoreMap {
94 | val map = onCreateFirestoreMap(key)
95 | map.clearDirt()
96 | return map
97 | }
98 |
99 | private fun createFirestoreList(key: String): FirestoreList {
100 | val list = onCreateFirestoreList(key)
101 | list.clearDirt()
102 | return list
103 | }
104 |
105 | protected open fun onCreateFirestoreMap(key: String): FirestoreMap {
106 | val metadata = this::class.metadata
107 | var candidate = metadata?.create>(key)
108 | candidate = candidate ?: metadata?.createInnerType()
109 | candidate = candidate ?: FirestoreMap()
110 | return candidate
111 | }
112 |
113 | protected open fun onCreateFirestoreList(key: String): FirestoreList {
114 | val metadata = this::class.metadata
115 | var candidate = metadata?.create>(key)
116 | candidate = candidate ?: metadata?.createInnerType()
117 | candidate = candidate ?: FirestoreList()
118 | return candidate
119 | }
120 |
121 | public operator fun set(key: String, value: T) {
122 | val result = onSet(key, value)
123 | /* if (result == null) {
124 | // Do nothing.
125 | } else */if (key.contains('.')) {
126 | val first = key.split('.')[0]
127 | val second = key.removePrefix("$first.")
128 | val data = getOrCreateFirestoreMap(first)
129 | data[second] = result
130 | } else {
131 | data[key] = result
132 | dirty.add(key)
133 | val resource = this::class.metadata?.getBindableResource(key)
134 | if (resource != null) notifyPropertyChanged(resource)
135 | }
136 | }
137 |
138 | internal open fun onSet(key: String, value: T): T = value
139 |
140 | public operator fun get(key: String): T? {
141 | return if (key.contains('.')) {
142 | val first = key.split('.')[0]
143 | val second = key.removePrefix("$first.")
144 | val data = getOrCreateFirestoreMap(first)
145 | data[second]
146 | } else {
147 | data[key]
148 | }
149 | }
150 |
151 | @Suppress("UNCHECKED_CAST")
152 | private fun getOrCreateFirestoreMap(key: String): FirestoreMap {
153 | val data = get(key)
154 | if (data == null) {
155 | val map = createFirestoreMap(key)
156 | set(key, map as T)
157 | return map
158 | } else if (data is FirestoreMap<*>) {
159 | return data as FirestoreMap
160 | } else {
161 | throw RuntimeException("Trying to access map with dot notation, " +
162 | "but it is not a FirestoreMap. key: $key, value: $data")
163 | }
164 | }
165 |
166 | /**
167 | * Returns a map that collects all the values in this [FirestoreMap].
168 | * Filters only dirty values if needed, and flattens [FirestoreMap]s and [FirestoreList]s
169 | * into real [Map] and [List] values.
170 | */
171 | @Suppress("UNCHECKED_CAST")
172 | internal fun collectValues(dirtyOnly: Boolean): Map {
173 | val map = mutableMapOf()
174 | for (key in keys) {
175 | val child = get(key)
176 | if (child is FirestoreMap<*>) {
177 | val childMap = child.collectValues(dirtyOnly)
178 | if (childMap.isNotEmpty()) map[key] = childMap as T?
179 | } else if (child is FirestoreList<*>) {
180 | val childList = child.collectValues(dirtyOnly)
181 | if (childList.isNotEmpty()) map[key] = childList as T?
182 | } else if (!dirtyOnly || dirty.contains(key)) {
183 | map[key] = child
184 | }
185 | }
186 | return map
187 | }
188 |
189 | /**
190 | * Flattens the values inside this map, with the given prefix for our own keys.
191 | * The final map will have all fields (only dirty if specified) at the base level.
192 | *
193 | *
194 | */
195 | internal fun flattenValues(map: MutableMap, prefix: String, dirtyOnly: Boolean) {
196 | for (key in keys) {
197 | val child = get(key)
198 | val childPrefix = "$prefix.$key".trim('.')
199 | if (child is FirestoreMap<*>) {
200 | child.flattenValues(map, childPrefix, dirtyOnly)
201 | } else if (child is FirestoreList<*>) {
202 | child.flattenValues(map, childPrefix, dirtyOnly)
203 | } else if (!dirtyOnly || dirty.contains(key)) {
204 | map[childPrefix] = child
205 | }
206 | }
207 | }
208 |
209 | @Suppress("UNCHECKED_CAST")
210 | internal fun mergeValues(values: Map, checkChanges: Boolean, tag: String): Boolean {
211 | var changed = false
212 | for ((key, value) in values) {
213 | log.v { "$tag mergeValues: key $key with value $value, dirty: ${isDirty(key)}" }
214 | if (isDirty(key)) continue
215 | if (value is Map<*, *> && value.keys.all { it is String }) {
216 | val child = get(key) ?: createFirestoreMap(key) as T // T
217 | data[key] = child
218 | child as FirestoreMap
219 | value as Map
220 | val childChanged = child.mergeValues(value, checkChanges && !changed, tag)
221 | changed = changed || childChanged
222 | } else if (value is List<*>) {
223 | val child = get(key) ?: createFirestoreList(key) as T // T
224 | data[key] = child
225 | child as FirestoreList
226 | value as List
227 | val childChanged = child.mergeValues(value, checkChanges && !changed, tag)
228 | changed = changed || childChanged
229 | } else {
230 | if (checkChanges && !changed) {
231 | log.v { "$tag mergeValues: key $key comparing with value ${data[key]}" }
232 | changed = changed || value != data[key]
233 | }
234 | data[key] = value
235 | }
236 | }
237 | return changed
238 | }
239 |
240 | override fun equals(other: Any?): Boolean {
241 | if (this === other) return true
242 | return other is FirestoreMap<*> &&
243 | other.data.size == data.size &&
244 | other.data.all { it.value == data[it.key] } &&
245 | other.dirty.size == dirty.size &&
246 | other.dirty.containsAll(dirty)
247 | // TODO it's better to collect the dirty keys, though it makes everything slow.
248 | }
249 |
250 | override fun hashCode(): Int {
251 | var result = data.hashCode()
252 | result = 31 * result + dirty.hashCode()
253 | return result
254 | }
255 |
256 | override fun describeContents(): Int = 0
257 |
258 | override fun writeToParcel(parcel: Parcel, flags: Int) {
259 | val hashcode = hashCode()
260 | parcel.writeInt(hashcode)
261 |
262 | // Write class name
263 | log.i { "Map $hashcode: writing class ${this::class.java.name}" }
264 | parcel.writeString(this::class.java.name)
265 |
266 | // Write dirty data
267 | log.v { "Map $hashcode: writing dirty count ${dirty.size} and dirty keys ${dirty.toTypedArray().joinToString()} ${dirty.toTypedArray().size}." }
268 | parcel.writeInt(dirty.size)
269 | parcel.writeStringArray(dirty.toTypedArray())
270 |
271 | log.v { "Map $hashcode: writing data size. $size" }
272 | parcel.writeInt(size)
273 | for ((key, value) in data) {
274 | parcel.writeString(key)
275 | log.v { "Map $hashcode: writing value for key $key..." }
276 | parcel.writeValue(value, hashcode.toString())
277 | }
278 |
279 | val bundle = Bundle()
280 | onWriteToBundle(bundle)
281 | log.v { "Map $hashcode: writing extra bundle. Size is ${bundle.size()}" }
282 | parcel.writeBundle(bundle)
283 | }
284 |
285 | public companion object {
286 |
287 | private val LOG = FirestoreLogger("FirestoreMap")
288 |
289 | @Suppress("unused")
290 | @JvmField
291 | public val CREATOR: Parcelable.Creator> = object : Parcelable.ClassLoaderCreator> {
292 |
293 | override fun createFromParcel(source: Parcel): FirestoreMap {
294 | // This should never be called by the framework.
295 | LOG.e { "Map: received call to createFromParcel without classLoader." }
296 | return createFromParcel(source, FirestoreMap::class.java.classLoader!!)
297 | }
298 |
299 | @Suppress("UNCHECKED_CAST")
300 | override fun createFromParcel(parcel: Parcel, loader: ClassLoader): FirestoreMap {
301 | val hashcode = parcel.readInt()
302 |
303 | // Read class and create the map object.
304 | val klass = Class.forName(parcel.readString()!!)
305 | LOG.i { "Map $hashcode: read class ${klass.simpleName}" }
306 | val firestoreMap = klass.newInstance() as FirestoreMap
307 |
308 | // Read dirty data
309 | val dirty = Array(parcel.readInt()) { "" }
310 | parcel.readStringArray(dirty)
311 | LOG.v { "Map $hashcode: read dirty count ${dirty.size} and array ${dirty.joinToString()}" }
312 |
313 | // Read actual data
314 | val count = parcel.readInt()
315 | LOG.v { "Map $hashcode: read data size $count" }
316 |
317 | val values = HashMap(count)
318 | repeat(count) {
319 | val key = parcel.readString()!!
320 | LOG.v { "Map $hashcode: reading value for key $key..." }
321 | values[key] = parcel.readValue(loader, hashcode.toString())
322 | }
323 |
324 | // Set both
325 | firestoreMap.dirty.clear()
326 | firestoreMap.dirty.addAll(dirty)
327 | firestoreMap.data.clear()
328 | firestoreMap.data.putAll(values)
329 |
330 | // Read the extra bundle
331 | LOG.v { "Map $hashcode: reading extra bundle." }
332 | val bundle = parcel.readBundle(loader)
333 | LOG.v { "Map $hashcode: read extra bundle, size ${bundle?.size()}" }
334 | firestoreMap.onReadFromBundle(bundle!!)
335 | return firestoreMap
336 | }
337 |
338 | override fun newArray(size: Int): Array?> {
339 | return Array(size) { null }
340 | }
341 | }
342 | }
343 |
344 | protected open fun onWriteToBundle(bundle: Bundle) {}
345 |
346 | protected open fun onReadFromBundle(bundle: Bundle) {}
347 |
348 | // Delegation
349 |
350 | protected operator fun invoke(name: String? = null): ReadWriteProperty, R>
351 | = FirestoreFieldDelegate(name)
352 |
353 |
354 | // To support "by this" without a function...
355 |
356 | private val defaultDelegate = this()
357 |
358 | protected operator fun getValue(source: FirestoreMap, property: KProperty<*>): R {
359 | @Suppress("UNCHECKED_CAST")
360 | return defaultDelegate.getValue(source, property) as R
361 | }
362 |
363 | protected operator fun setValue(source: FirestoreMap, property: KProperty<*>, what: R) {
364 | defaultDelegate.setValue(source, property, what)
365 | }
366 | }
--------------------------------------------------------------------------------