├── .github
├── ISSUE_TEMPLATE
│ └── bug_report.md
└── workflows
│ └── android.yml
├── .gitignore
├── .vscode
└── settings.json
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle.kts
├── lint.xml
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── google
│ │ └── firebase
│ │ └── example
│ │ └── fireeats
│ │ ├── FilterDialogFragment.kt
│ │ ├── Filters.kt
│ │ ├── MainActivity.kt
│ │ ├── MainFragment.kt
│ │ ├── RatingDialogFragment.kt
│ │ ├── RestaurantDetailFragment.kt
│ │ ├── adapter
│ │ ├── FirestoreAdapter.kt
│ │ ├── RatingAdapter.kt
│ │ └── RestaurantAdapter.kt
│ │ ├── model
│ │ ├── Rating.kt
│ │ └── Restaurant.kt
│ │ ├── util
│ │ ├── AuthInitializer.kt
│ │ ├── FirestoreInitializer.kt
│ │ ├── RatingUtil.kt
│ │ └── RestaurantUtil.kt
│ │ └── viewmodel
│ │ └── MainActivityViewModel.kt
│ └── res
│ ├── drawable-hdpi
│ └── pizza_monster.png
│ ├── drawable-mdpi
│ └── pizza_monster.png
│ ├── drawable-nodpi
│ └── food_1.png
│ ├── drawable-xhdpi
│ └── pizza_monster.png
│ ├── drawable-xxhdpi
│ └── pizza_monster.png
│ ├── drawable
│ ├── bg_shadow.xml
│ ├── gradient_up.xml
│ ├── ic_add_white_24px.xml
│ ├── ic_arrow_back_white_24px.xml
│ ├── ic_close_white_24px.xml
│ ├── ic_fastfood_white_24dp.xml
│ ├── ic_filter_list_white_24px.xml
│ ├── ic_local_dining_white_24px.xml
│ ├── ic_monetization_on_white_24px.xml
│ ├── ic_place_white_24px.xml
│ ├── ic_restaurant_white_24px.xml
│ └── ic_sort_white_24px.xml
│ ├── layout
│ ├── activity_main.xml
│ ├── activity_restaurant_detail.xml
│ ├── dialog_filters.xml
│ ├── dialog_rating.xml
│ ├── fragment_main.xml
│ ├── fragment_restaurant_detail.xml
│ ├── item_rating.xml
│ └── item_restaurant.xml
│ ├── menu
│ └── menu_main.xml
│ ├── mipmap-hdpi
│ └── ic_launcher.png
│ ├── mipmap-mdpi
│ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ └── ic_launcher.png
│ ├── navigation
│ └── nav_graph.xml
│ ├── values-v21
│ └── styles.xml
│ ├── values
│ ├── colors.xml
│ ├── strings.xml
│ └── styles.xml
│ └── xml
│ └── network_security_config.xml
├── build.gradle.kts
├── build.sh
├── docs
└── home.png
├── firebase.json
├── firestore.indexes.json
├── firestore.rules
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── mock-google-services.json
├── settings.gradle.kts
└── steps
├── img
├── 1129441c6ebb5eaf.png
├── 4c68c6654f4168ad.png
├── 67898572a35672a5.png
├── 73d151ed16016421.png
├── 78fa16cdf8ef435a.png
├── 7a67a8a400c80c50.png
├── 95691e9b71ba55e3.png
├── 9d2f625aebcab6af.png
├── 9e45f40faefce5d0.png
├── a670188398c3c59.png
├── de06424023ffb4b9.png
├── emulators-auth.png
├── emulators-firebase.png
├── f9e670f40bd615b0.png
└── sign-in-providers.png
└── index.lab.md
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Describe a bug you encountered in the codelab
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
18 |
19 | ### Step 1: Describe your environment
20 |
21 | * Android device: _____
22 | * Android OS version: _____
23 |
24 | ### Step 2: Describe the problem:
25 |
26 | #### Steps to reproduce:
27 |
28 | 1. _____
29 | 2. _____
30 | 3. _____
31 |
32 | #### Observed Results:
33 |
34 | * What happened? This could be a description, `logcat` output, etc.
35 |
36 | #### Expected Results:
37 |
38 | * What did you expect to happen?
39 |
40 | #### Relevant Code:
41 |
42 | ```
43 | // TODO(you): code here to reproduce the problem
44 | ```
45 |
--------------------------------------------------------------------------------
/.github/workflows/android.yml:
--------------------------------------------------------------------------------
1 | name: Android CI
2 |
3 | on:
4 | - pull_request
5 | - push
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: set up JDK 17
13 | uses: actions/setup-java@v1
14 | with:
15 | java-version: 17
16 | - name: Build with Gradle
17 | run: ./build.sh
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | local.properties
3 | .idea
4 | build/
5 | .DS_Store
6 | *.iml
7 | *.apk
8 | *.aar
9 | *.zip
10 | google-services.json
11 |
12 | .project
13 | .settings
14 | .classpath
15 |
16 | .firebaserc
17 | *debug.log
18 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "java.configuration.updateBuildConfiguration": "disabled"
3 | }
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to friendlyeats-android
2 |
3 | We'd love for you to contribute to our source code and to make this project even better than it is today! Here are the guidelines we'd like you to follow:
4 |
5 | - [Code of Conduct](#coc)
6 | - [Question or Problem?](#question)
7 | - [Issues and Bugs](#issue)
8 | - [Feature Requests](#feature)
9 | - [Submission Guidelines](#submit)
10 | - [Coding Rules](#rules)
11 | - [Signing the CLA](#cla)
12 |
13 | ## Code of Conduct
14 |
15 | As contributors and maintainers of the friendlyeats-android project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
16 |
17 | Communication through any of Firebase's channels (GitHub, StackOverflow, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
18 |
19 | We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the project to do the same.
20 |
21 | If any member of the community violates this code of conduct, the maintainers of the friendlyeats-android project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
22 |
23 | If you are subject to or witness unacceptable behavior, or have any other concerns, please drop us a line at samstern@google.com.
24 |
25 | ## Got a Question or Problem?
26 |
27 | If you have questions about how to use the friendlyeats-android, please open a Github issue. If you need help debugging your own app, please please direct these to [StackOverflow][stackoverflow] and use the `firebase` tag.
28 |
29 | If you feel that we're missing an important bit of documentation, feel free to
30 | file an issue so we can help. Here's an example to get you started:
31 |
32 | ```
33 | What are you trying to do or find out more about?
34 |
35 | Where have you looked?
36 |
37 | Where did you expect to find this information?
38 | ```
39 |
40 | ## Found an Issue?
41 | If you find a bug in the source code or a mistake in the documentation, you can help us by
42 | submitting an issue. Even better you can submit a Pull Request with a fix.
43 |
44 | See [below](#submit) for some guidelines.
45 |
46 | ## Submission Guidelines
47 |
48 | ### Submitting an Issue
49 | Before you submit your issue search the archive, maybe your question was already answered.
50 |
51 | If your issue appears to be a bug, and hasn't been reported, open a new issue.
52 | Help us to maximize the effort we can spend fixing issues and adding new
53 | features, by not reporting duplicate issues. Providing the following information will increase the
54 | chances of your issue being dealt with quickly:
55 |
56 | * **Overview of the Issue** - if an error is being thrown a non-minified stack trace helps
57 | * **Motivation for or Use Case** - explain why this is a bug for you
58 | * **Environment** - is this a problem with all devices or only some?
59 | * **Reproduce the Error** - provide a live example or a unambiguous set of steps.
60 | * **Related Issues** - has a similar issue been reported before?
61 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
62 | causing the problem (line of code or commit)
63 |
64 | **If you get help, help others. Good karma rulez!**
65 |
66 | Here's a template to get you started:
67 |
68 | ```
69 | Device:
70 | Operating system version:
71 | Firebase SDK version:
72 |
73 | What steps will reproduce the problem:
74 | 1.
75 | 2.
76 | 3.
77 |
78 | What is the expected result?
79 |
80 | What happens instead of that?
81 |
82 | Please provide any other information below, and attach a screenshot if possible.
83 | ```
84 |
85 | ### Submitting a Pull Request
86 | Before you submit your pull request consider the following guidelines:
87 |
88 | * Search [GitHub](https://github.com/firebase/friendlyeats-android/pulls) for an open or closed Pull Request
89 | that relates to your submission. You don't want to duplicate effort.
90 | * Please sign our [Contributor License Agreement (CLA)](#cla) before sending pull
91 | requests. We cannot accept code without this.
92 | * Make your changes in a new git branch:
93 |
94 | ```shell
95 | git checkout -b my-fix-branch master
96 | ```
97 |
98 | * Create your patch, **including appropriate test cases**.
99 | * Avoid checking in files that shouldn't be tracked (e.g `.tmp`, `.idea`). We recommend using a [global](#global-gitignore) gitignore for this.
100 | * Commit your changes using a descriptive commit message.
101 |
102 | ```shell
103 | git commit -a
104 | ```
105 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
106 |
107 | * Build your changes locally to ensure all the tests pass:
108 |
109 | ```shell
110 | gulp
111 | ```
112 |
113 | * Push your branch to GitHub:
114 |
115 | ```shell
116 | git push origin my-fix-branch
117 | ```
118 |
119 | * In GitHub, send a pull request to `friendlyeats-android:master`.
120 | * If we suggest changes then:
121 | * Make the required updates.
122 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request):
123 |
124 | ```shell
125 | git rebase master -i
126 | git push origin my-fix-branch -f
127 | ```
128 |
129 | That's it! Thank you for your contribution!
130 |
131 | #### After your pull request is merged
132 |
133 | After your pull request is merged, you can safely delete your branch and pull the changes
134 | from the main (upstream) repository:
135 |
136 | * Delete the remote branch on GitHub either through the GitHub UI or your local shell as follows:
137 |
138 | ```shell
139 | git push origin --delete my-fix-branch
140 | ```
141 |
142 | * Check out the master branch:
143 |
144 | ```shell
145 | git checkout master -f
146 | ```
147 |
148 | * Delete the local branch:
149 |
150 | ```shell
151 | git branch -D my-fix-branch
152 | ```
153 |
154 | * Update your master with the latest upstream version:
155 |
156 | ```shell
157 | git pull --ff upstream master
158 | ```
159 |
160 | ## Signing the CLA
161 |
162 | Please sign our [Contributor License Agreement][google-cla] (CLA) before sending pull requests. For any code
163 | changes to be accepted, the CLA must be signed. It's a quick process, we promise!
164 |
165 | [github]: https://github.com/firebase/friendyeats-android
166 | [google-cla]: https://cla.developers.google.com
167 | [stackoverflow]: http://stackoverflow.com/questions/tagged/firebase
168 | [global-gitignore]: https://help.github.com/articles/ignoring-files/#create-a-global-gitignore
169 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Apache License
3 | Version 2.0, January 2004
4 | http://www.apache.org/licenses/
5 |
6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7 |
8 | 1. Definitions.
9 |
10 | "License" shall mean the terms and conditions for use, reproduction,
11 | and distribution as defined by Sections 1 through 9 of this document.
12 |
13 | "Licensor" shall mean the copyright owner or entity authorized by
14 | the copyright owner that is granting the License.
15 |
16 | "Legal Entity" shall mean the union of the acting entity and all
17 | other entities that control, are controlled by, or are under common
18 | control with that entity. For the purposes of this definition,
19 | "control" means (i) the power, direct or indirect, to cause the
20 | direction or management of such entity, whether by contract or
21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 | outstanding shares, or (iii) beneficial ownership of such entity.
23 |
24 | "You" (or "Your") shall mean an individual or Legal Entity
25 | exercising permissions granted by this License.
26 |
27 | "Source" form shall mean the preferred form for making modifications,
28 | including but not limited to software source code, documentation
29 | source, and configuration files.
30 |
31 | "Object" form shall mean any form resulting from mechanical
32 | transformation or translation of a Source form, including but
33 | not limited to compiled object code, generated documentation,
34 | and conversions to other media types.
35 |
36 | "Work" shall mean the work of authorship, whether in Source or
37 | Object form, made available under the License, as indicated by a
38 | copyright notice that is included in or attached to the work
39 | (an example is provided in the Appendix below).
40 |
41 | "Derivative Works" shall mean any work, whether in Source or Object
42 | form, that is based on (or derived from) the Work and for which the
43 | editorial revisions, annotations, elaborations, or other modifications
44 | represent, as a whole, an original work of authorship. For the purposes
45 | of this License, Derivative Works shall not include works that remain
46 | separable from, or merely link (or bind by name) to the interfaces of,
47 | the Work and Derivative Works thereof.
48 |
49 | "Contribution" shall mean any work of authorship, including
50 | the original version of the Work and any modifications or additions
51 | to that Work or Derivative Works thereof, that is intentionally
52 | submitted to Licensor for inclusion in the Work by the copyright owner
53 | or by an individual or Legal Entity authorized to submit on behalf of
54 | the copyright owner. For the purposes of this definition, "submitted"
55 | means any form of electronic, verbal, or written communication sent
56 | to the Licensor or its representatives, including but not limited to
57 | communication on electronic mailing lists, source code control systems,
58 | and issue tracking systems that are managed by, or on behalf of, the
59 | Licensor for the purpose of discussing and improving the Work, but
60 | excluding communication that is conspicuously marked or otherwise
61 | designated in writing by the copyright owner as "Not a Contribution."
62 |
63 | "Contributor" shall mean Licensor and any individual or Legal Entity
64 | on behalf of whom a Contribution has been received by Licensor and
65 | subsequently incorporated within the Work.
66 |
67 | 2. Grant of Copyright License. Subject to the terms and conditions of
68 | this License, each Contributor hereby grants to You a perpetual,
69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 | copyright license to reproduce, prepare Derivative Works of,
71 | publicly display, publicly perform, sublicense, and distribute the
72 | Work and such Derivative Works in Source or Object form.
73 |
74 | 3. Grant of Patent License. Subject to the terms and conditions of
75 | this License, each Contributor hereby grants to You a perpetual,
76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 | (except as stated in this section) patent license to make, have made,
78 | use, offer to sell, sell, import, and otherwise transfer the Work,
79 | where such license applies only to those patent claims licensable
80 | by such Contributor that are necessarily infringed by their
81 | Contribution(s) alone or by combination of their Contribution(s)
82 | with the Work to which such Contribution(s) was submitted. If You
83 | institute patent litigation against any entity (including a
84 | cross-claim or counterclaim in a lawsuit) alleging that the Work
85 | or a Contribution incorporated within the Work constitutes direct
86 | or contributory patent infringement, then any patent licenses
87 | granted to You under this License for that Work shall terminate
88 | as of the date such litigation is filed.
89 |
90 | 4. Redistribution. You may reproduce and distribute copies of the
91 | Work or Derivative Works thereof in any medium, with or without
92 | modifications, and in Source or Object form, provided that You
93 | meet the following conditions:
94 |
95 | (a) You must give any other recipients of the Work or
96 | Derivative Works a copy of this License; and
97 |
98 | (b) You must cause any modified files to carry prominent notices
99 | stating that You changed the files; and
100 |
101 | (c) You must retain, in the Source form of any Derivative Works
102 | that You distribute, all copyright, patent, trademark, and
103 | attribution notices from the Source form of the Work,
104 | excluding those notices that do not pertain to any part of
105 | the Derivative Works; and
106 |
107 | (d) If the Work includes a "NOTICE" text file as part of its
108 | distribution, then any Derivative Works that You distribute must
109 | include a readable copy of the attribution notices contained
110 | within such NOTICE file, excluding those notices that do not
111 | pertain to any part of the Derivative Works, in at least one
112 | of the following places: within a NOTICE text file distributed
113 | as part of the Derivative Works; within the Source form or
114 | documentation, if provided along with the Derivative Works; or,
115 | within a display generated by the Derivative Works, if and
116 | wherever such third-party notices normally appear. The contents
117 | of the NOTICE file are for informational purposes only and
118 | do not modify the License. You may add Your own attribution
119 | notices within Derivative Works that You distribute, alongside
120 | or as an addendum to the NOTICE text from the Work, provided
121 | that such additional attribution notices cannot be construed
122 | as modifying the License.
123 |
124 | You may add Your own copyright statement to Your modifications and
125 | may provide additional or different license terms and conditions
126 | for use, reproduction, or distribution of Your modifications, or
127 | for any such Derivative Works as a whole, provided Your use,
128 | reproduction, and distribution of the Work otherwise complies with
129 | the conditions stated in this License.
130 |
131 | 5. Submission of Contributions. Unless You explicitly state otherwise,
132 | any Contribution intentionally submitted for inclusion in the Work
133 | by You to the Licensor shall be under the terms and conditions of
134 | this License, without any additional terms or conditions.
135 | Notwithstanding the above, nothing herein shall supersede or modify
136 | the terms of any separate license agreement you may have executed
137 | with Licensor regarding such Contributions.
138 |
139 | 6. Trademarks. This License does not grant permission to use the trade
140 | names, trademarks, service marks, or product names of the Licensor,
141 | except as required for reasonable and customary use in describing the
142 | origin of the Work and reproducing the content of the NOTICE file.
143 |
144 | 7. Disclaimer of Warranty. Unless required by applicable law or
145 | agreed to in writing, Licensor provides the Work (and each
146 | Contributor provides its Contributions) on an "AS IS" BASIS,
147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 | implied, including, without limitation, any warranties or conditions
149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 | PARTICULAR PURPOSE. You are solely responsible for determining the
151 | appropriateness of using or redistributing the Work and assume any
152 | risks associated with Your exercise of permissions under this License.
153 |
154 | 8. Limitation of Liability. In no event and under no legal theory,
155 | whether in tort (including negligence), contract, or otherwise,
156 | unless required by applicable law (such as deliberate and grossly
157 | negligent acts) or agreed to in writing, shall any Contributor be
158 | liable to You for damages, including any direct, indirect, special,
159 | incidental, or consequential damages of any character arising as a
160 | result of this License or out of the use or inability to use the
161 | Work (including but not limited to damages for loss of goodwill,
162 | work stoppage, computer failure or malfunction, or any and all
163 | other commercial damages or losses), even if such Contributor
164 | has been advised of the possibility of such damages.
165 |
166 | 9. Accepting Warranty or Additional Liability. While redistributing
167 | the Work or Derivative Works thereof, You may choose to offer,
168 | and charge a fee for, acceptance of support, warranty, indemnity,
169 | or other liability obligations and/or rights consistent with this
170 | License. However, in accepting such obligations, You may act only
171 | on Your own behalf and on Your sole responsibility, not on behalf
172 | of any other Contributor, and only if You agree to indemnify,
173 | defend, and hold each Contributor harmless for any liability
174 | incurred by, or claims asserted against, such Contributor by reason
175 | of your accepting any such warranty or additional liability.
176 |
177 | END OF TERMS AND CONDITIONS
178 |
179 | APPENDIX: How to apply the Apache License to your work.
180 |
181 | To apply the Apache License to your work, attach the following
182 | boilerplate notice, with the fields enclosed by brackets "[]"
183 | replaced with your own identifying information. (Don't include
184 | the brackets!) The text should be enclosed in the appropriate
185 | comment syntax for the file format. We also recommend that a
186 | file or class name and description of purpose be included on the
187 | same "printed page" as the copyright notice for easier
188 | identification within third-party archives.
189 |
190 | Copyright 2017 Google Inc
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
204 | All code in any directories or sub-directories that end with *.html or
205 | *.css is licensed under the Creative Commons Attribution International
206 | 4.0 License, which full text can be found here:
207 | https://creativecommons.org/licenses/by/4.0/legalcode.
208 |
209 | As an exception to this license, all html or css that is generated by
210 | the software at the direction of the user is copyright the user. The
211 | user has full ownership and control over such content, including
212 | whether and how they wish to license it.
213 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Friendly Eats
2 |
3 |
4 |
5 | This is the source code that accompanies the Firestore Android Codelab:
6 | https://codelabs.developers.google.com/codelabs/firestore-android
7 |
8 | The codelab will walk you through developing an Android restaurant recommendation
9 | app powered by Cloud Firestore.
10 |
11 |
12 |
13 | If you don't want to do the codelab and would rather view the completed
14 | sample code, see the Firebase Android Quickstart repository:
15 | https://github.com/firebase/quickstart-android
16 |
17 | ## Build Status
18 |
19 | [![Actions Status][gh-actions-badge]][gh-actions]
20 |
21 | [gh-actions]: https://github.com/firebase/friendlyeats-android/actions
22 | [gh-actions-badge]: https://github.com/firebase/friendlyeats-android/workflows/Android%20CI/badge.svg
23 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("com.android.application")
3 | id("kotlin-android")
4 | id("com.google.gms.google-services")
5 | id("androidx.navigation.safeargs")
6 | }
7 |
8 | android {
9 | namespace = "com.google.firebase.example.fireeats"
10 | compileSdk = 36
11 | defaultConfig {
12 | applicationId = "com.google.firebase.example.fireeats"
13 | minSdk = 23
14 | targetSdk = 36
15 | versionCode = 1
16 | versionName = "1.0"
17 |
18 | multiDexEnabled = true
19 | vectorDrawables.useSupportLibrary = true
20 | }
21 | buildTypes {
22 | getByName("release") {
23 | isMinifyEnabled = false
24 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
25 | }
26 | }
27 | buildFeatures {
28 | viewBinding = true
29 | }
30 | lint {
31 | // TODO(thatfiredev): Remove this once
32 | // https://github.com/bumptech/glide/issues/4940 is fixed
33 | disable.add("NotificationPermission")
34 | }
35 | compileOptions {
36 | sourceCompatibility = JavaVersion.VERSION_17
37 | targetCompatibility = JavaVersion.VERSION_17
38 | }
39 | kotlinOptions {
40 | jvmTarget = "17"
41 | }
42 | }
43 |
44 | dependencies {
45 | // Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom)
46 | implementation(platform("com.google.firebase:firebase-bom:33.14.0"))
47 |
48 | // Firestore
49 | implementation("com.google.firebase:firebase-firestore-ktx")
50 |
51 | // Other Firebase/Play services deps
52 | implementation("com.google.firebase:firebase-auth-ktx")
53 |
54 | // Pinned to 20.7.0 as a workaround for issue https://github.com/firebase/quickstart-android/issues/1647
55 | implementation("com.google.android.gms:play-services-auth:20.7.0")
56 |
57 | // FirebaseUI (for authentication)
58 | implementation("com.firebaseui:firebase-ui-auth:9.0.0")
59 |
60 | // Support Libs
61 | implementation("androidx.appcompat:appcompat:1.7.1")
62 | implementation("androidx.vectordrawable:vectordrawable-animated:1.2.0")
63 | implementation("androidx.cardview:cardview:1.0.0")
64 | implementation("androidx.browser:browser:1.5.0")
65 | implementation("com.google.android.material:material:1.12.0")
66 | implementation("androidx.multidex:multidex:2.0.1")
67 | implementation("androidx.recyclerview:recyclerview:1.4.0")
68 | implementation("androidx.navigation:navigation-fragment-ktx:2.9.0")
69 | implementation("androidx.navigation:navigation-ui-ktx:2.9.0")
70 | implementation("androidx.startup:startup-runtime:1.2.0")
71 |
72 | // Android architecture components
73 | implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.1")
74 | implementation("androidx.lifecycle:lifecycle-extensions:2.2.0")
75 | annotationProcessor("androidx.lifecycle:lifecycle-compiler:2.9.1")
76 |
77 | // Third-party libraries
78 | implementation("me.zhanghai.android.materialratingbar:library:1.4.0")
79 | implementation("com.github.bumptech.glide:glide:4.16.0")
80 | }
81 |
--------------------------------------------------------------------------------
/app/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/google/home/samstern/android-sdk-linux/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.kts.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
13 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
27 |
29 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/FilterDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.DialogFragment
9 | import com.google.firebase.example.fireeats.databinding.DialogFiltersBinding
10 | import com.google.firebase.example.fireeats.model.Restaurant
11 | import com.google.firebase.firestore.Query
12 |
13 | /**
14 | * Dialog Fragment containing filter form.
15 | */
16 | class FilterDialogFragment : DialogFragment() {
17 |
18 | private var _binding: DialogFiltersBinding? = null
19 | private val binding get() = _binding!!
20 | private var filterListener: FilterListener? = null
21 |
22 | private val selectedCategory: String?
23 | get() {
24 | val selected = binding.spinnerCategory.selectedItem as String
25 | return if (getString(R.string.value_any_category) == selected) {
26 | null
27 | } else {
28 | selected
29 | }
30 | }
31 |
32 | private val selectedCity: String?
33 | get() {
34 | val selected = binding.spinnerCity.selectedItem as String
35 | return if (getString(R.string.value_any_city) == selected) {
36 | null
37 | } else {
38 | selected
39 | }
40 | }
41 |
42 | private val selectedPrice: Int
43 | get() {
44 | val selected = binding.spinnerPrice.selectedItem as String
45 | return when (selected) {
46 | getString(R.string.price_1) -> 1
47 | getString(R.string.price_2) -> 2
48 | getString(R.string.price_3) -> 3
49 | else -> -1
50 | }
51 | }
52 |
53 | private val selectedSortBy: String?
54 | get() {
55 | val selected = binding.spinnerSort.selectedItem as String
56 | if (getString(R.string.sort_by_rating) == selected) {
57 | return Restaurant.FIELD_AVG_RATING
58 | }
59 | if (getString(R.string.sort_by_price) == selected) {
60 | return Restaurant.FIELD_PRICE
61 | }
62 | return if (getString(R.string.sort_by_popularity) == selected) {
63 | Restaurant.FIELD_POPULARITY
64 | } else {
65 | null
66 | }
67 | }
68 |
69 | private val sortDirection: Query.Direction
70 | get() {
71 | val selected = binding.spinnerSort.selectedItem as String
72 | if (getString(R.string.sort_by_rating) == selected) {
73 | return Query.Direction.DESCENDING
74 | }
75 | if (getString(R.string.sort_by_price) == selected) {
76 | return Query.Direction.ASCENDING
77 | }
78 | return if (getString(R.string.sort_by_popularity) == selected) {
79 | Query.Direction.DESCENDING
80 | } else {
81 | Query.Direction.DESCENDING
82 | }
83 | }
84 |
85 | val filters: Filters
86 | get() {
87 | val filters = Filters()
88 |
89 | filters.category = selectedCategory
90 | filters.city = selectedCity
91 | filters.price = selectedPrice
92 | filters.sortBy = selectedSortBy
93 | filters.sortDirection = sortDirection
94 |
95 | return filters
96 | }
97 |
98 | interface FilterListener {
99 |
100 | fun onFilter(filters: Filters)
101 | }
102 |
103 | override fun onCreateView(
104 | inflater: LayoutInflater,
105 | container: ViewGroup?,
106 | savedInstanceState: Bundle?
107 | ): View? {
108 | _binding = DialogFiltersBinding.inflate(inflater, container, false)
109 |
110 | binding.buttonSearch.setOnClickListener { onSearchClicked() }
111 | binding.buttonCancel.setOnClickListener { onCancelClicked() }
112 |
113 | return binding.root
114 | }
115 |
116 | override fun onDestroyView() {
117 | super.onDestroyView()
118 | _binding = null
119 | }
120 |
121 | override fun onAttach(context: Context) {
122 | super.onAttach(context)
123 |
124 | if (parentFragment is FilterListener) {
125 | filterListener = parentFragment as FilterListener
126 | }
127 | }
128 |
129 | override fun onResume() {
130 | super.onResume()
131 | dialog?.window?.setLayout(
132 | ViewGroup.LayoutParams.MATCH_PARENT,
133 | ViewGroup.LayoutParams.WRAP_CONTENT)
134 | }
135 |
136 | private fun onSearchClicked() {
137 | filterListener?.onFilter(filters)
138 | dismiss()
139 | }
140 |
141 | private fun onCancelClicked() {
142 | dismiss()
143 | }
144 |
145 | fun resetFilters() {
146 | _binding?.let {
147 | it.spinnerCategory.setSelection(0)
148 | it.spinnerCity.setSelection(0)
149 | it.spinnerPrice.setSelection(0)
150 | it.spinnerSort.setSelection(0)
151 | }
152 | }
153 |
154 | companion object {
155 |
156 | const val TAG = "FilterDialog"
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/Filters.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats
2 |
3 | import android.content.Context
4 | import android.text.TextUtils
5 | import com.google.firebase.example.fireeats.model.Restaurant
6 | import com.google.firebase.example.fireeats.util.RestaurantUtil
7 | import com.google.firebase.firestore.Query
8 |
9 | /**
10 | * Object for passing filters around.
11 | */
12 | class Filters {
13 |
14 | var category: String? = null
15 | var city: String? = null
16 | var price = -1
17 | var sortBy: String? = null
18 | var sortDirection: Query.Direction = Query.Direction.DESCENDING
19 |
20 | fun hasCategory(): Boolean {
21 | return !TextUtils.isEmpty(category)
22 | }
23 |
24 | fun hasCity(): Boolean {
25 | return !TextUtils.isEmpty(city)
26 | }
27 |
28 | fun hasPrice(): Boolean {
29 | return price > 0
30 | }
31 |
32 | fun hasSortBy(): Boolean {
33 | return !TextUtils.isEmpty(sortBy)
34 | }
35 |
36 | fun getSearchDescription(context: Context): String {
37 | val desc = StringBuilder()
38 |
39 | if (category == null && city == null) {
40 | desc.append("")
41 | desc.append(context.getString(R.string.all_restaurants))
42 | desc.append("")
43 | }
44 |
45 | if (category != null) {
46 | desc.append("")
47 | desc.append(category)
48 | desc.append("")
49 | }
50 |
51 | if (category != null && city != null) {
52 | desc.append(" in ")
53 | }
54 |
55 | if (city != null) {
56 | desc.append("")
57 | desc.append(city)
58 | desc.append("")
59 | }
60 |
61 | if (price > 0) {
62 | desc.append(" for ")
63 | desc.append("")
64 | desc.append(RestaurantUtil.getPriceString(price))
65 | desc.append("")
66 | }
67 |
68 | return desc.toString()
69 | }
70 |
71 | fun getOrderDescription(context: Context): String {
72 | return when (sortBy) {
73 | Restaurant.FIELD_PRICE -> context.getString(R.string.sorted_by_price)
74 | Restaurant.FIELD_POPULARITY -> context.getString(R.string.sorted_by_popularity)
75 | else -> context.getString(R.string.sorted_by_rating)
76 | }
77 | }
78 |
79 | companion object {
80 |
81 | val default: Filters
82 | get() {
83 | val filters = Filters()
84 | filters.sortBy = Restaurant.FIELD_AVG_RATING
85 | filters.sortDirection = Query.Direction.DESCENDING
86 |
87 | return filters
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.navigation.Navigation
6 |
7 | class MainActivity : AppCompatActivity() {
8 | override fun onCreate(savedInstanceState: Bundle?) {
9 | super.onCreate(savedInstanceState)
10 | setContentView(R.layout.activity_main)
11 | setSupportActionBar(findViewById(R.id.toolbar))
12 | Navigation.findNavController(this, R.id.nav_host_fragment)
13 | .setGraph(R.navigation.nav_graph)
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/MainFragment.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats
2 |
3 | import android.app.Activity
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.Menu
7 | import android.view.MenuInflater
8 | import android.view.MenuItem
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import android.widget.Toast
12 | import androidx.annotation.StringRes
13 | import androidx.appcompat.app.AlertDialog
14 | import androidx.core.text.HtmlCompat
15 | import androidx.fragment.app.Fragment
16 | import androidx.lifecycle.ViewModelProvider
17 | import androidx.navigation.fragment.findNavController
18 | import androidx.recyclerview.widget.LinearLayoutManager
19 | import com.firebase.ui.auth.AuthUI
20 | import com.firebase.ui.auth.ErrorCodes
21 | import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract
22 | import com.firebase.ui.auth.data.model.FirebaseAuthUIAuthenticationResult
23 | import com.google.android.material.snackbar.Snackbar
24 | import com.google.firebase.auth.ktx.auth
25 | import com.google.firebase.example.fireeats.databinding.FragmentMainBinding
26 | import com.google.firebase.example.fireeats.adapter.RestaurantAdapter
27 | import com.google.firebase.example.fireeats.model.Restaurant
28 | import com.google.firebase.example.fireeats.viewmodel.MainActivityViewModel
29 | import com.google.firebase.firestore.DocumentSnapshot
30 | import com.google.firebase.firestore.FirebaseFirestore
31 | import com.google.firebase.firestore.FirebaseFirestoreException
32 | import com.google.firebase.firestore.Query
33 | import com.google.firebase.firestore.ktx.firestore
34 | import com.google.firebase.ktx.Firebase
35 |
36 | class MainFragment : Fragment(),
37 | FilterDialogFragment.FilterListener,
38 | RestaurantAdapter.OnRestaurantSelectedListener {
39 |
40 | lateinit var firestore: FirebaseFirestore
41 | private var query: Query? = null
42 |
43 | private lateinit var binding: FragmentMainBinding
44 | private lateinit var filterDialog: FilterDialogFragment
45 | private var adapter: RestaurantAdapter? = null
46 |
47 | private lateinit var viewModel: MainActivityViewModel
48 | private val signInLauncher = registerForActivityResult(
49 | FirebaseAuthUIActivityResultContract()
50 | ) { result -> this.onSignInResult(result) }
51 |
52 | override fun onCreateView(
53 | inflater: LayoutInflater,
54 | container: ViewGroup?,
55 | savedInstanceState: Bundle?
56 | ): View {
57 | setHasOptionsMenu(true)
58 | binding = FragmentMainBinding.inflate(inflater, container, false);
59 | return binding.root;
60 | }
61 |
62 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
63 | super.onViewCreated(view, savedInstanceState)
64 |
65 | // View model
66 | viewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
67 |
68 | // Enable Firestore logging
69 | FirebaseFirestore.setLoggingEnabled(true)
70 |
71 | // Firestore
72 | firestore = Firebase.firestore
73 |
74 | // RecyclerView
75 | query?.let {
76 | adapter = object : RestaurantAdapter(it, this@MainFragment) {
77 | override fun onDataChanged() {
78 | // Show/hide content if the query returns empty.
79 | if (itemCount == 0) {
80 | binding.recyclerRestaurants.visibility = View.GONE
81 | binding.viewEmpty.visibility = View.VISIBLE
82 | } else {
83 | binding.recyclerRestaurants.visibility = View.VISIBLE
84 | binding.viewEmpty.visibility = View.GONE
85 | }
86 | }
87 |
88 | override fun onError(e: FirebaseFirestoreException) {
89 | // Show a snackbar on errors
90 | Snackbar.make(
91 | binding.root,
92 | "Error: check logs for info.", Snackbar.LENGTH_LONG
93 | ).show()
94 | }
95 | }
96 | binding.recyclerRestaurants.adapter = adapter
97 | }
98 |
99 | binding.recyclerRestaurants.layoutManager = LinearLayoutManager(context)
100 |
101 | // Filter Dialog
102 | filterDialog = FilterDialogFragment()
103 |
104 | binding.filterBar.setOnClickListener { onFilterClicked() }
105 | binding.buttonClearFilter.setOnClickListener { onClearFilterClicked() }
106 | }
107 |
108 | override fun onStart() {
109 | super.onStart()
110 |
111 | // Start sign in if necessary
112 | if (shouldStartSignIn()) {
113 | startSignIn()
114 | return
115 | }
116 |
117 | // Apply filters
118 | onFilter(viewModel.filters)
119 |
120 | // Start listening for Firestore updates
121 | adapter?.startListening()
122 | }
123 |
124 | override fun onStop() {
125 | super.onStop()
126 | adapter?.stopListening()
127 | }
128 |
129 | override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
130 | inflater.inflate(R.menu.menu_main, menu)
131 | return super.onCreateOptionsMenu(menu, inflater)
132 | }
133 |
134 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
135 | when (item.itemId) {
136 | R.id.menu_add_items -> onAddItemsClicked()
137 | R.id.menu_sign_out -> {
138 | AuthUI.getInstance().signOut(requireContext())
139 | startSignIn()
140 | }
141 | }
142 | return super.onOptionsItemSelected(item)
143 | }
144 |
145 | private fun onSignInResult(result: FirebaseAuthUIAuthenticationResult) {
146 | val response = result.idpResponse
147 | viewModel.isSigningIn = false
148 |
149 | if (result.resultCode != Activity.RESULT_OK) {
150 | if (response == null) {
151 | // User pressed the back button.
152 | requireActivity().finish()
153 | } else if (response.error != null && response.error!!.errorCode == ErrorCodes.NO_NETWORK) {
154 | showSignInErrorDialog(R.string.message_no_network)
155 | } else {
156 | showSignInErrorDialog(R.string.message_unknown)
157 | }
158 | }
159 | }
160 |
161 | private fun onFilterClicked() {
162 | // Show the dialog containing filter options
163 | filterDialog.show(childFragmentManager, FilterDialogFragment.TAG)
164 | }
165 |
166 | private fun onClearFilterClicked() {
167 | filterDialog.resetFilters()
168 |
169 | onFilter(Filters.default)
170 | }
171 |
172 | override fun onRestaurantSelected(restaurant: DocumentSnapshot) {
173 | // Go to the details page for the selected restaurant
174 | val action = MainFragmentDirections
175 | .actionMainFragmentToRestaurantDetailFragment(restaurant.id)
176 |
177 | findNavController().navigate(action)
178 | }
179 |
180 | override fun onFilter(filters: Filters) {
181 | // TODO(developer): Construct new query
182 |
183 | // Set header
184 | binding.textCurrentSearch.text = HtmlCompat.fromHtml(
185 | filters.getSearchDescription(requireContext()),
186 | HtmlCompat.FROM_HTML_MODE_LEGACY
187 | )
188 | binding.textCurrentSortBy.text = filters.getOrderDescription(requireContext())
189 |
190 | // Save filters
191 | viewModel.filters = filters
192 | }
193 |
194 | private fun shouldStartSignIn(): Boolean {
195 | return !viewModel.isSigningIn && Firebase.auth.currentUser == null
196 | }
197 |
198 | private fun startSignIn() {
199 | // Sign in with FirebaseUI
200 | val intent = AuthUI.getInstance().createSignInIntentBuilder()
201 | .setAvailableProviders(listOf(AuthUI.IdpConfig.EmailBuilder().build()))
202 | .setCredentialManagerEnabled(false)
203 | .build()
204 |
205 | signInLauncher.launch(intent)
206 | viewModel.isSigningIn = true
207 | }
208 |
209 | private fun onAddItemsClicked() {
210 | // TODO(developer): Add random restaurants
211 | showTodoToast()
212 | }
213 |
214 | private fun showSignInErrorDialog(@StringRes message: Int) {
215 | val dialog = AlertDialog.Builder(requireContext())
216 | .setTitle(R.string.title_sign_in_error)
217 | .setMessage(message)
218 | .setCancelable(false)
219 | .setPositiveButton(R.string.option_retry) { _, _ -> startSignIn() }
220 | .setNegativeButton(R.string.option_exit) { _, _ -> requireActivity().finish() }.create()
221 |
222 | dialog.show()
223 | }
224 |
225 | private fun showTodoToast() {
226 | Toast.makeText(context, "TODO: Implement", Toast.LENGTH_SHORT).show()
227 | }
228 |
229 | companion object {
230 |
231 | private const val TAG = "MainActivity"
232 |
233 | private const val LIMIT = 50
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/RatingDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.view.LayoutInflater
6 | import android.view.View
7 | import android.view.ViewGroup
8 | import androidx.fragment.app.DialogFragment
9 | import com.google.firebase.auth.ktx.auth
10 | import com.google.firebase.example.fireeats.databinding.DialogRatingBinding
11 | import com.google.firebase.example.fireeats.model.Rating
12 | import com.google.firebase.ktx.Firebase
13 |
14 | /**
15 | * Dialog Fragment containing rating form.
16 | */
17 | class RatingDialogFragment : DialogFragment() {
18 |
19 | private var _binding: DialogRatingBinding? = null
20 | private val binding get() = _binding!!
21 | private var ratingListener: RatingListener? = null
22 |
23 | internal interface RatingListener {
24 |
25 | fun onRating(rating: Rating)
26 | }
27 |
28 | override fun onCreateView(
29 | inflater: LayoutInflater,
30 | container: ViewGroup?,
31 | savedInstanceState: Bundle?
32 | ): View? {
33 | _binding = DialogRatingBinding.inflate(inflater, container, false)
34 |
35 | binding.restaurantFormButton.setOnClickListener { onSubmitClicked() }
36 | binding.restaurantFormCancel.setOnClickListener { onCancelClicked() }
37 |
38 | return binding.root
39 | }
40 |
41 | override fun onDestroyView() {
42 | super.onDestroyView()
43 | _binding = null
44 | }
45 |
46 | override fun onAttach(context: Context) {
47 | super.onAttach(context)
48 |
49 | if (parentFragment is RatingListener) {
50 | ratingListener = parentFragment as RatingListener
51 | }
52 | }
53 |
54 | override fun onResume() {
55 | super.onResume()
56 | dialog?.window?.setLayout(
57 | ViewGroup.LayoutParams.MATCH_PARENT,
58 | ViewGroup.LayoutParams.WRAP_CONTENT)
59 | }
60 |
61 | private fun onSubmitClicked() {
62 | val user = Firebase.auth.currentUser
63 | user?.let {
64 | val rating = Rating(
65 | it,
66 | binding.restaurantFormRating.rating.toDouble(),
67 | binding.restaurantFormText.text.toString())
68 |
69 | ratingListener?.onRating(rating)
70 | }
71 |
72 | dismiss()
73 | }
74 |
75 | private fun onCancelClicked() {
76 | dismiss()
77 | }
78 |
79 | companion object {
80 |
81 | const val TAG = "RatingDialog"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/RestaurantDetailFragment.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.util.Log
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import android.view.inputmethod.InputMethodManager
10 | import androidx.fragment.app.Fragment
11 | import androidx.recyclerview.widget.LinearLayoutManager
12 | import com.bumptech.glide.Glide
13 | import com.google.android.gms.tasks.Task
14 | import com.google.android.gms.tasks.Tasks
15 | import com.google.android.material.snackbar.Snackbar
16 | import com.google.firebase.example.fireeats.databinding.FragmentRestaurantDetailBinding
17 | import com.google.firebase.example.fireeats.adapter.RatingAdapter
18 | import com.google.firebase.example.fireeats.model.Rating
19 | import com.google.firebase.example.fireeats.model.Restaurant
20 | import com.google.firebase.example.fireeats.util.RestaurantUtil
21 | import com.google.firebase.firestore.DocumentReference
22 | import com.google.firebase.firestore.DocumentSnapshot
23 | import com.google.firebase.firestore.EventListener
24 | import com.google.firebase.firestore.FirebaseFirestore
25 | import com.google.firebase.firestore.FirebaseFirestoreException
26 | import com.google.firebase.firestore.ListenerRegistration
27 | import com.google.firebase.firestore.Query
28 | import com.google.firebase.firestore.ktx.firestore
29 | import com.google.firebase.firestore.ktx.toObject
30 | import com.google.firebase.ktx.Firebase
31 |
32 | class RestaurantDetailFragment : Fragment(),
33 | EventListener,
34 | RatingDialogFragment.RatingListener {
35 |
36 | private var ratingDialog: RatingDialogFragment? = null
37 |
38 | private lateinit var binding: FragmentRestaurantDetailBinding
39 | private lateinit var firestore: FirebaseFirestore
40 | private lateinit var restaurantRef: DocumentReference
41 | private lateinit var ratingAdapter: RatingAdapter
42 |
43 | private var restaurantRegistration: ListenerRegistration? = null
44 |
45 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
46 | binding = FragmentRestaurantDetailBinding.inflate(inflater, container, false)
47 | return binding.root
48 | }
49 |
50 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
51 | super.onViewCreated(view, savedInstanceState)
52 |
53 | // Get restaurant ID from extras
54 | val restaurantId = RestaurantDetailFragmentArgs.fromBundle(requireArguments()).keyRestaurantId
55 |
56 | // Initialize Firestore
57 | firestore = Firebase.firestore
58 |
59 | // Get reference to the restaurant
60 | restaurantRef = firestore.collection("restaurants").document(restaurantId)
61 |
62 | // Get ratings
63 | val ratingsQuery = restaurantRef
64 | .collection("ratings")
65 | .orderBy("timestamp", Query.Direction.DESCENDING)
66 | .limit(50)
67 |
68 | // RecyclerView
69 | ratingAdapter = object : RatingAdapter(ratingsQuery) {
70 | override fun onDataChanged() {
71 | if (itemCount == 0) {
72 | binding.recyclerRatings.visibility = View.GONE
73 | binding.viewEmptyRatings.visibility = View.VISIBLE
74 | } else {
75 | binding.recyclerRatings.visibility = View.VISIBLE
76 | binding.viewEmptyRatings.visibility = View.GONE
77 | }
78 | }
79 | }
80 | binding.recyclerRatings.layoutManager = LinearLayoutManager(context)
81 | binding.recyclerRatings.adapter = ratingAdapter
82 |
83 | ratingDialog = RatingDialogFragment()
84 |
85 | binding.restaurantButtonBack.setOnClickListener { onBackArrowClicked() }
86 | binding.fabShowRatingDialog.setOnClickListener { onAddRatingClicked() }
87 | }
88 |
89 | public override fun onStart() {
90 | super.onStart()
91 |
92 | ratingAdapter.startListening()
93 | restaurantRegistration = restaurantRef.addSnapshotListener(this)
94 | }
95 |
96 | public override fun onStop() {
97 | super.onStop()
98 |
99 | ratingAdapter.stopListening()
100 |
101 | restaurantRegistration?.remove()
102 | restaurantRegistration = null
103 | }
104 |
105 | /**
106 | * Listener for the Restaurant document ([.restaurantRef]).
107 | */
108 | override fun onEvent(snapshot: DocumentSnapshot?, e: FirebaseFirestoreException?) {
109 | if (e != null) {
110 | Log.w(TAG, "restaurant:onEvent", e)
111 | return
112 | }
113 |
114 | snapshot?.let {
115 | val restaurant = snapshot.toObject()
116 | if (restaurant != null) {
117 | onRestaurantLoaded(restaurant)
118 | }
119 | }
120 | }
121 |
122 | private fun onRestaurantLoaded(restaurant: Restaurant) {
123 | binding.restaurantName.text = restaurant.name
124 | binding.restaurantRating.rating = restaurant.avgRating.toFloat()
125 | binding.restaurantNumRatings.text = getString(R.string.fmt_num_ratings, restaurant.numRatings)
126 | binding.restaurantCity.text = restaurant.city
127 | binding.restaurantCategory.text = restaurant.category
128 | binding.restaurantPrice.text = RestaurantUtil.getPriceString(restaurant)
129 |
130 | // Background image
131 | Glide.with(binding.restaurantImage.context)
132 | .load(restaurant.photo)
133 | .into(binding.restaurantImage)
134 | }
135 |
136 | private fun onBackArrowClicked() {
137 | requireActivity().onBackPressed()
138 | }
139 |
140 | private fun onAddRatingClicked() {
141 | ratingDialog?.show(childFragmentManager, RatingDialogFragment.TAG)
142 | }
143 |
144 | override fun onRating(rating: Rating) {
145 | // In a transaction, add the new rating and update the aggregate totals
146 | addRating(restaurantRef, rating)
147 | .addOnSuccessListener(requireActivity()) {
148 | Log.d(TAG, "Rating added")
149 |
150 | // Hide keyboard and scroll to top
151 | hideKeyboard()
152 | binding.recyclerRatings.smoothScrollToPosition(0)
153 | }
154 | .addOnFailureListener(requireActivity()) { e ->
155 | Log.w(TAG, "Add rating failed", e)
156 |
157 | // Show failure message and hide keyboard
158 | hideKeyboard()
159 | Snackbar.make(
160 | requireView().findViewById(android.R.id.content), "Failed to add rating",
161 | Snackbar.LENGTH_SHORT).show()
162 | }
163 | }
164 |
165 | private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task {
166 | // TODO(developer): Implement
167 | return Tasks.forException(Exception("not yet implemented"))
168 | }
169 |
170 | private fun hideKeyboard() {
171 | val view = requireActivity().currentFocus
172 | if (view != null) {
173 | (requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
174 | .hideSoftInputFromWindow(view.windowToken, 0)
175 | }
176 | }
177 |
178 | companion object {
179 |
180 | private const val TAG = "RestaurantDetail"
181 |
182 | const val KEY_RESTAURANT_ID = "key_restaurant_id"
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/adapter/FirestoreAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats.adapter
2 |
3 | import androidx.recyclerview.widget.RecyclerView
4 | import android.util.Log
5 | import com.google.firebase.firestore.DocumentSnapshot
6 | import com.google.firebase.firestore.FirebaseFirestoreException
7 | import com.google.firebase.firestore.ListenerRegistration
8 | import com.google.firebase.firestore.Query
9 | import java.util.ArrayList
10 |
11 | /**
12 | * RecyclerView adapter for displaying the results of a Firestore [Query].
13 | *
14 | * Note that this class forgoes some efficiency to gain simplicity. For example, the result of
15 | * [DocumentSnapshot.toObject] is not cached so the same object may be deserialized
16 | * many times as the user scrolls.
17 | */
18 | abstract class FirestoreAdapter(private var query: Query) :
19 | RecyclerView.Adapter() {
20 |
21 | private var registration: ListenerRegistration? = null
22 |
23 | private val snapshots = ArrayList()
24 |
25 | fun startListening() {
26 | // TODO(developer): Implement
27 | }
28 |
29 | fun stopListening() {
30 | registration?.remove()
31 | registration = null
32 |
33 | snapshots.clear()
34 | notifyDataSetChanged()
35 | }
36 |
37 | fun setQuery(query: Query) {
38 | // Stop listening
39 | stopListening()
40 |
41 | // Clear existing data
42 | snapshots.clear()
43 | notifyDataSetChanged()
44 |
45 | // Listen to new query
46 | this.query = query
47 | startListening()
48 | }
49 |
50 | open fun onError(e: FirebaseFirestoreException) {
51 | Log.w(TAG, "onError", e)
52 | }
53 |
54 | open fun onDataChanged() {}
55 |
56 | override fun getItemCount(): Int {
57 | return snapshots.size
58 | }
59 |
60 | protected fun getSnapshot(index: Int): DocumentSnapshot {
61 | return snapshots[index]
62 | }
63 |
64 | companion object {
65 |
66 | private const val TAG = "FirestoreAdapter"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/adapter/RatingAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.RecyclerView
6 | import com.google.firebase.example.fireeats.databinding.ItemRatingBinding
7 | import com.google.firebase.example.fireeats.model.Rating
8 | import com.google.firebase.firestore.Query
9 | import com.google.firebase.firestore.ktx.toObject
10 | import java.text.SimpleDateFormat
11 | import java.util.Locale
12 |
13 | /**
14 | * RecyclerView adapter for a list of [Rating].
15 | */
16 | open class RatingAdapter(query: Query) : FirestoreAdapter(query) {
17 |
18 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
19 | return ViewHolder(ItemRatingBinding.inflate(LayoutInflater.from(parent.context), parent, false))
20 | }
21 |
22 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
23 | holder.bind(getSnapshot(position).toObject())
24 | }
25 |
26 | class ViewHolder(val binding: ItemRatingBinding) : RecyclerView.ViewHolder(binding.root) {
27 |
28 | fun bind(rating: Rating?) {
29 | if (rating == null) {
30 | return
31 | }
32 |
33 | binding.ratingItemName.text = rating.userName
34 | binding.ratingItemRating.rating = rating.rating.toFloat()
35 | binding.ratingItemText.text = rating.text
36 |
37 | if (rating.timestamp != null) {
38 | binding.ratingItemDate.text = FORMAT.format(rating.timestamp)
39 | }
40 | }
41 |
42 | companion object {
43 |
44 | private val FORMAT = SimpleDateFormat(
45 | "MM/dd/yyyy", Locale.US)
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/adapter/RestaurantAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats.adapter
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.RecyclerView
6 | import com.bumptech.glide.Glide
7 | import com.google.firebase.example.fireeats.R
8 | import com.google.firebase.example.fireeats.databinding.ItemRestaurantBinding
9 | import com.google.firebase.example.fireeats.model.Restaurant
10 | import com.google.firebase.example.fireeats.util.RestaurantUtil
11 | import com.google.firebase.firestore.DocumentSnapshot
12 | import com.google.firebase.firestore.Query
13 | import com.google.firebase.firestore.ktx.toObject
14 |
15 | /**
16 | * RecyclerView adapter for a list of Restaurants.
17 | */
18 | open class RestaurantAdapter(query: Query, private val listener: OnRestaurantSelectedListener) :
19 | FirestoreAdapter(query) {
20 |
21 | interface OnRestaurantSelectedListener {
22 |
23 | fun onRestaurantSelected(restaurant: DocumentSnapshot)
24 | }
25 |
26 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
27 | return ViewHolder(ItemRestaurantBinding.inflate(
28 | LayoutInflater.from(parent.context), parent, false))
29 | }
30 |
31 | override fun onBindViewHolder(holder: ViewHolder, position: Int) {
32 | holder.bind(getSnapshot(position), listener)
33 | }
34 |
35 | class ViewHolder(val binding: ItemRestaurantBinding) : RecyclerView.ViewHolder(binding.root) {
36 |
37 | fun bind(
38 | snapshot: DocumentSnapshot,
39 | listener: OnRestaurantSelectedListener?
40 | ) {
41 |
42 | val restaurant = snapshot.toObject()
43 | if (restaurant == null) {
44 | return
45 | }
46 |
47 | val resources = binding.root.resources
48 |
49 | // Load image
50 | Glide.with(binding.restaurantItemImage.context)
51 | .load(restaurant.photo)
52 | .into(binding.restaurantItemImage)
53 |
54 | val numRatings: Int = restaurant.numRatings
55 |
56 | binding.restaurantItemName.text = restaurant.name
57 | binding.restaurantItemRating.rating = restaurant.avgRating.toFloat()
58 | binding.restaurantItemCity.text = restaurant.city
59 | binding.restaurantItemCategory.text = restaurant.category
60 | binding.restaurantItemNumRatings.text = resources.getString(
61 | R.string.fmt_num_ratings,
62 | numRatings)
63 | binding.restaurantItemPrice.text = RestaurantUtil.getPriceString(restaurant)
64 |
65 | // Click listener
66 | binding.root.setOnClickListener {
67 | listener?.onRestaurantSelected(snapshot)
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/model/Rating.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats.model
2 |
3 | import android.text.TextUtils
4 | import com.google.firebase.auth.FirebaseUser
5 | import com.google.firebase.firestore.ServerTimestamp
6 | import java.util.Date
7 |
8 | /**
9 | * Model POJO for a rating.
10 | */
11 | data class Rating(
12 | var userId: String? = null,
13 | var userName: String? = null,
14 | var rating: Double = 0.toDouble(),
15 | var text: String? = null,
16 | @ServerTimestamp var timestamp: Date? = null
17 | ) {
18 |
19 | constructor(user: FirebaseUser, rating: Double, text: String) : this() {
20 | this.userId = user.uid
21 | this.userName = user.displayName
22 | if (TextUtils.isEmpty(this.userName)) {
23 | this.userName = user.email
24 | }
25 |
26 | this.rating = rating
27 | this.text = text
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/model/Restaurant.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats.model
2 |
3 | import com.google.firebase.firestore.IgnoreExtraProperties
4 |
5 | /**
6 | * Restaurant POJO.
7 | */
8 | @IgnoreExtraProperties
9 | data class Restaurant(
10 | var name: String? = null,
11 | var city: String? = null,
12 | var category: String? = null,
13 | var photo: String? = null,
14 | var price: Int = 0,
15 | var numRatings: Int = 0,
16 | var avgRating: Double = 0.toDouble()
17 | ) {
18 |
19 | companion object {
20 |
21 | const val FIELD_CITY = "city"
22 | const val FIELD_CATEGORY = "category"
23 | const val FIELD_PRICE = "price"
24 | const val FIELD_POPULARITY = "numRatings"
25 | const val FIELD_AVG_RATING = "avgRating"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/util/AuthInitializer.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats.util
2 |
3 | import android.content.Context
4 | import androidx.startup.Initializer
5 | import com.google.firebase.auth.FirebaseAuth
6 | import com.google.firebase.auth.ktx.auth
7 | import com.google.firebase.example.fireeats.BuildConfig
8 | import com.google.firebase.ktx.Firebase
9 |
10 | class AuthInitializer : Initializer {
11 | // The host '10.0.2.2' is a special IP address to let the
12 | // Android emulator connect to 'localhost'.
13 | private val AUTH_EMULATOR_HOST = "10.0.2.2"
14 | private val AUTH_EMULATOR_PORT = 9099
15 |
16 | override fun create(context: Context): FirebaseAuth {
17 | val firebaseAuth = Firebase.auth
18 | // Use emulators only in debug builds
19 | if (BuildConfig.DEBUG) {
20 | firebaseAuth.useEmulator(AUTH_EMULATOR_HOST, AUTH_EMULATOR_PORT)
21 | }
22 | return firebaseAuth
23 | }
24 |
25 | // No dependencies on other libraries
26 | override fun dependencies(): MutableList>> = mutableListOf()
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/util/FirestoreInitializer.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats.util
2 |
3 | import android.content.Context
4 | import androidx.startup.Initializer
5 | import com.google.firebase.example.fireeats.BuildConfig
6 | import com.google.firebase.firestore.FirebaseFirestore
7 | import com.google.firebase.firestore.ktx.firestore
8 | import com.google.firebase.ktx.Firebase
9 |
10 | class FirestoreInitializer : Initializer {
11 |
12 | // The host '10.0.2.2' is a special IP address to let the
13 | // Android emulator connect to 'localhost'.
14 | private val FIRESTORE_EMULATOR_HOST = "10.0.2.2"
15 | private val FIRESTORE_EMULATOR_PORT = 8080
16 |
17 | override fun create(context: Context): FirebaseFirestore {
18 | val firestore = Firebase.firestore
19 | // Use emulators only in debug builds
20 | if (BuildConfig.DEBUG) {
21 | firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
22 | }
23 | return firestore
24 | }
25 |
26 | // No dependencies on other libraries
27 | override fun dependencies(): MutableList>> = mutableListOf()
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/util/RatingUtil.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats.util
2 |
3 | import com.google.firebase.example.fireeats.model.Rating
4 | import java.util.ArrayList
5 | import java.util.Random
6 | import java.util.UUID
7 |
8 | /**
9 | * Utilities for Ratings.
10 | */
11 | object RatingUtil {
12 |
13 | private val REVIEW_CONTENTS = arrayOf(
14 | // 0 - 1 stars
15 | "This was awful! Totally inedible.",
16 |
17 | // 1 - 2 stars
18 | "This was pretty bad, would not go back.",
19 |
20 | // 2 - 3 stars
21 | "I was fed, so that's something.",
22 |
23 | // 3 - 4 stars
24 | "This was a nice meal, I'd go back.",
25 |
26 | // 4 - 5 stars
27 | "This was fantastic! Best ever!")
28 |
29 | /**
30 | * Create a random Rating POJO.
31 | */
32 | private val random: Rating
33 | get() {
34 | val rating = Rating()
35 |
36 | val random = Random()
37 |
38 | val score = random.nextDouble() * 5.0
39 | val text = REVIEW_CONTENTS[Math.floor(score).toInt()]
40 |
41 | rating.userId = UUID.randomUUID().toString()
42 | rating.userName = "Random User"
43 | rating.rating = score
44 | rating.text = text
45 |
46 | return rating
47 | }
48 |
49 | /**
50 | * Get a list of random Rating POJOs.
51 | */
52 | fun getRandomList(length: Int): List {
53 | val result = ArrayList()
54 |
55 | for (i in 0 until length) {
56 | result.add(random)
57 | }
58 |
59 | return result
60 | }
61 |
62 | /**
63 | * Get the average rating of a List.
64 | */
65 | fun getAverageRating(ratings: List): Double {
66 | var sum = 0.0
67 |
68 | for (rating in ratings) {
69 | sum += rating.rating
70 | }
71 |
72 | return sum / ratings.size
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/util/RestaurantUtil.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats.util
2 |
3 | import android.content.Context
4 | import com.google.firebase.example.fireeats.R
5 | import com.google.firebase.example.fireeats.model.Restaurant
6 | import java.util.Arrays
7 | import java.util.Locale
8 | import java.util.Random
9 |
10 | /**
11 | * Utilities for Restaurants.
12 | */
13 | object RestaurantUtil {
14 |
15 | private const val RESTAURANT_URL_FMT = "https://storage.googleapis.com/firestorequickstarts.appspot.com/food_%d.png"
16 | private const val MAX_IMAGE_NUM = 22
17 |
18 | private val NAME_FIRST_WORDS = arrayOf(
19 | "Foo", "Bar", "Baz", "Qux", "Fire", "Sam's", "World Famous", "Google", "The Best")
20 |
21 | private val NAME_SECOND_WORDS = arrayOf(
22 | "Restaurant", "Cafe", "Spot", "Eatin' Place", "Eatery", "Drive Thru", "Diner")
23 |
24 | /**
25 | * Create a random Restaurant POJO.
26 | */
27 | fun getRandom(context: Context): Restaurant {
28 | val restaurant = Restaurant()
29 | val random = Random()
30 |
31 | // Cities (first elemnt is 'Any')
32 | var cities = context.resources.getStringArray(R.array.cities)
33 | cities = Arrays.copyOfRange(cities, 1, cities.size)
34 |
35 | // Categories (first element is 'Any')
36 | var categories = context.resources.getStringArray(R.array.categories)
37 | categories = Arrays.copyOfRange(categories, 1, categories.size)
38 |
39 | val prices = intArrayOf(1, 2, 3)
40 |
41 | restaurant.name = getRandomName(random)
42 | restaurant.city = getRandomString(cities, random)
43 | restaurant.category = getRandomString(categories, random)
44 | restaurant.photo = getRandomImageUrl(random)
45 | restaurant.price = getRandomInt(prices, random)
46 | restaurant.numRatings = random.nextInt(20)
47 |
48 | // Note: average rating intentionally not set
49 |
50 | return restaurant
51 | }
52 |
53 | /**
54 | * Get a random image.
55 | */
56 | private fun getRandomImageUrl(random: Random): String {
57 | // Integer between 1 and MAX_IMAGE_NUM (inclusive)
58 | val id = random.nextInt(MAX_IMAGE_NUM) + 1
59 |
60 | return String.format(Locale.getDefault(), RESTAURANT_URL_FMT, id)
61 | }
62 |
63 | /**
64 | * Get price represented as dollar signs.
65 | */
66 | fun getPriceString(restaurant: Restaurant): String {
67 | return getPriceString(restaurant.price)
68 | }
69 |
70 | /**
71 | * Get price represented as dollar signs.
72 | */
73 | fun getPriceString(priceInt: Int): String {
74 | when (priceInt) {
75 | 1 -> return "$"
76 | 2 -> return "$$"
77 | 3 -> return "$$$"
78 | else -> return "$$$"
79 | }
80 | }
81 |
82 | private fun getRandomName(random: Random): String {
83 | return (getRandomString(NAME_FIRST_WORDS, random) + " " +
84 | getRandomString(NAME_SECOND_WORDS, random))
85 | }
86 |
87 | private fun getRandomString(array: Array, random: Random): String {
88 | val ind = random.nextInt(array.size)
89 | return array[ind]
90 | }
91 |
92 | private fun getRandomInt(array: IntArray, random: Random): Int {
93 | val ind = random.nextInt(array.size)
94 | return array[ind]
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/app/src/main/java/com/google/firebase/example/fireeats/viewmodel/MainActivityViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.google.firebase.example.fireeats.viewmodel
2 |
3 | import androidx.lifecycle.ViewModel
4 | import com.google.firebase.example.fireeats.Filters
5 |
6 | /**
7 | * ViewModel for [com.google.firebase.example.fireeats.MainActivity].
8 | */
9 |
10 | class MainActivityViewModel : ViewModel() {
11 |
12 | var isSigningIn: Boolean = false
13 | var filters: Filters = Filters.default
14 | }
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-hdpi/pizza_monster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/app/src/main/res/drawable-hdpi/pizza_monster.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-mdpi/pizza_monster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/app/src/main/res/drawable-mdpi/pizza_monster.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/food_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/app/src/main/res/drawable-nodpi/food_1.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xhdpi/pizza_monster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/app/src/main/res/drawable-xhdpi/pizza_monster.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-xxhdpi/pizza_monster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/app/src/main/res/drawable-xxhdpi/pizza_monster.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_shadow.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/gradient_up.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add_white_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_back_white_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_close_white_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_fastfood_white_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_filter_list_white_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_local_dining_white_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_monetization_on_white_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_place_white_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_restaurant_white_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_sort_white_24px.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
24 |
25 |
35 |
36 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_restaurant_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
23 |
24 |
28 |
29 |
30 |
38 |
39 |
50 |
51 |
60 |
61 |
73 |
74 |
85 |
86 |
93 |
94 |
104 |
105 |
117 |
118 |
119 |
120 |
129 |
130 |
131 |
142 |
143 |
144 |
155 |
156 |
159 |
160 |
166 |
167 |
168 |
169 |
170 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_filters.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
20 |
30 |
31 |
42 |
43 |
44 |
45 |
53 |
54 |
65 |
66 |
67 |
75 |
76 |
87 |
88 |
89 |
97 |
98 |
109 |
110 |
111 |
121 |
122 |
134 |
135 |
136 |
147 |
148 |
149 |
150 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dialog_rating.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
30 |
31 |
40 |
41 |
42 |
52 |
53 |
65 |
66 |
67 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
21 |
22 |
29 |
30 |
35 |
36 |
48 |
49 |
61 |
62 |
72 |
73 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
104 |
105 |
106 |
113 |
114 |
115 |
127 |
128 |
131 |
132 |
138 |
139 |
140 |
141 |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_restaurant_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
19 |
20 |
30 |
31 |
40 |
41 |
42 |
52 |
53 |
66 |
67 |
78 |
79 |
91 |
92 |
104 |
105 |
113 |
114 |
124 |
125 |
139 |
140 |
141 |
142 |
152 |
153 |
154 |
167 |
168 |
169 |
182 |
183 |
187 |
188 |
194 |
195 |
196 |
197 |
198 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_rating.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
24 |
25 |
36 |
37 |
49 |
50 |
57 |
58 |
68 |
69 |
75 |
76 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_restaurant.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
20 |
21 |
33 |
34 |
42 |
43 |
55 |
56 |
66 |
67 |
75 |
76 |
87 |
88 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/menu_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
15 |
16 |
17 |
21 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/main/res/values-v21/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #4285F4
4 | #3367D6
5 | #F4B400
6 |
7 | #DE000000
8 | #8B000000
9 | #61000000
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Friendly Eats
3 |
4 | All food
5 | Anywhere
6 | Any price
7 |
8 | $
9 | $$
10 | $$$
11 |
12 | Category
13 | City
14 | Price
15 | Sort By
16 |
17 | (%d)
18 | All Restaurants
19 | Filter
20 | Search
21 |
22 | Sort by Rating
23 | Sort by Popularity
24 | Sort by Price
25 |
26 | sorted by rating
27 | sorted by price
28 | sorted by popularity
29 |
30 | Add Random Items
31 | Sign Out
32 | •
33 | Apply
34 | Cancel
35 | Oops, couldn\'t find any results\nthat matched your filter
36 | Be the first to leave a review!
37 | How was your experience?
38 | Submit
39 | Add review
40 |
41 | Sign In Error
42 | No network connection.
43 | Unknown error.
44 | Retry
45 | Exit
46 |
47 |
48 |
49 | - @string/value_any_category
50 |
51 | - Brunch
52 | - Burgers
53 | - Coffee
54 | - Deli
55 | - Dim Sum
56 | - Indian
57 | - Italian
58 | - Mediterranean
59 | - Mexican
60 | - Pizza
61 | - Ramen
62 | - Sushi
63 |
64 |
65 |
66 |
67 | - @string/value_any_city
68 |
69 | - Albuquerque
70 | - Arlington
71 | - Atlanta
72 | - Austin
73 | - Baltimore
74 | - Boston
75 | - Charlotte
76 | - Chicago
77 | - Cleveland
78 | - Colorado Springs
79 | - Columbus
80 | - Dallas
81 | - Denver
82 | - Detroit
83 | - El Paso
84 | - Fort Worth
85 | - Fresno
86 | - Houston
87 | - Indianapolis
88 | - Jacksonville
89 | - Kansas City
90 | - Las Vegas
91 | - Long Beach
92 | - Los Angeles
93 | - Louisville
94 | - Memphis
95 | - Mesa
96 | - Miami
97 | - Milwaukee
98 | - Nashville
99 | - New York
100 | - Oakland
101 | - Oklahoma
102 | - Omaha
103 | - Philadelphia
104 | - Phoenix
105 | - Portland
106 | - Raleigh
107 | - Sacramento
108 | - San Antonio
109 | - San Diego
110 | - San Francisco
111 | - San Jose
112 | - Tucson
113 | - Tulsa
114 | - Virginia Beach
115 | - Washington
116 |
117 |
118 |
119 |
120 | - @string/value_any_price
121 |
122 | - @string/price_1
123 | - @string/price_2
124 | - @string/price_3
125 |
126 |
127 |
128 |
129 | - @string/sort_by_rating
130 | - @string/sort_by_popularity
131 | - @string/sort_by_price
132 |
133 |
134 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
19 |
20 |
21 |
22 |
26 |
27 |
35 |
36 |
43 |
44 |
50 |
51 |
57 |
58 |
61 |
62 |
66 |
67 |
71 |
72 |
76 |
77 |
81 |
82 |
86 |
87 |
91 |
92 |
99 |
100 |
104 |
105 |
106 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/network_security_config.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 10.0.2.2
6 |
7 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
3 |
4 | plugins {
5 | id("com.android.application") version "8.10.1" apply false
6 | id("com.android.library") version "8.10.1" apply false
7 | id("org.jetbrains.kotlin.android") version "2.1.21" apply false
8 | id("com.google.gms.google-services") version "4.4.2" apply false
9 | id("androidx.navigation.safeargs") version "2.9.0" apply false
10 | id("com.github.ben-manes.versions") version "0.52.0" apply true
11 | }
12 |
13 | allprojects {
14 | repositories {
15 | google()
16 | mavenCentral()
17 | mavenLocal()
18 | }
19 | }
20 |
21 | fun isNonStable(candidate: ModuleComponentIdentifier): Boolean {
22 | return listOf("alpha", "beta", "rc", "snapshot").any { keyword ->
23 | keyword in candidate.version.lowercase()
24 | }
25 | }
26 |
27 | fun isBlockListed(candidate: ModuleComponentIdentifier): Boolean {
28 | return listOf(
29 | "androidx.browser:browser",
30 | "com.google.android.gms:play-services-auth"
31 | ).any { keyword ->
32 | keyword in candidate.toString().lowercase()
33 | }
34 | }
35 |
36 | tasks.withType {
37 | rejectVersionIf {
38 | isNonStable(candidate) || isBlockListed(candidate)
39 | }
40 | }
41 |
42 | tasks {
43 | register("clean", Delete::class) {
44 | delete(rootProject.buildDir)
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Exit on error
4 | set -e
5 |
6 | # Copy mock google-services file
7 | echo "Using mock google-services.json"
8 | cp mock-google-services.json app/google-services.json
9 |
10 | # Build
11 | ./gradlew clean build
12 |
--------------------------------------------------------------------------------
/docs/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/docs/home.png
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "firestore": {
3 | "rules": "firestore.rules",
4 | "indexes": "firestore.indexes.json"
5 | },
6 | "emulators": {
7 | "auth": {
8 | "port": 9099
9 | },
10 | "firestore": {
11 | "port": 8080
12 | },
13 | "ui": {
14 | "enabled": true
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/firestore.indexes.json:
--------------------------------------------------------------------------------
1 | {
2 | "indexes": [
3 | {
4 | "collectionId": "restaurants",
5 | "queryScope": "COLLECTION",
6 | "fields": [
7 | { "fieldPath": "city", "mode": "ASCENDING" },
8 | { "fieldPath": "avgRating", "mode": "DESCENDING" }
9 | ]
10 | },
11 | {
12 | "collectionId": "restaurants",
13 | "queryScope": "COLLECTION",
14 | "fields": [
15 | { "fieldPath": "category", "mode": "ASCENDING" },
16 | { "fieldPath": "avgRating", "mode": "DESCENDING" }
17 | ]
18 | },
19 | {
20 | "collectionId": "restaurants",
21 | "queryScope": "COLLECTION",
22 | "fields": [
23 | { "fieldPath": "price", "mode": "ASCENDING" },
24 | { "fieldPath": "avgRating", "mode": "DESCENDING" }
25 | ]
26 | },
27 | {
28 | "collectionId": "restaurants",
29 | "queryScope": "COLLECTION",
30 | "fields": [
31 | { "fieldPath": "city", "mode": "ASCENDING" },
32 | { "fieldPath": "numRatings", "mode": "DESCENDING" }
33 | ]
34 | },
35 | {
36 | "collectionId": "restaurants",
37 | "queryScope": "COLLECTION",
38 | "fields": [
39 | { "fieldPath": "category", "mode": "ASCENDING" },
40 | { "fieldPath": "numRatings", "mode": "DESCENDING" }
41 | ]
42 | },
43 | {
44 | "collectionId": "restaurants",
45 | "queryScope": "COLLECTION",
46 | "fields": [
47 | { "fieldPath": "price", "mode": "ASCENDING" },
48 | { "fieldPath": "numRatings", "mode": "DESCENDING" }
49 | ]
50 | },
51 | {
52 | "collectionId": "restaurants",
53 | "queryScope": "COLLECTION",
54 | "fields": [
55 | { "fieldPath": "city", "mode": "ASCENDING" },
56 | { "fieldPath": "price", "mode": "ASCENDING" }
57 | ]
58 | },
59 | {
60 | "collectionId": "restaurants",
61 | "fields": [
62 | { "fieldPath": "category", "mode": "ASCENDING" },
63 | { "fieldPath": "price", "mode": "ASCENDING" }
64 | ]
65 | }
66 | ],
67 | "fieldOverrides": []
68 | }
69 |
--------------------------------------------------------------------------------
/firestore.rules:
--------------------------------------------------------------------------------
1 | rules_version = '2';
2 | service cloud.firestore {
3 | match /databases/{database}/documents {
4 | match /{document=**} {
5 | //
6 | // WARNING: These rules are insecure! We will replace them with
7 | // more secure rules later in the codelab
8 | //
9 | allow read, write: if request.auth != null;
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 | android.enableJetifier=true
14 | android.useAndroidX=true
15 | android.defaults.buildfeatures.buildconfig=true
16 | android.nonTransitiveRClass=false
17 | android.nonFinalResIds=false
18 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/mock-google-services.json:
--------------------------------------------------------------------------------
1 | {
2 | "project_info": {
3 | "project_number": "606665771234",
4 | "firebase_url": "https://mock-firestore-project.firebaseio.com",
5 | "project_id": "mock-firestore-project",
6 | "storage_bucket": "mock-firestore-project.appspot.com"
7 | },
8 | "client": [
9 | {
10 | "client_info": {
11 | "mobilesdk_app_id": "1:606665771229:android:c1f0f09aa42abc12",
12 | "android_client_info": {
13 | "package_name": "com.google.firebase.example.fireeats"
14 | }
15 | },
16 | "oauth_client": [
17 | {
18 | "client_id": "123455771229-sc9bpuefbjceq7i1qabk7gssstefrdlv.apps.googleusercontent.com",
19 | "client_type": 3
20 | }
21 | ],
22 | "api_key": [
23 | {
24 | "current_key": "AIzaSyCNzhU2a9gMc_JHurHbywOrRI9Vj4VQZZZ"
25 | }
26 | ],
27 | "services": {
28 | "analytics_service": {
29 | "status": 1
30 | },
31 | "appinvite_service": {
32 | "status": 1,
33 | "other_platform_oauth_client": []
34 | },
35 | "ads_service": {
36 | "status": 2
37 | }
38 | }
39 | }
40 | ],
41 | "configuration_version": "1"
42 | }
43 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 |
9 | include(":app")
10 |
--------------------------------------------------------------------------------
/steps/img/1129441c6ebb5eaf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/1129441c6ebb5eaf.png
--------------------------------------------------------------------------------
/steps/img/4c68c6654f4168ad.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/4c68c6654f4168ad.png
--------------------------------------------------------------------------------
/steps/img/67898572a35672a5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/67898572a35672a5.png
--------------------------------------------------------------------------------
/steps/img/73d151ed16016421.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/73d151ed16016421.png
--------------------------------------------------------------------------------
/steps/img/78fa16cdf8ef435a.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/78fa16cdf8ef435a.png
--------------------------------------------------------------------------------
/steps/img/7a67a8a400c80c50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/7a67a8a400c80c50.png
--------------------------------------------------------------------------------
/steps/img/95691e9b71ba55e3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/95691e9b71ba55e3.png
--------------------------------------------------------------------------------
/steps/img/9d2f625aebcab6af.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/9d2f625aebcab6af.png
--------------------------------------------------------------------------------
/steps/img/9e45f40faefce5d0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/9e45f40faefce5d0.png
--------------------------------------------------------------------------------
/steps/img/a670188398c3c59.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/a670188398c3c59.png
--------------------------------------------------------------------------------
/steps/img/de06424023ffb4b9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/de06424023ffb4b9.png
--------------------------------------------------------------------------------
/steps/img/emulators-auth.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/emulators-auth.png
--------------------------------------------------------------------------------
/steps/img/emulators-firebase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/emulators-firebase.png
--------------------------------------------------------------------------------
/steps/img/f9e670f40bd615b0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/f9e670f40bd615b0.png
--------------------------------------------------------------------------------
/steps/img/sign-in-providers.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/firebase/friendlyeats-android/e7d6a647d0857efc06e03db971b3ada4bc4697e4/steps/img/sign-in-providers.png
--------------------------------------------------------------------------------
/steps/index.lab.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: firestore-android
3 | summary: In this codelab you'll learn how to build an Android app that uses Cloud Firestore.
4 | status: [published]
5 | categories: Firebase
6 | tags: devfest-lon,firebase17,io2018,kiosk,tag-cloud,tag-firebase,web
7 | feedback link: https://github.com/firebase/friendlyeats-android/issues
8 |
9 | ---
10 |
11 | # Cloud Firestore Android Codelab
12 |
13 | [Codelab Feedback](https://github.com/firebase/friendlyeats-android/issues)
14 |
15 |
16 | ## Overview
17 | Duration: 01:00
18 |
19 |
20 | ### Goals
21 |
22 | In this codelab you will build a restaurant recommendation app on Android backed by Cloud Firestore. You will learn how to:
23 |
24 | * Read and write data to Firestore from an Android app
25 | * Listen to changes in Firestore data in realtime
26 | * Use Firebase Authentication and security rules to secure Firestore data
27 | * Write complex Firestore queries
28 |
29 | ### Prerequisites
30 |
31 | Before starting this codelab make sure you have:
32 |
33 | * Android Studio **4.0** or higher
34 | * An Android emulator with API **19** or higher
35 | * Node.js version **10** or higher
36 | * Java version **8** or higher
37 |
38 |
39 | ## Create a Firebase project
40 |
41 | 1. Sign into the [Firebase console](https://firebase.google.com/) with your Google account.
42 | 2. In the [Firebase console](https://console.firebase.google.com), click **Add project**.
43 | 3. As shown in the screen capture below, enter a name for your Firebase project (for example, "Friendly Eats"), and click **Continue**.
44 |
45 |
46 |
47 | 4. You may be asked to enable Google Analytics, for the purposes of this codelab your selection does not matter.
48 | 5. After a minute or so, your Firebase project will be ready. Click **Continue**.
49 |
50 | ## Set up the sample project
51 | Duration: 05:00
52 |
53 | ### Download the code
54 |
55 | Run the following command to clone the sample code for this codelab. This will create a folder called `friendlyeats-android` on your machine:
56 |
57 | ```console
58 | $ git clone https://github.com/firebase/friendlyeats-android
59 | ```
60 |
61 | If you don't have git on your machine, you can also download the code directly from GitHub.
62 |
63 | ### Add Firebase configuration
64 |
65 | 1. In the [Firebase console](https://console.firebase.google.com), select **Project Overview** in the left nav. Click the **Android** button to select the platform. When prompted for a package name use `com.google.firebase.example.fireeats`
66 |
67 |
68 |
69 | 2. Click **Register App** and follow the instructions to download the `google-services.json` file, and move it into the `app/` folder of the code you just downloaded. Then click **Next**.
70 |
71 | ### Import the project
72 |
73 | Open Android Studio. Click **File** > **New** > **Import Project** and select the **friendlyeats-android** folder.
74 |
75 | ## Set up the Firebase Emulators
76 | Duration: 05:00
77 |
78 | In this codelab you'll use the [Firebase Emulator Suite](https://firebase.google.com/docs/emulator-suite) to locally emulate Cloud Firestore and other Firebase services. This provides a safe, fast, and no-cost local development environment to build your app.
79 |
80 | ### Install the Firebase CLI
81 |
82 | First you will need to install the [Firebase CLI](https://firebase.google.com/docs/cli). If you are using macOS or Linux, you can run the following cURL command:
83 |
84 | ```console
85 | curl -sL https://firebase.tools | bash
86 | ```
87 |
88 | If you are using Windows, read the [installation instructions](https://firebase.google.com/docs/cli#install-cli-windows) to get a standalone binary or to install via `npm`.
89 |
90 | Once you've installed the CLI, running `firebase --version` should report a version of `9.0.0` or higher:
91 |
92 | ```console
93 | $ firebase --version
94 | 9.0.0
95 | ```
96 |
97 | ### Log In
98 |
99 | Run `firebase login` to connect the CLI to your Google account. This will open a new browser window to complete the login process. Make sure to choose the same account you used when creating your Firebase project earlier.
100 |
101 | ### Link your project
102 |
103 | From within the `friendlyeats-android` folder run `firebase use --add` to connect your local project to your Firebase project. Follow the prompts to select the project you created earlier and if asked to choose an alias enter `default`.
104 |
105 | ## Run the app
106 | Duration: 02:00
107 |
108 | Now it's time to run the Firebase Emulator Suite and the FriendlyEats Android app for the first time.
109 |
110 | ### Run the emulators
111 |
112 | In your terminal from within the `friendlyeats-android` directory run `firebase emulators:start` to start up the Firebase Emulators. You should see logs like this:
113 |
114 | ```console
115 | $ firebase emulators:start
116 | i emulators: Starting emulators: auth, firestore
117 | i firestore: Firestore Emulator logging to firestore-debug.log
118 | i ui: Emulator UI logging to ui-debug.log
119 |
120 | ┌─────────────────────────────────────────────────────────────┐
121 | │ ✔ All emulators ready! It is now safe to connect your app. │
122 | │ i View Emulator UI at http://localhost:4000 │
123 | └─────────────────────────────────────────────────────────────┘
124 |
125 | ┌────────────────┬────────────────┬─────────────────────────────────┐
126 | │ Emulator │ Host:Port │ View in Emulator UI │
127 | ├────────────────┼────────────────┼─────────────────────────────────┤
128 | │ Authentication │ localhost:9099 │ http://localhost:4000/auth │
129 | ├────────────────┼────────────────┼─────────────────────────────────┤
130 | │ Firestore │ localhost:8080 │ http://localhost:4000/firestore │
131 | └────────────────┴────────────────┴─────────────────────────────────┘
132 | Emulator Hub running at localhost:4400
133 | Other reserved ports: 4500
134 |
135 | Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
136 | ```
137 |
138 | You now have a complete local development environment running on your machine! Make sure to leave this command running for the rest of the codelab, your Android app will need to connect to the emulators.
139 |
140 | ### Connect app to the Emulators
141 |
142 | Open the files `util/FirestoreInitializer.kt` and `util/AuthInitializer.kt` in Android Studio.
143 | These files contain the logic to connect the Firebase SDKs to the local emulators running on your machine, upon application startup.
144 |
145 | On the `create()` method of the `FirestoreInitializer` class, examine this piece of code:
146 |
147 | ```kotlin
148 | // Use emulators only in debug builds
149 | if (BuildConfig.DEBUG) {
150 | firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
151 | }
152 | ```
153 |
154 | We are using `BuildConfig` to make sure we only connect to the emulators when our app is running in `debug` mode. When we compile the app in `release` mode this condition will be false.
155 |
156 | We can see that it is using the `useEmulator(host, port)` method to connect the Firebase SDK to the local Firestore emulator. Throughout the app we will use `FirebaseUtil.getFirestore()` to access this instance of `FirebaseFirestore` so we are sure that we're always connecting to the Firestore emulator when running in `debug` mode.
157 |
158 | ### Run the app
159 |
160 | If you have added the `google-services.json` file properly, the project should now compile. In Android Studio click **Build** > **Rebuild Project** and ensure that there are no remaining errors.
161 |
162 | In Android Studio **Run** the app on your Android emulator. At first you will be presented with a "Sign in" screen. You can use any email and password to sign into the app. This sign in process is connecting to the Firebase Authentication emulator, so no real credentials are being transmitted.
163 |
164 | > aside negative
165 | >
166 | > Note: In order for your app to communicate with the Firebase Emulator Suite, it must be running on an Android Emulator, not a real Android device. This will allow the app to communicate with the Firebase Emulator Suite on `localhost`.
167 |
168 | Now open the Emulators UI by navigating to [http://localhost:4000](http://localhost:4000) in your web browser. Then click on the **Authentication** tab and you should see the account you just created:
169 |
170 |
171 |
172 | Once you have completed the sign in process you should see the app home screen:
173 |
174 |
175 |
176 | Soon we will add some data to populate the home screen.
177 |
178 |
179 | ## Write data to Firestore
180 | Duration: 05:00
181 |
182 |
183 | In this section we will write some data to Firestore so that we can populate the currently empty home screen.
184 |
185 | The main model object in our app is a restaurant (see `model/Restaurant.kt`). Firestore data is split into documents, collections, and subcollections. We will store each restaurant as a document in a top-level collection called `"restaurants"`. To learn more about the Firestore data model, read about documents and collections in [the documentation](https://firebase.google.com/docs/firestore/data-model).
186 |
187 | For demonstration purposes, we will add functionality in the app to create ten random restaurants when we click the "Add Random Items" button in the overflow menu. Open the file `MainFragment.kt` and replace the content in the `onAddItemsClicked()` method with:
188 |
189 | ```kotlin
190 | private fun onAddItemsClicked() {
191 | val restaurantsRef = firestore.collection("restaurants")
192 | for (i in 0..9) {
193 | // Create random restaurant / ratings
194 | val randomRestaurant = RestaurantUtil.getRandom(requireContext())
195 |
196 | // Add restaurant
197 | restaurantsRef.add(randomRestaurant)
198 | }
199 | }
200 | ```
201 |
202 | There are a few important things to note about the code above:
203 |
204 | * We started by getting a reference to the `"restaurants"` collection. Collections are created implicitly when documents are added, so there was no need to create the collection before writing data.
205 | * Documents can be created using Kotlin data classes, which we use to create each Restaurant doc.
206 | * The `add()` method adds a document to a collection with an auto-generated ID, so we did not need to specify a unique ID for each Restaurant.
207 |
208 | Now run the app again and click the "Add Random Items" button in the overflow menu (at the top right corner) to invoke the code you just wrote:
209 |
210 |
211 |
212 | Now open the Emulators UI by navigating to [http://localhost:4000](http://localhost:4000) in your web browser. Then click on the **Firestore** tab and you should see the data you just added:
213 |
214 |
215 |
216 | This data is 100% local to your machine. In fact, your real project doesn't even contain a Firestore database yet! This means it's safe to experiment with modifying and deleting this data without consequence.
217 |
218 | Congratulations, you just wrote data to Firestore! In the next step we'll learn how to display this data in the app.
219 |
220 |
221 | ## Display data from Firestore
222 | Duration: 10:00
223 |
224 |
225 | In this step we will learn how to retrieve data from Firestore and display it in our app. The first step to reading data from Firestore is to create a `Query`. Open the file `MainFragment.kt` and add the following code to the beginning of the `onViewCreated()` method:
226 |
227 | ```kotlin
228 | // Firestore
229 | firestore = Firebase.firestore
230 |
231 | // Get the 50 highest rated restaurants
232 | query = firestore.collection("restaurants")
233 | .orderBy("avgRating", Query.Direction.DESCENDING)
234 | .limit(LIMIT.toLong())
235 | ```
236 |
237 | Now we want to listen to the query, so that we get all matching documents and are notified of future updates in real time. Because our eventual goal is to bind this data to a `RecyclerView`, we need to create a `RecyclerView.Adapter` class to listen to the data.
238 |
239 | Open the `FirestoreAdapter` class, which has been partially implemented already. First, let's make the adapter implement `EventListener` and define the `onEvent` function so that it can receive updates to a Firestore query:
240 |
241 | ```kotlin
242 | abstract class FirestoreAdapter(private var query: Query?) :
243 | RecyclerView.Adapter(),
244 | EventListener { // Add this implements
245 |
246 | // ...
247 |
248 | // Add this method
249 | override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
250 |
251 | // Handle errors
252 | if (e != null) {
253 | Log.w(TAG, "onEvent:error", e)
254 | return
255 | }
256 |
257 | // Dispatch the event
258 | if (documentSnapshots != null) {
259 | for (change in documentSnapshots.documentChanges) {
260 | // snapshot of the changed document
261 | when (change.type) {
262 | DocumentChange.Type.ADDED -> {
263 | // TODO: handle document added
264 | }
265 | DocumentChange.Type.MODIFIED -> {
266 | // TODO: handle document changed
267 | }
268 | DocumentChange.Type.REMOVED -> {
269 | // TODO: handle document removed
270 | }
271 | }
272 | }
273 | }
274 |
275 | onDataChanged()
276 | }
277 |
278 | // ...
279 | }
280 | ```
281 |
282 | On initial load the listener will receive one `ADDED` event for each new document. As the result set of the query changes over time the listener will receive more events containing the changes. Now let's finish implementing the listener. First add three new methods: `onDocumentAdded`, `onDocumentModified`, and `onDocumentRemoved`:
283 |
284 | ```kotlin
285 | private fun onDocumentAdded(change: DocumentChange) {
286 | snapshots.add(change.newIndex, change.document)
287 | notifyItemInserted(change.newIndex)
288 | }
289 |
290 | private fun onDocumentModified(change: DocumentChange) {
291 | if (change.oldIndex == change.newIndex) {
292 | // Item changed but remained in same position
293 | snapshots[change.oldIndex] = change.document
294 | notifyItemChanged(change.oldIndex)
295 | } else {
296 | // Item changed and changed position
297 | snapshots.removeAt(change.oldIndex)
298 | snapshots.add(change.newIndex, change.document)
299 | notifyItemMoved(change.oldIndex, change.newIndex)
300 | }
301 | }
302 |
303 | private fun onDocumentRemoved(change: DocumentChange) {
304 | snapshots.removeAt(change.oldIndex)
305 | notifyItemRemoved(change.oldIndex)
306 | }
307 | ```
308 |
309 | Then call these new methods from `onEvent`:
310 |
311 | ```kotlin
312 | override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
313 |
314 | // Handle errors
315 | if (e != null) {
316 | Log.w(TAG, "onEvent:error", e)
317 | return
318 | }
319 |
320 | // Dispatch the event
321 | if (documentSnapshots != null) {
322 | for (change in documentSnapshots.documentChanges) {
323 | // snapshot of the changed document
324 | when (change.type) {
325 | DocumentChange.Type.ADDED -> {
326 | onDocumentAdded(change) // Add this line
327 | }
328 | DocumentChange.Type.MODIFIED -> {
329 | onDocumentModified(change) // Add this line
330 | }
331 | DocumentChange.Type.REMOVED -> {
332 | onDocumentRemoved(change) // Add this line
333 | }
334 | }
335 | }
336 | }
337 |
338 | onDataChanged()
339 | }
340 | ```
341 |
342 | Finally implement the `startListening()` method to attach the listener:
343 |
344 | ```kotlin
345 | fun startListening() {
346 | if (registration == null) {
347 | registration = query.addSnapshotListener(this)
348 | }
349 | }
350 | ```
351 |
352 | > aside positive
353 | >
354 | > **Note**: this codelab demonstrates the real-time capabilities of Firestore, but it's also simple to fetch data without a listener. You can call `get()` on any query or reference to fetch a data snapshot.
355 |
356 | Now the app is fully configured to read data from Firestore. **Run** the app again and you should see the restaurants you added in the previous step:
357 |
358 |
359 |
360 | Now go back to the Emulator UI in your browser and edit one of the restaurant names. You should see it change in the app almost instantly!
361 |
362 |
363 | ## Sort and filter data
364 | Duration: 05:00
365 |
366 |
367 | The app currently displays the top-rated restaurants across the entire collection, but in a real restaurant app the user would want to sort and filter the data. For example the app should be able to show "Top seafood restaurants in Philadelphia" or "Least expensive pizza".
368 |
369 | Clicking white bar at the top of the app brings up a filters dialog. In this section we'll use Firestore queries to make this dialog work:
370 |
371 |
372 |
373 | Let's edit the `onFilter()` method of `MainFragment.kt`. This method accepts a `Filters` object which is a helper object we created to capture the output of the filters dialog. We will change this method to construct a query from the filters:
374 |
375 | ```kotlin
376 | override fun onFilter(filters: Filters) {
377 | // Construct query basic query
378 | var query: Query = firestore.collection("restaurants")
379 |
380 | // Category (equality filter)
381 | if (filters.hasCategory()) {
382 | query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.category)
383 | }
384 |
385 | // City (equality filter)
386 | if (filters.hasCity()) {
387 | query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.city)
388 | }
389 |
390 | // Price (equality filter)
391 | if (filters.hasPrice()) {
392 | query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.price)
393 | }
394 |
395 | // Sort by (orderBy with direction)
396 | if (filters.hasSortBy()) {
397 | query = query.orderBy(filters.sortBy.toString(), filters.sortDirection)
398 | }
399 |
400 | // Limit items
401 | query = query.limit(LIMIT.toLong())
402 |
403 | // Update the query
404 | adapter.setQuery(query)
405 |
406 | // Set header
407 | binding.textCurrentSearch.text = HtmlCompat.fromHtml(
408 | filters.getSearchDescription(requireContext()),
409 | HtmlCompat.FROM_HTML_MODE_LEGACY
410 | )
411 | binding.textCurrentSortBy.text = filters.getOrderDescription(requireContext())
412 |
413 | // Save filters
414 | viewModel.filters = filters
415 | }
416 | ```
417 |
418 | In the snippet above we build a `Query` object by attaching `where` and `orderBy` clauses to match the given filters.
419 |
420 | **Run** the app again and select the following filter to show the most popular low-price restaurants:
421 |
422 |
423 |
424 | > aside negative
425 | >
426 | > A complex query like this one requires a **compound index**. When using the Firestore emulator all queries are allowed but if you tried to run this app with a real database you'd get the following warnings in the logs:
427 | >
428 | > ```console
429 | > W/Firestore Adapter: onEvent:error
430 | > com.google.firebase.firestore.FirebaseFirestoreException: FAILED_PRECONDITION: The query requires an index. You can create it here: https://console.firebase.google.com/project/firestore-codelab-android/database/firestore/indexes?create_index=EgtyZXN0YXVyYW50cxoJCgVwcmljZRACGg4KCm51bVJhdGluZ3MQAxoMCghfX25hbWVfXxAD
431 | > at com.google.android.gms.internal.ajs.zze(Unknown Source)
432 | > // ...
433 | > ```
434 | >
435 | > There are two ways to add an index to your app:
436 | >
437 | > 1. Click the link in the error message to create it interactively.
438 | > 1. Add the index to `firebase.indexes.json` and deploy it with the Firebase CLI.
439 | >
440 | > At the end of this codelab we will walk through this process.
441 |
442 | You should now see a filtered list of restaurants containing only low-price options:
443 |
444 |
445 |
446 |
447 | If you've made it this far, you have now built a fully functioning restaurant recommendation viewing app on Firestore! You can now sort and filter restaurants in real time. In the next few sections we'll add reviews to the restaurants and add security rules to the app.
448 |
449 |
450 | ## Organize data in subcollections
451 | Duration: 05:00
452 |
453 |
454 | In this section we'll add ratings to the app so users can review their favorite (or least favorite) restaurants.
455 |
456 | ### Collections and subcollections
457 |
458 | So far we have stored all restaurant data in a top-level collection called "restaurants". When a user rates a restaurant we want to add a new `Rating` object to the restaurants. For this task we will use a subcollection. You can think of a subcollection as a collection that is attached to a document. So each restaurant document will have a ratings subcollection full of rating documents. Subcollections help organize data without bloating our documents or requiring complex queries.
459 |
460 | To access a subcollection, call `.collection()` on the parent document:
461 |
462 | ```kotlin
463 | val subRef = firestore.collection("restaurants")
464 | .document("abc123")
465 | .collection("ratings")
466 | ```
467 |
468 | You can access and query a subcollection just like with a top-level collection, there are no size limitations or performance changes. You can read more about the Firestore data model [here](https://firebase.google.com/docs/firestore/data-model).
469 |
470 | ### Writing data in a transaction
471 |
472 | Adding a `Rating` to the proper subcollection only requires calling `.add()`, but we also need to update the `Restaurant` object's average rating and number of ratings to reflect the new data. If we use separate operations to make these two changes there are a number of race conditions that could result in stale or incorrect data.
473 |
474 | To ensure that ratings are added properly, we will use a transaction to add ratings to a restaurant. This transaction will perform a few actions:
475 |
476 | * Read the restaurant's current rating and calculate the new one
477 | * Add the rating to the subcollection
478 | * Update the restaurant's average rating and number of ratings
479 |
480 | Open `RestaurantDetailFragment.kt` and implement the `addRating` function:
481 |
482 | ```kotlin
483 | private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task {
484 | // Create reference for new rating, for use inside the transaction
485 | val ratingRef = restaurantRef.collection("ratings").document()
486 |
487 | // In a transaction, add the new rating and update the aggregate totals
488 | return firestore.runTransaction { transaction ->
489 | val restaurant = transaction.get(restaurantRef).toObject()
490 | ?: throw Exception("Restaurant not found at ${restaurantRef.path}")
491 |
492 | // Compute new number of ratings
493 | val newNumRatings = restaurant.numRatings + 1
494 |
495 | // Compute new average rating
496 | val oldRatingTotal = restaurant.avgRating * restaurant.numRatings
497 | val newAvgRating = (oldRatingTotal + rating.rating) / newNumRatings
498 |
499 | // Set new restaurant info
500 | restaurant.numRatings = newNumRatings
501 | restaurant.avgRating = newAvgRating
502 |
503 | // Commit to Firestore
504 | transaction.set(restaurantRef, restaurant)
505 | transaction.set(ratingRef, rating)
506 |
507 | null
508 | }
509 | }
510 | ```
511 |
512 | The `addRating()` function returns a `Task` representing the entire transaction. In the `onRating()` function listeners are added to the task to respond to the result of the transaction.
513 |
514 | Now **Run** the app again and click on one of the restaurants, which should bring up the restaurant detail screen. Click the **+** button to start adding a review. Add a review by picking a number of stars and entering some text.
515 |
516 |
517 |
518 | Hitting **Submit** will kick off the transaction. When the transaction completes, you will see your review displayed below and an update to the restaurant's review count:
519 |
520 |
521 |
522 | Congrats! You now have a social, local, mobile restaurant review app built on Cloud Firestore. I hear those are very popular these days.
523 |
524 |
525 | ## Secure your data
526 | Duration: 05:00
527 |
528 | So far we have not considered the security of this application. How do we know that users can only read and write the correct own data? Firestore databases are secured by a configuration file called [Security Rules](https://firebase.google.com/docs/firestore/security/get-started).
529 |
530 | Open the `firestore.rules` file, you should see the following:
531 |
532 | ```
533 | rules_version = '2';
534 | service cloud.firestore {
535 | match /databases/{database}/documents {
536 | match /{document=**} {
537 | //
538 | // WARNING: These rules are insecure! We will replace them with
539 | // more secure rules later in the codelab
540 | //
541 | allow read, write: if request.auth != null;
542 | }
543 | }
544 | }
545 | ```
546 |
547 | > aside negative
548 | >
549 | > **Warning**: the rules above are extremely insecure! Never deploy a real Firebase app without writing custom security rules.
550 |
551 | Let's change these rules to prevent unwanted data acesss or changes, open the `firestore.rules` file and replace the content with the following:
552 |
553 | ```
554 | rules_version = '2';
555 | service cloud.firestore {
556 | match /databases/{database}/documents {
557 | // Determine if the value of the field "key" is the same
558 | // before and after the request.
559 | function isUnchanged(key) {
560 | return (key in resource.data)
561 | && (key in request.resource.data)
562 | && (resource.data[key] == request.resource.data[key]);
563 | }
564 |
565 | // Restaurants
566 | match /restaurants/{restaurantId} {
567 | // Any signed-in user can read
568 | allow read: if request.auth != null;
569 |
570 | // Any signed-in user can create
571 | // WARNING: this rule is for demo purposes only!
572 | allow create: if request.auth != null;
573 |
574 | // Updates are allowed if no fields are added and name is unchanged
575 | allow update: if request.auth != null
576 | && (request.resource.data.keys() == resource.data.keys())
577 | && isUnchanged("name");
578 |
579 | // Deletes are not allowed.
580 | // Note: this is the default, there is no need to explicitly state this.
581 | allow delete: if false;
582 |
583 | // Ratings
584 | match /ratings/{ratingId} {
585 | // Any signed-in user can read
586 | allow read: if request.auth != null;
587 |
588 | // Any signed-in user can create if their uid matches the document
589 | allow create: if request.auth != null
590 | && request.resource.data.userId == request.auth.uid;
591 |
592 | // Deletes and updates are not allowed (default)
593 | allow update, delete: if false;
594 | }
595 | }
596 | }
597 | }
598 | ```
599 |
600 | These rules restrict access to ensure that clients only make safe changes. For example updates to a restaurant document can only change the ratings, not the name or any other immutable data. Ratings can only be created if the user ID matches the signed-in user, which prevents spoofing.
601 |
602 | > aside positive
603 | >
604 | > When you save the `firestore.rules` file the Firestore emulator will automatically hot reload the new rules and apply them to future requests, there is no need to restart the emulators.
605 |
606 | To read more about Security Rules, visit [the documentation](https://firebase.google.com/docs/firestore/security/get-started).
607 |
608 |
609 | ## Conclusion
610 | Duration: 01:00
611 |
612 |
613 | You have now created a fully-featured app on top of Firestore. You learned about the most important Firestore features including:
614 |
615 | * Documents and collections
616 | * Reading and writing data
617 | * Sorting and filtering with queries
618 | * Subcollections
619 | * Transactions
620 |
621 | ### Learn More
622 |
623 | To keep learning about Firestore, here are some good places to get started:
624 |
625 | * [Choose a data structure](https://firebase.google.com/docs/firestore/manage-data/structure-data)
626 | * [Simple and compound queries](https://firebase.google.com/docs/firestore/query-data/queries)
627 |
628 | The restaurant app in this codelab was based on the "Friendly Eats" example application. You can browse the source code for that app [here](https://github.com/firebase/quickstart-android).
629 |
630 | ### Optional: Deploy to production
631 |
632 | So far this app has only used the Firebase Emulator Suite. If you want to learn how to deploy this app to a real Firebase project, continue on to the next step.
633 |
634 |
635 | ## (Optional) Deploy your app
636 | Duration: 05:00
637 |
638 | So far this app has been entirely local, all of the data is contained in the Firebase Emulator Suite. In this section you will learn how to configure your Firebase project so that this app will work in production.
639 |
640 | > aside positive
641 | >
642 | > All of the products used in this codelab are available on the [Spark plan](https://firebase.google.com/pricing/).
643 |
644 | ### Firebase Authentication
645 |
646 | In the Firebase console go to the **Authentication** section and click **Get started**. Navigate to the **Sign-in method** tab and select the **Email/Password** option from **Native providers**.
647 |
648 | Enable the **Email/Password** sign-in method and click **Save**.
649 |
650 |
651 |
652 |
653 | ### Firestore
654 |
655 | #### Create database
656 |
657 | Navigate to the **Firestore Database** section of the console and click **Create Database**:
658 |
659 | 1. When prompted about Security Rules choose to start in **Production Mode**, we'll update those rules soon.
660 | 1. Choose the database location that you'd like to use for your app. Note that selecting a database location is a _permanent_ decision and to change it you will have to create a new project. For more information on choosing a project location, see the [documentation](https://firebase.google.com/docs/projects/locations).
661 |
662 | #### Deploy Rules
663 |
664 | To deploy the Security Rules you wrote earlier, run the following command in the codelab directory:
665 |
666 | ```console
667 | $ firebase deploy --only firestore:rules
668 | ```
669 |
670 | This will deploy the contents of `firestore.rules` to your project, which you can confirm by navigating to the **Rules** tab in the console.
671 |
672 | #### Deploy Indexes
673 |
674 | The FriendlyEats app has complex sorting and filtering which requires a number of custom compound indexes. These can be created by hand in the Firebase console but it is simpler to write their definitions in the `firestore.indexes.json` file and deploy them using the Firebase CLI.
675 |
676 | If you open the `firestore.indexes.json` file you will see that the required indexes have already been provided:
677 |
678 | ```json
679 | {
680 | "indexes": [
681 | {
682 | "collectionId": "restaurants",
683 | "queryScope": "COLLECTION",
684 | "fields": [
685 | { "fieldPath": "city", "mode": "ASCENDING" },
686 | { "fieldPath": "avgRating", "mode": "DESCENDING" }
687 | ]
688 | },
689 | {
690 | "collectionId": "restaurants",
691 | "queryScope": "COLLECTION",
692 | "fields": [
693 | { "fieldPath": "category", "mode": "ASCENDING" },
694 | { "fieldPath": "avgRating", "mode": "DESCENDING" }
695 | ]
696 | },
697 | {
698 | "collectionId": "restaurants",
699 | "queryScope": "COLLECTION",
700 | "fields": [
701 | { "fieldPath": "price", "mode": "ASCENDING" },
702 | { "fieldPath": "avgRating", "mode": "DESCENDING" }
703 | ]
704 | },
705 | {
706 | "collectionId": "restaurants",
707 | "queryScope": "COLLECTION",
708 | "fields": [
709 | { "fieldPath": "city", "mode": "ASCENDING" },
710 | { "fieldPath": "numRatings", "mode": "DESCENDING" }
711 | ]
712 | },
713 | {
714 | "collectionId": "restaurants",
715 | "queryScope": "COLLECTION",
716 | "fields": [
717 | { "fieldPath": "category", "mode": "ASCENDING" },
718 | { "fieldPath": "numRatings", "mode": "DESCENDING" }
719 | ]
720 | },
721 | {
722 | "collectionId": "restaurants",
723 | "queryScope": "COLLECTION",
724 | "fields": [
725 | { "fieldPath": "price", "mode": "ASCENDING" },
726 | { "fieldPath": "numRatings", "mode": "DESCENDING" }
727 | ]
728 | },
729 | {
730 | "collectionId": "restaurants",
731 | "queryScope": "COLLECTION",
732 | "fields": [
733 | { "fieldPath": "city", "mode": "ASCENDING" },
734 | { "fieldPath": "price", "mode": "ASCENDING" }
735 | ]
736 | },
737 | {
738 | "collectionId": "restaurants",
739 | "fields": [
740 | { "fieldPath": "category", "mode": "ASCENDING" },
741 | { "fieldPath": "price", "mode": "ASCENDING" }
742 | ]
743 | }
744 | ],
745 | "fieldOverrides": []
746 | }
747 | ```
748 |
749 | To deploy these indexes run the following command:
750 |
751 | ```console
752 | $ firebase deploy --only firestore:indexes
753 | ```
754 |
755 | Note that index creation is not instantaneous, you can monitor the progress in the Firebase console.
756 |
757 | ### Configure the app
758 |
759 | In the `util/FirestoreInitializer.kt` and `util/AuthInitializer.kt` files we configured the Firebase SDK to connect to the emulators when in debug mode:
760 |
761 | ```kotlin
762 | override fun create(context: Context): FirebaseFirestore {
763 | val firestore = Firebase.firestore
764 | // Use emulators only in debug builds
765 | if (BuildConfig.DEBUG) {
766 | firestore.useEmulator(FIRESTORE_EMULATOR_HOST, FIRESTORE_EMULATOR_PORT)
767 | }
768 | return firestore
769 | }
770 | ```
771 |
772 | If you would like to test your app with your real Firebase project you can either:
773 |
774 | 1. Build the app in release mode and run it on a device.
775 | 1. Temporarily replace `BuildConfig.DEBUG` with `false` and run the app again.
776 |
777 | Note that you may need to **Sign Out** of the app and sign in again in order to properly connect to production.
778 |
--------------------------------------------------------------------------------