├── 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 | [![Build Status](https://github.com/natario1/Firestore/workflows/Build/badge.svg?event=push)](https://github.com/natario1/Firestore/actions) 2 | [![Release](https://img.shields.io/github/release/natario1/Firestore.svg)](https://github.com/natario1/Firestore/releases) 3 | [![Issues](https://img.shields.io/github/issues-raw/natario1/Firestore.svg)](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 | } --------------------------------------------------------------------------------