├── .gitignore ├── .idea ├── .gitignore ├── .name ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── misc.xml └── vcs.xml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── database ├── .gitignore ├── README.md ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── rosariopfernandes │ │ └── firebasecompose │ │ └── database │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── io │ │ └── github │ │ └── rosariopfernandes │ │ └── firebasecompose │ │ └── database │ │ ├── DatabaseListRefState.kt │ │ ├── DatabaseRefState.kt │ │ ├── RTDBData.kt │ │ └── RTDBDataList.kt │ └── test │ └── java │ └── io │ └── github │ └── rosariopfernandes │ └── firebasecompose │ └── database │ └── ExampleUnitTest.kt ├── demo ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── rosariopfernandes │ │ └── firebasecompose │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── io │ │ │ └── github │ │ │ └── rosariopfernandes │ │ │ └── firebasecompose │ │ │ ├── model │ │ │ └── Snack.kt │ │ │ └── ui │ │ │ ├── DetailActivity.kt │ │ │ ├── MainActivity.kt │ │ │ ├── components │ │ │ ├── LoadingBar.kt │ │ │ └── OnlyText.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── io │ └── github │ └── rosariopfernandes │ └── firebasecompose │ └── ExampleUnitTest.kt ├── firestore ├── .gitignore ├── README.md ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── github │ │ └── rosariopfernandes │ │ └── firebasecompose │ │ └── firestore │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── io │ │ └── github │ │ └── rosariopfernandes │ │ └── firebasecompose │ │ └── firestore │ │ ├── CollectionState.kt │ │ ├── DocumentState.kt │ │ ├── FirestoreCollection.kt │ │ └── FirestoreDocument.kt │ └── test │ └── java │ └── io │ └── github │ └── rosariopfernandes │ └── firebasecompose │ └── firestore │ └── ExampleUnitTest.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Firebase Compose -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | We love contributions! But we'd like you to follow these guidelines to make the contribution process 4 | easy for everyone involved: 5 | 6 | ## Issue Tracker 7 | 8 | The issue tracker is our main channel to report a bug in the source code or a mistake in the 9 | documentation, and to request a new feature. 10 | 11 | Before submitting your issue, please search the archive. Maybe your question was already answered, 12 | your bug has already been reported or your feature has already been requested. 13 | 14 | Providing the following information will increase the chances of your issue being dealt with 15 | quickly: 16 | 17 | * **Bug reports** - if an error is being thrown, please include a stack trace and the steps to 18 | reproduce the error. If there is no error, please explain why do you consider it a bug. 19 | 20 | * **Feature Requests** - please make it clear whether you're willing to write the code for it or you 21 | need someone else to do it. 22 | 23 | * **Related Issues** - if you found a similar issue that has been reported before, be sure to 24 | mention it. 25 | 26 | ## Pull Requests 27 | 28 | Before making any changes, consider following these steps: 29 | 30 | 1. Search for an open or closed Pull Request related to your changes. 31 | 32 | 2. Search the issue tracker for issues related to your changes. 33 | 34 | 3. Open a [new issue](github.com/rosariopfernandes/FirebaseCompose/issues/new) to discuss your changes 35 | with the project owners. If they approve it, send the Pull Request. 36 | 37 | ### Sending Pull Requests 38 | If your change has been approved, follow this process: 39 | 40 | 1. [Fork](http://help.github.com/fork-a-repo/) the project, clone your fork and configure the 41 | remotes: 42 | 43 | ```bash 44 | # Clone your fork into the current directory 45 | git clone https://github.com// 46 | # Navigate to the newly cloned directory 47 | cd 48 | # Assign the original repo to a remote called "upstream" 49 | git remote add upstream https://github.com/rosariopfernandes/FirebaseCompose 50 | ``` 51 | 52 | 2. Make your changes in a new branch: 53 | 54 | ```bash 55 | git checkout -b my-fix-branch master 56 | ``` 57 | 58 | 3. Commit the changes using a descriptive commit message 59 | 60 | ```bash 61 | git commit -a 62 | ``` 63 | 64 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 65 | 66 | 4. Push your branch to GitHub: 67 | 68 | ```bash 69 | git push origin my-fix-branch 70 | ``` 71 | 72 | 5. In GitHub, [send a Pull Request](https://help.github.com/articles/using-pull-requests/) with a 73 | clear title and description. 74 | 75 | * If we suggest changes then: 76 | * Make the required changes; 77 | * Rebase your branch and force push to your GitHub repository (this updates your Pull Request): 78 | ```bash 79 | # Rebase the branch 80 | git rebase master -i 81 | # Update the Pull Request 82 | git push origin my-fix-branch -f 83 | ``` 84 | That's it! Thank you for you contribution! 85 | 86 | ### After your Pull Request is merged 87 | 88 | You can delete your branch and pull changes from the original 89 | (upstream) repository: 90 | 91 | 1. Delete the remote branch on GitHub either through the GitHub UI or your local shell as follows: 92 | 93 | ```bash 94 | git push origin --delete my-fix-branch 95 | ``` 96 | 97 | 2. Check out the master branch: 98 | 99 | ```bash 100 | git checkout master -f 101 | ``` 102 | 103 | 3. Delete the local branch: 104 | 105 | ```bash 106 | git branch -D my-fix-branch 107 | ``` 108 | 109 | 4. Update your master with the latest upstream version: 110 | 111 | ```bash 112 | git pull --ff upstream master 113 | ``` 114 | 115 | ## Coding Rules 116 | 117 | We generally follow the 118 | [Android Kotlin Style Guide](https://android.github.io/kotlin-guides/style.html). 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rosário Pereira Fernandes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Firebase Compose 2 | 3 | Firebase Compose is an Open Source library for Android that allows you to 4 | quickly connect a Jetpack Compose app to [Firebase](https://firebase.google.com) APIs. 5 | 6 | ## Table of contents 7 | 8 | 1. [Usage](#usage) 9 | 1. [Installation](#installation) 10 | 1. [Dependencies](#dependencies) 11 | 1. [Compatibility](#compatibility-with-firebase-libraries) 12 | 1. [Sample App](#sample-app) 13 | 1. [Contributing](#contributing) 14 | 1. [License](#license) 15 | 1. [Acknowledgment](#acknowledgment) 16 | 17 | ## Usage 18 | 19 | Firebase Compose has separate modules for using Firebase Realtime Database and Cloud Firestore. 20 | To get started, see the individual instructions for each module: 21 | 22 | * [Firebase Compose Firestore](firestore/README.md) 23 | * [Firebase Compose Database](database/README.md) 24 | 25 | ## Installation 26 | 27 | Firebase Compose is published as a collection of libraries separated by the 28 | Firebase API they target. Each Firebase Compose library has a transitive 29 | dependency on the appropriate Firebase SDK so there is no need to include 30 | those separately in your app. 31 | 32 | **Step 1** - Add the jitpack maven in your root `build.gradle` at the end of repositories: 33 | ```gradle 34 | allprojects { 35 | repositories { 36 | ... 37 | maven { url 'https://jitpack.io' } 38 | } 39 | } 40 | ``` 41 | 42 | **Step 2** - In your `app/build.gradle` file add a dependency on one of the Firebase Compose 43 | libraries. 44 | 45 | ```groovy 46 | dependencies { 47 | // Firebase Compose for Firebase Realtime Database 48 | implementation 'com.github.rosariopfernandes.firebase-compose:database:1.0.0-beta01' 49 | 50 | // Firebase Compose for Cloud Firestore 51 | implementation 'com.github.rosariopfernandes.firebase-compose:firestore:1.0.0-beta01' 52 | } 53 | ``` 54 | 55 | After the project is synchronized, we're ready to start using Firebase functionality in our Compose app. 56 | 57 | ## Dependencies 58 | 59 | ### Compatibility with Firebase libraries 60 | 61 | Firebase Compose libraries have the following transitive dependencies on the Firebase SDK: 62 | ``` 63 | firebase-compose:database 64 | |--- com.google.firebase:firebase-database-ktx 65 | 66 | firebase-compose:firestore 67 | |--- com.google.firebase:firebase-firestore-ktx 68 | ``` 69 | 70 | ## Sample app 71 | A sample app is available in the [demo](/demo/) directory. 72 | 73 | ## Contributing 74 | 75 | Anyone and everyone is welcome to contribute. Please take a moment to 76 | review the [contributing guidelines](CONTRIBUTING.md). 77 | 78 | ## License 79 | 80 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 81 | 82 | ## Acknowledgment 83 | README files inspired by [FirebaseUI](https://github.com/firebase/FirebaseUI-Android/) 84 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext { 4 | compose_version = '1.0.0-beta01' 5 | } 6 | repositories { 7 | google() 8 | jcenter() 9 | } 10 | dependencies { 11 | classpath 'com.android.tools.build:gradle:7.0.0-alpha08' 12 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.30" 13 | classpath 'com.google.gms:google-services:4.3.5' 14 | 15 | // NOTE: Do not place your application dependencies here; they belong 16 | // in the individual module build.gradle files 17 | } 18 | } 19 | 20 | allprojects { 21 | repositories { 22 | google() 23 | jcenter() 24 | } 25 | } 26 | 27 | task clean(type: Delete) { 28 | delete rootProject.buildDir 29 | } -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /database/README.md: -------------------------------------------------------------------------------- 1 | # Firebase Compose for the Realtime Database 2 | 3 | Coming Soon 4 | -------------------------------------------------------------------------------- /database/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion 30 8 | buildToolsVersion "30.0.3" 9 | 10 | defaultConfig { 11 | minSdkVersion 21 12 | targetSdkVersion 30 13 | versionCode 1 14 | versionName "1.0.0-beta01" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles "consumer-rules.pro" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | useIR = true 33 | } 34 | buildFeatures { 35 | compose true 36 | } 37 | composeOptions { 38 | kotlinCompilerExtensionVersion compose_version 39 | } 40 | } 41 | 42 | dependencies { 43 | 44 | implementation 'androidx.core:core-ktx:1.3.2' 45 | implementation 'androidx.appcompat:appcompat:1.2.0' 46 | implementation 'com.google.android.material:material:1.3.0' 47 | implementation "androidx.compose.ui:ui:$compose_version" 48 | 49 | api 'com.google.firebase:firebase-database-ktx:19.6.0' 50 | 51 | testImplementation 'junit:junit:4.+' 52 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 53 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 54 | } -------------------------------------------------------------------------------- /database/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatfiredev/firebase-compose/c6eebe2e6d0c9aeafed704a8634b94025e13da59/database/consumer-rules.pro -------------------------------------------------------------------------------- /database/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. 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 -------------------------------------------------------------------------------- /database/src/androidTest/java/io/github/rosariopfernandes/firebasecompose/database/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.database 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals( 23 | "io.github.rosariopfernandes.firebasecompose.database.test", 24 | appContext.packageName 25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /database/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /database/src/main/java/io/github/rosariopfernandes/firebasecompose/database/DatabaseListRefState.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.database 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.lifecycle.LifecycleObserver 9 | import androidx.lifecycle.LifecycleOwner 10 | import androidx.lifecycle.OnLifecycleEvent 11 | import com.google.firebase.database.DataSnapshot 12 | import com.google.firebase.database.DatabaseError 13 | import com.google.firebase.database.DatabaseReference 14 | import com.google.firebase.database.ValueEventListener 15 | 16 | /** 17 | * A value holder where reads to the [value] property during the execution of a 18 | * [androidx.compose.runtime.Composable] function, the current 19 | * [androidx.compose.runtime.RecomposeScope] will be subscribed to changes 20 | * of that value. When the [value] property is changed in that Database Reference, 21 | * a recomposition of any subscribed [androidx.compose.runtime.RecomposeScope]s 22 | * will be scheduled. 23 | * 24 | * @see [State] 25 | * @see [databaseListRefStateOf] 26 | */ 27 | interface DatabaseListRefState : State, LifecycleObserver { 28 | override val value: RTDBDataList 29 | fun startListening() 30 | fun stopListening() 31 | operator fun component1(): RTDBDataList 32 | } 33 | 34 | /** 35 | * Return a new [DatabaseListRefState] initialized with the passed [databaseRef] 36 | * 37 | * The DatabaseListRefState class is a single value holder whose reads are observed by 38 | * Compose. 39 | * 40 | * @param databaseRef the database reference whose children will be observed 41 | * @param lifecycleOwner the lifecycle owner that the state should react to 42 | * 43 | * @see State 44 | * @see DatabaseRefState 45 | */ 46 | fun databaseListRefStateOf( 47 | databaseRef: DatabaseReference, 48 | lifecycleOwner: LifecycleOwner? = null 49 | ) = object : DatabaseListRefState { 50 | private var listener: ValueEventListener? = null 51 | private var dataState: RTDBDataList by mutableStateOf(RTDBDataList.Loading) 52 | 53 | override val value: RTDBDataList 54 | get() = dataState 55 | 56 | init { 57 | lifecycleOwner?.lifecycle?.addObserver(this) 58 | } 59 | 60 | @OnLifecycleEvent(Lifecycle.Event.ON_START) 61 | override fun startListening() { 62 | if (listener == null) { 63 | listener = databaseRef.addValueEventListener(object : ValueEventListener { 64 | override fun onDataChange(snapshot: DataSnapshot) { 65 | val list = snapshot.children.toList() 66 | dataState = RTDBDataList.SnapshotList(list) 67 | } 68 | 69 | override fun onCancelled(error: DatabaseError) { 70 | dataState = RTDBDataList.Error(error) 71 | } 72 | }) 73 | } 74 | } 75 | 76 | @OnLifecycleEvent(Lifecycle.Event.ON_STOP) 77 | override fun stopListening() { 78 | listener?.let { databaseRef.removeEventListener(it) } 79 | } 80 | 81 | override fun component1() = value 82 | } 83 | -------------------------------------------------------------------------------- /database/src/main/java/io/github/rosariopfernandes/firebasecompose/database/DatabaseRefState.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.database 2 | 3 | import androidx.compose.runtime.State 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.Lifecycle 8 | import androidx.lifecycle.LifecycleObserver 9 | import androidx.lifecycle.LifecycleOwner 10 | import androidx.lifecycle.OnLifecycleEvent 11 | import com.google.firebase.database.DataSnapshot 12 | import com.google.firebase.database.DatabaseError 13 | import com.google.firebase.database.DatabaseReference 14 | import com.google.firebase.database.ValueEventListener 15 | 16 | /** 17 | * A value holder where reads to the [value] property during the execution of a 18 | * [androidx.compose.runtime.Composable] function, the current 19 | * [androidx.compose.runtime.RecomposeScope] will be subscribed to changes of that value. When the 20 | * [value] property is changed in that Database Reference, a recomposition of any subscribed 21 | * [androidx.compose.runtime.RecomposeScope]s will be scheduled. 22 | * 23 | * @see [State] 24 | * @see [databaseRefStateOf] 25 | */ 26 | interface DatabaseRefState : State, LifecycleObserver { 27 | override val value: RTDBData 28 | fun startListening() 29 | fun stopListening() 30 | operator fun component1(): RTDBData 31 | } 32 | 33 | /** 34 | * Return a new [DatabaseRefState] initialized with the passed [databaseRef] 35 | * 36 | * The DatabaseRefState class is a single value holder whose reads are observed by 37 | * Compose. 38 | * 39 | * @param databaseRef the database reference to be observed 40 | * @param lifecycleOwner the lifecycle owner that the state should react to 41 | * 42 | * @see State 43 | * @see DatabaseRefState 44 | */ 45 | fun databaseRefStateOf( 46 | databaseRef: DatabaseReference, 47 | lifecycleOwner: LifecycleOwner? = null 48 | ) = object : DatabaseRefState { 49 | private var listener: ValueEventListener? = null 50 | private var dataState: RTDBData by mutableStateOf(RTDBData.Loading) 51 | 52 | override val value: RTDBData 53 | get() = dataState 54 | 55 | init { 56 | lifecycleOwner?.lifecycle?.addObserver(this) 57 | } 58 | 59 | @OnLifecycleEvent(Lifecycle.Event.ON_START) 60 | override fun startListening() { 61 | if (listener == null) { 62 | listener = databaseRef.addValueEventListener(object : ValueEventListener { 63 | override fun onDataChange(snapshot: DataSnapshot) { 64 | dataState = RTDBData.Snapshot(snapshot) 65 | } 66 | 67 | override fun onCancelled(error: DatabaseError) { 68 | dataState = RTDBData.Error(error) 69 | } 70 | }) 71 | } 72 | } 73 | 74 | @OnLifecycleEvent(Lifecycle.Event.ON_STOP) 75 | override fun stopListening() { 76 | listener?.let { databaseRef.removeEventListener(it) } 77 | } 78 | 79 | override fun component1() = value 80 | } 81 | -------------------------------------------------------------------------------- /database/src/main/java/io/github/rosariopfernandes/firebasecompose/database/RTDBData.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.database 2 | 3 | import com.google.firebase.database.DataSnapshot 4 | import com.google.firebase.database.DatabaseError 5 | 6 | sealed class RTDBData { 7 | data class Snapshot(val snapshot: DataSnapshot) : RTDBData() 8 | data class Error(val databaseError: DatabaseError) : RTDBData() 9 | object Loading : RTDBData() 10 | } -------------------------------------------------------------------------------- /database/src/main/java/io/github/rosariopfernandes/firebasecompose/database/RTDBDataList.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.database 2 | 3 | import com.google.firebase.database.DataSnapshot 4 | import com.google.firebase.database.DatabaseError 5 | 6 | sealed class RTDBDataList { 7 | data class SnapshotList(val snapshot: List) : RTDBDataList() 8 | data class Error(val databaseError: DatabaseError) : RTDBDataList() 9 | object Loading : RTDBDataList() 10 | } -------------------------------------------------------------------------------- /database/src/test/java/io/github/rosariopfernandes/firebasecompose/database/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.database 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /demo/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'kotlin-android' 4 | id 'com.google.gms.google-services' 5 | } 6 | 7 | android { 8 | compileSdkVersion 30 9 | buildToolsVersion "30.0.3" 10 | 11 | defaultConfig { 12 | applicationId "io.github.rosariopfernandes.firebasecompose" 13 | minSdkVersion 21 14 | targetSdkVersion 30 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | } 20 | 21 | buildTypes { 22 | release { 23 | minifyEnabled false 24 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 25 | } 26 | } 27 | compileOptions { 28 | sourceCompatibility JavaVersion.VERSION_1_8 29 | targetCompatibility JavaVersion.VERSION_1_8 30 | } 31 | kotlinOptions { 32 | jvmTarget = '1.8' 33 | useIR = true 34 | } 35 | buildFeatures { 36 | compose true 37 | } 38 | composeOptions { 39 | kotlinCompilerExtensionVersion compose_version 40 | } 41 | } 42 | 43 | dependencies { 44 | 45 | implementation 'androidx.core:core-ktx:1.3.2' 46 | implementation 'androidx.appcompat:appcompat:1.2.0' 47 | implementation 'com.google.android.material:material:1.3.0' 48 | implementation "androidx.compose.ui:ui:$compose_version" 49 | implementation "androidx.compose.ui:ui-util:$compose_version" 50 | implementation "androidx.activity:activity-compose:1.3.0-alpha02" 51 | implementation "androidx.compose.material:material:$compose_version" 52 | implementation "androidx.compose.ui:ui-tooling:$compose_version" 53 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0' 54 | 55 | // Firebase Compose 56 | implementation project(':firestore') 57 | 58 | // Helper libraries for UI 59 | implementation "dev.chrisbanes.accompanist:accompanist-glide:0.6.0" 60 | implementation "dev.chrisbanes.accompanist:accompanist-insets:0.6.0" 61 | 62 | testImplementation 'junit:junit:4.+' 63 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 64 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 65 | } -------------------------------------------------------------------------------- /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. 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 -------------------------------------------------------------------------------- /demo/src/androidTest/java/io/github/rosariopfernandes/firebasecompose/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("io.github.rosariopfernandes.firebasecompose", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /demo/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 14 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/rosariopfernandes/firebasecompose/model/Snack.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.model 2 | 3 | data class Snack( 4 | val id: Long = 1L, 5 | val name: String = "Donut", 6 | val imageUrl: String = "", 7 | val price: Long = 100L, 8 | val tagline: String = "Dessert", 9 | ) 10 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/rosariopfernandes/firebasecompose/ui/DetailActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.ui 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.compose.foundation.* 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.material.* 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.outlined.ArrowBack 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Brush 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.graphicsLayer 18 | import androidx.compose.ui.unit.lerp 19 | import androidx.compose.ui.util.lerp 20 | import androidx.compose.ui.layout.Layout 21 | import androidx.compose.ui.platform.LocalDensity 22 | import androidx.compose.ui.res.stringResource 23 | import androidx.compose.ui.unit.Constraints 24 | import androidx.compose.ui.unit.dp 25 | import androidx.compose.ui.unit.sp 26 | import com.bumptech.glide.request.RequestOptions 27 | import com.google.firebase.firestore.ktx.firestore 28 | import com.google.firebase.firestore.ktx.toObject 29 | import com.google.firebase.ktx.Firebase 30 | import dev.chrisbanes.accompanist.glide.GlideImage 31 | import dev.chrisbanes.accompanist.insets.ProvideWindowInsets 32 | import dev.chrisbanes.accompanist.insets.navigationBarsPadding 33 | import dev.chrisbanes.accompanist.insets.statusBarsPadding 34 | import io.github.rosariopfernandes.firebasecompose.R 35 | import io.github.rosariopfernandes.firebasecompose.firestore.FirestoreDocument 36 | import io.github.rosariopfernandes.firebasecompose.firestore.documentStateOf 37 | import io.github.rosariopfernandes.firebasecompose.model.Snack 38 | import io.github.rosariopfernandes.firebasecompose.ui.components.LoadingBar 39 | import io.github.rosariopfernandes.firebasecompose.ui.components.OnlyText 40 | import io.github.rosariopfernandes.firebasecompose.ui.theme.FirebaseComposeTheme 41 | import io.github.rosariopfernandes.firebasecompose.ui.theme.purple200 42 | import io.github.rosariopfernandes.firebasecompose.ui.theme.purple500 43 | import io.github.rosariopfernandes.firebasecompose.ui.theme.purple700 44 | import kotlin.math.max 45 | import kotlin.math.min 46 | 47 | private val BottomBarHeight = 56.dp 48 | private val TitleHeight = 128.dp 49 | private val GradientScroll = 180.dp 50 | private val ImageOverlap = 115.dp 51 | private val MinTitleOffset = 56.dp 52 | private val MinImageOffset = 12.dp 53 | private val MaxTitleOffset = ImageOverlap + MinTitleOffset + GradientScroll 54 | private val ExpandedImageSize = 300.dp 55 | private val CollapsedImageSize = 150.dp 56 | private val HzPadding = Modifier.padding(horizontal = 24.dp) 57 | 58 | class DetailActivity : AppCompatActivity() { 59 | @ExperimentalFoundationApi 60 | override fun onCreate(savedInstanceState: Bundle?) { 61 | super.onCreate(savedInstanceState) 62 | 63 | val snackId = intent.extras!!.getString("snackId")!! 64 | setContent { 65 | FirebaseComposeTheme { 66 | ProvideWindowInsets { 67 | val docRef = Firebase.firestore.collection("snacks").document(snackId) 68 | val (result) = remember { documentStateOf(docRef, this@DetailActivity) } 69 | when (result) { 70 | is FirestoreDocument.Loading -> { 71 | LoadingBar() 72 | } 73 | is FirestoreDocument.Error -> { 74 | val exception = result.exception 75 | OnlyText(title = stringResource(id = R.string.title_error), 76 | message = exception.message ?: "") 77 | } 78 | is FirestoreDocument.Snapshot -> { 79 | val snack = result.snapshot.toObject()!! 80 | SnackDetail(snack = snack, upPress = { finish() }) 81 | } 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | @Composable 90 | fun SnackDetail( 91 | snack: Snack, 92 | upPress: () -> Unit 93 | ) { 94 | Box(Modifier.fillMaxSize()) { 95 | val scroll = rememberScrollState(0) 96 | Header() 97 | Body(scroll) 98 | Title(snack, scroll.value) 99 | Image(snack.imageUrl, scroll.value) 100 | Up(upPress) 101 | } 102 | } 103 | 104 | @Composable 105 | private fun Header() { 106 | Spacer( 107 | modifier = Modifier 108 | .height(280.dp) 109 | .fillMaxWidth() 110 | .background(Brush.horizontalGradient(listOf(purple200, purple500))) 111 | ) 112 | } 113 | 114 | @Composable 115 | private fun Up(upPress: () -> Unit) { 116 | IconButton( 117 | onClick = upPress, 118 | modifier = Modifier 119 | .statusBarsPadding() 120 | .padding(horizontal = 16.dp, vertical = 10.dp) 121 | .size(36.dp) 122 | .background( 123 | color = purple200, 124 | shape = CircleShape 125 | ) 126 | ) { 127 | Icon( 128 | imageVector = Icons.Outlined.ArrowBack, 129 | tint = Color.White, 130 | contentDescription = stringResource(R.string.label_back) 131 | ) 132 | } 133 | } 134 | 135 | @Composable 136 | private fun Body( 137 | scroll: ScrollState 138 | ) { 139 | Column { 140 | Spacer( 141 | modifier = Modifier 142 | .fillMaxWidth() 143 | .statusBarsPadding() 144 | .height(MinTitleOffset) 145 | ) 146 | Column( 147 | modifier = Modifier.verticalScroll(scroll) 148 | ) { 149 | Spacer(Modifier.height(GradientScroll)) 150 | Surface(Modifier.fillMaxWidth()) { 151 | Column { 152 | Spacer(Modifier.height(ImageOverlap)) 153 | Spacer(Modifier.height(TitleHeight)) 154 | 155 | Spacer(Modifier.height(16.dp)) 156 | Text( 157 | text = stringResource(R.string.detail_header), 158 | style = MaterialTheme.typography.overline, 159 | color = Color.DarkGray, 160 | modifier = HzPadding 161 | ) 162 | Spacer(Modifier.height(4.dp)) 163 | Text( 164 | text = stringResource(R.string.detail_placeholder), 165 | style = MaterialTheme.typography.body1, 166 | color = Color.Gray, 167 | modifier = HzPadding 168 | ) 169 | 170 | Spacer(Modifier.height(40.dp)) 171 | Text( 172 | text = stringResource(R.string.ingredients), 173 | style = MaterialTheme.typography.overline, 174 | color = Color.DarkGray, 175 | modifier = HzPadding 176 | ) 177 | Spacer(Modifier.height(4.dp)) 178 | Text( 179 | text = stringResource(R.string.ingredients_list), 180 | style = MaterialTheme.typography.body1, 181 | color = Color.Gray, 182 | modifier = HzPadding 183 | ) 184 | 185 | Spacer(Modifier.height(16.dp)) 186 | Divider( color = Color.LightGray, thickness = 1.dp, startIndent = 0.dp ) 187 | 188 | Spacer( 189 | modifier = Modifier 190 | .padding(bottom = BottomBarHeight) 191 | .navigationBarsPadding(left = false, right = false) 192 | .height(8.dp) 193 | ) 194 | } 195 | } 196 | } 197 | } 198 | } 199 | 200 | @Composable 201 | private fun Title(snack: Snack, scroll: Int) { 202 | val maxOffset = with(LocalDensity.current) { MaxTitleOffset.toPx() } 203 | val minOffset = with(LocalDensity.current) { MinTitleOffset.toPx() } 204 | val offset = (maxOffset - scroll).coerceAtLeast(minOffset) 205 | Column( 206 | verticalArrangement = Arrangement.Bottom, 207 | modifier = Modifier 208 | .heightIn(min = TitleHeight) 209 | .statusBarsPadding() 210 | .graphicsLayer { translationY = offset } 211 | .background(color = Color.White) 212 | ) { 213 | Spacer(Modifier.height(16.dp)) 214 | Text( 215 | text = snack.name, 216 | style = MaterialTheme.typography.h4, 217 | color = purple500, 218 | modifier = HzPadding 219 | ) 220 | Text( 221 | text = snack.tagline, 222 | style = MaterialTheme.typography.subtitle2, 223 | fontSize = 20.sp, 224 | color = Color.DarkGray, 225 | modifier = HzPadding 226 | ) 227 | Spacer(Modifier.height(4.dp)) 228 | Text( 229 | text = "${snack.price} MZN", 230 | style = MaterialTheme.typography.h6, 231 | color = purple700, 232 | modifier = HzPadding 233 | ) 234 | 235 | Spacer(Modifier.height(8.dp)) 236 | Divider( color = Color.LightGray, thickness = 1.dp, startIndent = 0.dp ) 237 | } 238 | } 239 | 240 | @Composable 241 | private fun Image( 242 | imageUrl: String, 243 | scroll: Int 244 | ) { 245 | val collapseRange = with(LocalDensity.current) { (MaxTitleOffset - MinTitleOffset).toPx() } 246 | val collapseFraction = (scroll / collapseRange).coerceIn(0f, 1f) 247 | 248 | CollapsingImageLayout( 249 | collapseFraction = collapseFraction, 250 | modifier = HzPadding.then(Modifier.statusBarsPadding()) 251 | ) { 252 | GlideImage( 253 | data = imageUrl, 254 | contentDescription = null, 255 | modifier = Modifier.fillMaxSize(), 256 | requestBuilder = { 257 | val options = RequestOptions() 258 | options.circleCrop() 259 | apply(options) 260 | } 261 | ) 262 | } 263 | } 264 | 265 | @Composable 266 | private fun CollapsingImageLayout( 267 | collapseFraction: Float, 268 | modifier: Modifier = Modifier, 269 | content: @Composable () -> Unit 270 | ) { 271 | Layout( 272 | modifier = modifier, 273 | content = content 274 | ) { measurables, constraints -> 275 | check(measurables.size == 1) 276 | 277 | val imageMaxSize = min(ExpandedImageSize.roundToPx(), constraints.maxWidth) 278 | val imageMinSize = max(CollapsedImageSize.roundToPx(), constraints.minWidth) 279 | val imageWidth = lerp(imageMaxSize, imageMinSize, collapseFraction) 280 | val imagePlaceable = measurables[0].measure(Constraints.fixed(imageWidth, imageWidth)) 281 | 282 | val imageY = lerp(MinTitleOffset, MinImageOffset, collapseFraction).roundToPx() 283 | val imageX = lerp( 284 | (constraints.maxWidth - imageWidth) / 2, // centered when expanded 285 | constraints.maxWidth - imageWidth, // right aligned when collapsed 286 | collapseFraction 287 | ) 288 | layout( 289 | width = constraints.maxWidth, 290 | height = imageY + imageWidth 291 | ) { 292 | imagePlaceable.place(imageX, imageY) 293 | } 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/rosariopfernandes/firebasecompose/ui/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.ui 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.widget.Toast 7 | import androidx.activity.compose.setContent 8 | import androidx.appcompat.app.AppCompatActivity 9 | import androidx.compose.foundation.ExperimentalFoundationApi 10 | import androidx.compose.foundation.clickable 11 | import androidx.compose.foundation.layout.* 12 | import androidx.compose.foundation.lazy.GridCells 13 | import androidx.compose.foundation.lazy.LazyVerticalGrid 14 | import androidx.compose.foundation.lazy.items 15 | import androidx.compose.material.* 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.res.stringResource 22 | import androidx.compose.ui.unit.dp 23 | import com.bumptech.glide.request.RequestOptions 24 | import com.google.firebase.firestore.DocumentSnapshot 25 | import com.google.firebase.firestore.ktx.firestore 26 | import com.google.firebase.firestore.ktx.toObject 27 | import com.google.firebase.ktx.Firebase 28 | import dev.chrisbanes.accompanist.glide.GlideImage 29 | import io.github.rosariopfernandes.firebasecompose.R 30 | import io.github.rosariopfernandes.firebasecompose.firestore.FirestoreCollection 31 | import io.github.rosariopfernandes.firebasecompose.firestore.collectionStateOf 32 | import io.github.rosariopfernandes.firebasecompose.model.Snack 33 | import io.github.rosariopfernandes.firebasecompose.ui.components.LoadingBar 34 | import io.github.rosariopfernandes.firebasecompose.ui.components.OnlyText 35 | import io.github.rosariopfernandes.firebasecompose.ui.theme.FirebaseComposeTheme 36 | 37 | const val FIRESTORE_COLLECTION = "snacks" 38 | 39 | class MainActivity : AppCompatActivity() { 40 | @ExperimentalFoundationApi 41 | override fun onCreate(savedInstanceState: Bundle?) { 42 | super.onCreate(savedInstanceState) 43 | setContent { 44 | FirebaseComposeTheme { 45 | Scaffold( 46 | topBar = { Toolbar(this@MainActivity) }, 47 | content = { 48 | val query = Firebase.firestore.collection(FIRESTORE_COLLECTION) 49 | val (result) = remember { collectionStateOf (query, this) } 50 | 51 | when (result) { 52 | is FirestoreCollection.Error -> { 53 | OnlyText( 54 | title = stringResource(R.string.title_error), 55 | message = result.exception.message ?: "" 56 | ) 57 | } 58 | is FirestoreCollection.Loading -> { 59 | LoadingBar() 60 | } 61 | is FirestoreCollection.Snapshot -> { 62 | if (result.list.isEmpty()) { 63 | OnlyText( 64 | title = stringResource(id = R.string.title_empty), 65 | message = stringResource(id = R.string.message_empty) 66 | ) 67 | } else { 68 | SnackList(context = this@MainActivity, items = result.list) 69 | } 70 | } 71 | } 72 | } 73 | ) 74 | } 75 | } 76 | } 77 | } 78 | 79 | @Composable 80 | fun Toolbar(context: Context) { 81 | TopAppBar( 82 | title = { Text(text = "FireSnacks") }, 83 | actions = { 84 | Button(onClick = { addItems(context) }) { 85 | Text(text = "Add Items") 86 | } 87 | } 88 | ) 89 | } 90 | 91 | fun addItems(context: Context) { 92 | val firestore = Firebase.firestore 93 | val collection = firestore.collection(FIRESTORE_COLLECTION) 94 | firestore.runBatch { batch -> 95 | for (item in getItems()) { 96 | val docRef = collection.document() 97 | batch.set(docRef, item) 98 | } 99 | }.addOnSuccessListener { 100 | Toast.makeText(context, "Items added!", Toast.LENGTH_SHORT).show() 101 | }.addOnFailureListener { e -> 102 | Toast.makeText(context, "Error adding items: ${e.message}", Toast.LENGTH_LONG).show() 103 | } 104 | } 105 | 106 | private fun getItems() = listOf( 107 | Snack(id = 1, name = "Cupcake", imageUrl = "https://upload.wikimedia.org/wikipedia/commons/5/56/Chocolate_cupcakes.jpg"), 108 | Snack(id = 2, name = "Donut", imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Glazed-Donut.jpg/640px-Glazed-Donut.jpg"), 109 | Snack(id = 3, name = "Eclair", imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/Ginger_Spice_Eclair_-_39843168741.jpg/640px-Ginger_Spice_Eclair_-_39843168741.jpg"), 110 | Snack(id = 4, name = "Froyo", imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/06/ICE_CREAM_%28FROYO%29_01.jpg/640px-ICE_CREAM_%28FROYO%29_01.jpg"), 111 | Snack(id = 5, name = "Gingerbread", imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Gingerbread_men.jpg/401px-Gingerbread_men.jpg"), 112 | Snack(id = 6, name = "Honey", imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/37/Bienenwabe_mit_Eiern_und_Brut_5.jpg/640px-Bienenwabe_mit_Eiern_und_Brut_5.jpg"), 113 | Snack(id = 7, name = "Ice Cream", imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/Bananas_Foster_Ice_Cream_at_Little_Giant_Ice_Cream.jpg/480px-Bananas_Foster_Ice_Cream_at_Little_Giant_Ice_Cream.jpg"), 114 | Snack(id = 8, name = "Jelly Beans", imageUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/JellyBellyBeans.jpg/616px-JellyBellyBeans.jpg") 115 | ) 116 | 117 | @ExperimentalFoundationApi 118 | @Composable 119 | fun SnackList( 120 | context: Context, 121 | items: List 122 | ) { 123 | LazyVerticalGrid(cells = GridCells.Fixed(2), content = { 124 | items(items) { snapshot -> 125 | val snack = snapshot.toObject()!! 126 | SnackItem(snack = snack, onSnackClick = { 127 | val intent = Intent(context, DetailActivity::class.java) 128 | intent.putExtra("snackId", snapshot.id) 129 | context.startActivity(intent) 130 | }) 131 | } 132 | }) 133 | } 134 | 135 | @Composable 136 | fun SnackItem( 137 | snack: Snack, 138 | onSnackClick: () -> Unit, 139 | modifier: Modifier = Modifier 140 | ) { 141 | Card( 142 | shape = MaterialTheme.shapes.medium, 143 | modifier = modifier.padding(start = 4.dp, end = 4.dp, bottom = 8.dp) 144 | ) { 145 | Column( 146 | horizontalAlignment = Alignment.CenterHorizontally, 147 | modifier = Modifier 148 | .clickable(onClick = { onSnackClick() }) 149 | .padding(8.dp) 150 | ) { 151 | GlideImage( 152 | data = snack.imageUrl, 153 | contentDescription = null, 154 | modifier = Modifier.size(120.dp), 155 | requestBuilder = { 156 | val options = RequestOptions() 157 | options.circleCrop() 158 | apply(options) 159 | } 160 | ) 161 | Text( 162 | text = snack.name, 163 | style = MaterialTheme.typography.subtitle1, 164 | color = Color.DarkGray, 165 | modifier = Modifier.padding(top = 8.dp) 166 | ) 167 | } 168 | } 169 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/rosariopfernandes/firebasecompose/ui/components/LoadingBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.ui.components 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material.CircularProgressIndicator 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | 10 | @Composable 11 | fun LoadingBar() { 12 | Column( 13 | horizontalAlignment = Alignment.CenterHorizontally, 14 | verticalArrangement = Arrangement.Center, 15 | modifier = Modifier.fillMaxWidth().fillMaxHeight() 16 | ) { 17 | CircularProgressIndicator(modifier = Modifier.width(64.dp).height(64.dp)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demo/src/main/java/io/github/rosariopfernandes/firebasecompose/ui/components/OnlyText.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.ui.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.text.style.TextAlign 12 | import io.github.rosariopfernandes.firebasecompose.ui.theme.purple500 13 | 14 | @Composable 15 | fun OnlyText( 16 | title: String, 17 | message: String 18 | ) { 19 | Column( 20 | horizontalAlignment = Alignment.CenterHorizontally, 21 | verticalArrangement = Arrangement.Center, 22 | modifier = Modifier.fillMaxSize() 23 | ) { 24 | Text(text = title, style = MaterialTheme.typography.h4, color = purple500) 25 | Text(text = message, textAlign = TextAlign.Center) 26 | } 27 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/rosariopfernandes/firebasecompose/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val purple200 = Color(0xFFBB86FC) 6 | val purple500 = Color(0xFF6200EE) 7 | val purple700 = Color(0xFF3700B3) 8 | val teal200 = Color(0xFF03DAC5) -------------------------------------------------------------------------------- /demo/src/main/java/io/github/rosariopfernandes/firebasecompose/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /demo/src/main/java/io/github/rosariopfernandes/firebasecompose/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | 9 | private val DarkColorPalette = darkColors( 10 | primary = purple200, 11 | primaryVariant = purple700, 12 | secondary = teal200 13 | ) 14 | 15 | private val LightColorPalette = lightColors( 16 | primary = purple500, 17 | primaryVariant = purple700, 18 | secondary = teal200 19 | 20 | /* Other default colors to override 21 | background = Color.White, 22 | surface = Color.White, 23 | onPrimary = Color.White, 24 | onSecondary = Color.Black, 25 | onBackground = Color.Black, 26 | onSurface = Color.Black, 27 | */ 28 | ) 29 | 30 | @Composable 31 | fun FirebaseComposeTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { 32 | val colors = if (darkTheme) { 33 | DarkColorPalette 34 | } else { 35 | LightColorPalette 36 | } 37 | 38 | MaterialTheme( 39 | colors = colors, 40 | typography = typography, 41 | shapes = shapes, 42 | content = content 43 | ) 44 | } -------------------------------------------------------------------------------- /demo/src/main/java/io/github/rosariopfernandes/firebasecompose/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package io.github.rosariopfernandes.firebasecompose.ui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val typography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /demo/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatfiredev/firebase-compose/c6eebe2e6d0c9aeafed704a8634b94025e13da59/demo/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatfiredev/firebase-compose/c6eebe2e6d0c9aeafed704a8634b94025e13da59/demo/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatfiredev/firebase-compose/c6eebe2e6d0c9aeafed704a8634b94025e13da59/demo/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatfiredev/firebase-compose/c6eebe2e6d0c9aeafed704a8634b94025e13da59/demo/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatfiredev/firebase-compose/c6eebe2e6d0c9aeafed704a8634b94025e13da59/demo/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatfiredev/firebase-compose/c6eebe2e6d0c9aeafed704a8634b94025e13da59/demo/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatfiredev/firebase-compose/c6eebe2e6d0c9aeafed704a8634b94025e13da59/demo/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatfiredev/firebase-compose/c6eebe2e6d0c9aeafed704a8634b94025e13da59/demo/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatfiredev/firebase-compose/c6eebe2e6d0c9aeafed704a8634b94025e13da59/demo/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thatfiredev/firebase-compose/c6eebe2e6d0c9aeafed704a8634b94025e13da59/demo/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /demo/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /demo/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /demo/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Firebase Compose 3 | Navigate Up 4 | Details 5 | Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud.. 6 | Ingredients 7 | Vanilla, Almond Flour, Eggs, Butter, Cream, Sugar 8 | 9 | An error occured :( 10 | No Snacks :/ 11 | You havent added any snacks yet. Press the top right button to add some snacks. 12 | -------------------------------------------------------------------------------- /demo/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 21 | 22 |