Firestore Restaurants Rating App Examples
A step by step Firestore Restaurants Rating App example.
Firestore Restaurants Rating App Quickstart
This is a full comprehensive app that will teach you various aspects of Firebase Firestore. It is a restaurants rating app and you can see it's screenshot below:
To run the app
- Download the code.
- Run the app on an Android device or emulator.
- Use the package name
com.google.firebase.example.fireeats
- This app uses FirebaseUI for authentication.
Security Rules
service cloud.firestore {
match /databases/{database}/documents {
// Anyone can read a restaurant, only authorized
// users can create or update. Deletes are not allowed.
match /restaurants/{restaurantId} {
allow read: if true;
allow create, update: if request.auth.uid != null;
}
// Anyone can read a rating. Only the user who made the rating
// can delete it. Ratings can never be updated.
match /restaurants/{restaurantId}/ratings/{ratingId} {
allow read: if true;
allow create: if request.auth.uid != null;
allow delete: if request.resource.data.userId == request.auth.uid;
allow update: if false;
}
}
}
Indexes
com.google.firebase.example.fireeats W/Firestore Adapter: onEvent:error
com.google.firebase.firestore.FirebaseFirestoreException: FAILED_PRECONDITION: The query requires an index. You can create it here: https://console.firebase.google.com/project/...
Let's start
Follow these steps to create a full Firestore Restaurants Rating App example.
Step 1. Dependencies
We need to add some dependencies in our app/build.gradle
file as shown below:
(a). build.gradle
Our app-level
build.gradle
.
We Prepare our dependencies as shown below. You may use later versions.
We will enable view binding by setting the viewBinding
property to true.
We then declare our app dependencies under the dependencies
closure. We will need the following 20 dependencies:
- Our
Firebase-bom
library. - Our
Com.google.firebase
library. - Our
Play-services-auth
library. - Our
Firebase-ui-auth
library. - Our
Activity-ktx
library. - Our
Appcompat
library. - Our
Core-ktx
library. - Our
Vectordrawable-animated
library. - Our
Cardview
library. - Our
Browser
library. - Our
Material
library. - Our
Media
library. - Our
Recyclerview
library. - Our
Multidex
library. - Our
Navigation-fragment-ktx
library. - Our
Navigation-ui-ktx
library. - Our
Lifecycle-runtime-ktx
library. - Our
Lifecycle-extensions
library. - Our
Glide
library.
Here is our full app/build.gradle
:
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'com.google.gms.google-services'
id 'androidx.navigation.safeargs'
}
android {
testBuildType "release"
compileSdkVersion 33
defaultConfig {
applicationId "com.google.firebase.example.fireeats"
minSdkVersion 19
targetSdkVersion 33
versionCode 1
versionName "1.0"
multiDexEnabled true
vectorDrawables.useSupportLibrary true
lintOptions {
disable 'InvalidPackage'
}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled true
testProguardFiles getDefaultProguardFile('proguard-android.txt'), 'test-proguard-rules.pro'
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
}
}
buildFeatures {
viewBinding = true
}
}
dependencies {
implementation project(":internal:lintchecks")
implementation project(":internal:chooserx")
// Import the Firebase BoM (see: https://firebase.google.com/docs/android/learn-more#bom)
implementation platform('com.google.firebase:firebase-bom:30.5.0')
// Firestore (Java)
implementation 'com.google.firebase:firebase-firestore'
// Firestore (Kotlin)
implementation 'com.google.firebase:firebase-firestore-ktx'
// Firebase Authentication (Java)
implementation 'com.google.firebase:firebase-auth'
// Firebase Authentication (Kotlin)
implementation 'com.google.firebase:firebase-auth-ktx'
// Google Play services
implementation 'com.google.android.gms:play-services-auth:20.3.0'
// FirebaseUI (for authentication)
implementation 'com.firebaseui:firebase-ui-auth:8.0.2'
// Support Libs
implementation 'androidx.activity:activity-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.vectordrawable:vectordrawable-animated:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.browser:browser:1.0.0'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.media:media:1.6.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.2'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.2'
// Android architecture components
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.5.1'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
annotationProcessor 'androidx.lifecycle:lifecycle-compiler:2.5.1'
// Third-party libraries
implementation 'me.zhanghai.android.materialratingbar:library:1.4.0'
implementation 'com.github.bumptech.glide:glide:4.12.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
androidTestImplementation 'androidx.test:rules:1.4.0'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'
androidTestImplementation 'junit:junit:4.13.2'
androidTestImplementation 'org.hamcrest:hamcrest-library:2.2'
androidTestImplementation 'com.google.firebase:firebase-auth'
}
Step 2. Create Firebase App
You will need to create or setup a Firebase app first. This link explains how to do so.
Step 3. Our Android Manifest
We will need to look at our AndroidManifest.xml
.
(a). AndroidManifest.xml
Our
AndroidManifest
file.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.firebase.example.fireeats">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:name="androidx.multidex.MultiDexApplication">
<activity android:name=".EntryChoiceActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.EntryChoice"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".java.MainActivity"
android:theme="@style/AppTheme.Activity" />
<activity
android:name=".kotlin.MainActivity"
android:theme="@style/AppTheme.Activity" />
</application>
</manifest>
Step 4. Create Navigation Rules
Create a directory known as navigation
inside your res
directory and add the following navigation rules:
(a). nav_graph_kotlin.xml
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph_kotlin"
app:startDestination="@id/MainFragment">
<fragment
android:id="@+id/MainFragment"
android:name="com.google.firebase.example.fireeats.kotlin.MainFragment"
tools:layout="@layout/fragment_main" >
<action
android:id="@+id/action_MainFragment_to_RestaurantDetailFragment"
app:destination="@id/RestaurantDetailFragment"
app:enterAnim="@anim/slide_in_from_right"
app:exitAnim="@anim/slide_out_to_left"
app:popEnterAnim="@anim/slide_in_from_left"
app:popExitAnim="@anim/slide_out_to_right" />
</fragment>
<fragment
android:id="@+id/RestaurantDetailFragment"
android:name="com.google.firebase.example.fireeats.kotlin.RestaurantDetailFragment"
tools:layout="@layout/fragment_restaurant_detail" >
<argument
android:name="key_restaurant_id"
app:argType="string" />
</fragment>
</navigation>
Step 5. Create Menus
Create a directory known as menu
inside your res
directory and add the following menu files:
(a). menu_main.xml
Inside your /res/menu/
directory create a menu resource file named menu_main.xml
and add menu items as shown below:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_sign_out"
android:title="@string/sign_out" />
<item
android:id="@+id/menu_add_items"
android:title="@string/add_random_items" />
</menu>
Step 6. Design Layouts
In Android we design our UI interfaces using XML. So let's create the following layouts:
(a). item_restaurant.xml
Our
item_restaurant
layout.
Inside your /res/layout/
directory create an xml layout file named item_restaurant.xml
.
Design your XML layout using the following 4 UI widgets and ViewGroups:
androidx.constraintlayout.widget.ConstraintLayout
ImageView
TextView
me.zhanghai.android.materialratingbar.MaterialRatingBar
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:orientation="vertical"
android:padding="8dp">
<ImageView
android:id="@+id/restaurantItemImage"
android:layout_width="60dp"
android:layout_height="60dp"
android:background="#757575"
android:scaleType="centerCrop"
android:src="@drawable/food_1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/restaurantItemName"
style="@style/AppTheme.Subheader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintStart_toEndOf="@+id/restaurantItemImage"
app:layout_constraintTop_toTopOf="@+id/restaurantItemImage"
tools:text="Foo's Bar" />
<me.zhanghai.android.materialratingbar.MaterialRatingBar
android:id="@+id/restaurantItemRating"
style="@style/Widget.MaterialRatingBar.RatingBar.Indicator.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constraintStart_toStartOf="@+id/restaurantItemName"
app:layout_constraintTop_toBottomOf="@+id/restaurantItemName" />
<TextView
android:id="@+id/restaurantItemNumRatings"
style="@style/AppTheme.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:textColor="@color/greyDisabled"
app:layout_constraintBottom_toBottomOf="@+id/restaurantItemRating"
app:layout_constraintStart_toEndOf="@+id/restaurantItemRating"
app:layout_constraintTop_toTopOf="@+id/restaurantItemRating"
tools:text="(10)" />
<TextView
android:id="@+id/restaurantItemCategory"
style="@style/AppTheme.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/greySecondary"
app:layout_constraintStart_toStartOf="@+id/restaurantItemRating"
app:layout_constraintTop_toBottomOf="@+id/restaurantItemRating"
tools:text="Italian" />
<TextView
android:id="@+id/restaurantItemCityDivider"
style="@style/AppTheme.TextDivider"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:text="@string/divider_bullet"
app:layout_constraintStart_toEndOf="@+id/restaurantItemCategory"
app:layout_constraintTop_toTopOf="@+id/restaurantItemCategory" />
<TextView
android:id="@+id/restaurantItemCity"
style="@style/AppTheme.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:textColor="@color/greySecondary"
app:layout_constraintStart_toEndOf="@+id/restaurantItemCityDivider"
app:layout_constraintTop_toTopOf="@+id/restaurantItemCategory"
tools:text="San Francisco" />
<TextView
android:id="@+id/restaurantItemPrice"
style="@style/AppTheme.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/greySecondary"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/restaurantItemName"
tools:text="$$$" />
</androidx.constraintlayout.widget.ConstraintLayout>
(b). item_rating.xml
Our
item_rating
layout.
Create an xml layout file named item_rating.xml
.
Design your XML layout using the following 4 UI widgets and ViewGroups:
androidx.constraintlayout.widget.ConstraintLayout
TextView
me.zhanghai.android.materialratingbar.MaterialRatingBar
View
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<TextView
android:id="@+id/ratingItemName"
style="@style/AppTheme.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxWidth="120dp"
android:maxLines="1"
android:textColor="@color/greySecondary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="John Doe" />
<TextView
android:id="@+id/ratingItemDivider"
style="@style/AppTheme.TextDivider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:text="@string/divider_bullet"
app:layout_constraintBottom_toBottomOf="@+id/ratingItemName"
app:layout_constraintStart_toEndOf="@+id/ratingItemName"
app:layout_constraintTop_toTopOf="@+id/ratingItemName" />
<TextView
android:id="@+id/ratingItemDate"
style="@style/AppTheme.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:textColor="@color/greySecondary"
app:layout_constraintBottom_toBottomOf="@+id/ratingItemName"
app:layout_constraintStart_toEndOf="@+id/ratingItemDivider"
app:layout_constraintTop_toTopOf="@+id/ratingItemName"
tools:text="9/27/2017" />
<me.zhanghai.android.materialratingbar.MaterialRatingBar
android:id="@+id/ratingItemRating"
style="@style/Widget.MaterialRatingBar.RatingBar.Indicator.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/ratingItemText"
style="@style/AppTheme.Subheader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/greyPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ratingItemName"
tools:text="I thought it was pretty great! And I really have a ton to say wow." />
<View
android:id="@+id/view4"
style="@style/AppTheme.Divider"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/ratingItemText" />
</androidx.constraintlayout.widget.ConstraintLayout>
(c). fragment_restaurant_detail.xml
Our
fragment_restaurant_detail
layout.
Inside your /res/layout/
directory create an xml layout file named fragment_restaurant_detail.xml
.
Design your XML layout using the following 9 UI widgets and ViewGroups:
androidx.constraintlayout.widget.ConstraintLayout
ImageView
View
TextView
me.zhanghai.android.materialratingbar.MaterialRatingBar
com.google.android.material.floatingactionbutton.FloatingActionButton
androidx.recyclerview.widget.RecyclerView
LinearLayout
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#E0E0E0"
tools:ignore="ContentDescription">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/restaurant_top_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/restaurantImage"
android:layout_width="match_parent"
android:layout_height="200dp"
android:alpha="1.0"
android:scaleType="centerCrop"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:src="@drawable/food_1" />
<View
android:layout_width="match_parent"
android:layout_height="200dp"
android:background="@drawable/gradient_up"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="@+id/restaurantImage"
/>
<!-- Back button -->
<ImageView
android:id="@+id/restaurantButtonBack"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="?attr/selectableItemBackground"
app:srcCompat="@drawable/ic_close_white_24px"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="@+id/restaurantName"
/>
<TextView
android:id="@+id/restaurantName"
style="@style/AppTheme.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/restaurantRating"
tools:text="Some Restaurant"
/>
<me.zhanghai.android.materialratingbar.MaterialRatingBar
android:id="@+id/restaurantRating"
style="@style/Widget.MaterialRatingBar.RatingBar.Indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/restaurantCategory"
app:mrb_progressTint="@android:color/white"
app:mrb_secondaryProgressTint="@android:color/white"
app:layout_constraintBottom_toTopOf="@+id/restaurantCategory"
app:layout_constraintStart_toStartOf="@+id/restaurantName"
/>
<TextView
android:id="@+id/restaurantNumRatings"
style="@style/AppTheme.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dp"
android:layout_marginStart="4dp"
android:textColor="@android:color/white"
app:layout_constraintTop_toTopOf="@+id/restaurantRating"
app:layout_constraintBottom_toBottomOf="@+id/restaurantRating"
app:layout_constraintStart_toEndOf="@+id/restaurantRating"
tools:text="(10)" />
<TextView
android:id="@+id/restaurantCategory"
style="@style/AppTheme.Subheader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginBottom="8dp"
android:textColor="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="Italian" />
<TextView
android:id="@+id/restaurantCity_divider"
style="@style/AppTheme.TextDivider"
android:text="@string/divider_bullet"
android:textColor="@android:color/white"
app:layout_constraintTop_toTopOf="@+id/restaurantCategory"
app:layout_constraintStart_toEndOf="@+id/restaurantCategory"
/>
<TextView
android:id="@+id/restaurantCity"
style="@style/AppTheme.Subheader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
tools:text="San Francisco"
app:layout_constraintTop_toTopOf="@+id/restaurantCategory"
app:layout_constraintStart_toEndOf="@+id/restaurantCategory"
/>
<TextView
android:id="@+id/restaurantPrice"
style="@style/AppTheme.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="@+id/restaurantName"
app:layout_constraintEnd_toEndOf="parent"
tools:text="$$$"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabShowRatingDialog"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:translationY="-28dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/restaurant_top_card"
app:srcCompat="@drawable/ic_add_white_24px" />
<!-- Ratings -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerRatings"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@android:color/transparent"
android:clipToPadding="false"
android:paddingTop="28dp"
android:paddingBottom="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/restaurant_top_card"
tools:listitem="@layout/item_rating"
tools:visibility="invisible" />
<!-- View for empty ratings -->
<LinearLayout
android:id="@+id/viewEmptyRatings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/restaurant_top_card"
tools:visibility="visible"
tools:ignore="UseCompoundDrawables">
<ImageView
style="@style/AppTheme.PizzaGuy"
android:src="@drawable/pizza_monster"
tools:ignore="ContentDescription" />
<TextView
style="@style/AppTheme.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/message_no_reviews"
android:textColor="@color/greyDisabled" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
(d). fragment_main.xml
Our
fragment_main
layout.
Create an xml layout file named fragment_main.xml
and design your XML layout using the following 9 UI widgets and ViewGroups:
androidx.constraintlayout.widget.ConstraintLayout
FrameLayout
androidx.cardview.widget.CardView
ImageView
TextView
androidx.recyclerview.widget.RecyclerView
View
LinearLayout
ProgressBar
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#E0E0E0">
<FrameLayout
android:id="@+id/filterBarContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:paddingLeft="12dp"
android:paddingRight="12dp"
android:paddingBottom="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.cardview.widget.CardView
android:id="@+id/filterBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:elevation="12dp"
android:foreground="?attr/selectableItemBackground">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<ImageView
android:id="@+id/buttonFilter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_filter_list_white_24px"
app:tint="@color/greySecondary" />
<TextView
android:id="@+id/textCurrentSearch"
style="@style/AppTheme.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:text="@string/all_restaurants"
android:textColor="@color/greySecondary"
app:layout_constraintStart_toEndOf="@+id/buttonFilter"
app:layout_constraintTop_toTopOf="parent"
tools:text="Filter" />
<TextView
android:id="@+id/textCurrentSortBy"
style="@style/AppTheme.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/sorted_by_rating"
android:textColor="@color/greyDisabled"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@+id/textCurrentSearch"
app:layout_constraintTop_toBottomOf="@+id/textCurrentSearch" />
<ImageView
android:id="@+id/buttonClearFilter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_close_white_24px"
app:tint="@color/greySecondary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>
<!-- Main Restaurants recycler -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerRestaurants"
android:layout_width="match_parent"
android:layout_height="0dp"
android:background="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filterBarContainer"
tools:listitem="@layout/item_restaurant" />
<!-- Shadow below toolbar -->
<View
android:id="@+id/view"
android:layout_width="match_parent"
android:layout_height="4dp"
android:background="@drawable/bg_shadow"
app:layout_constraintTop_toBottomOf="@+id/filterBarContainer"
/>
<!-- Empty list (pizza guy) view -->
<LinearLayout
android:id="@+id/viewEmpty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent"
tools:ignore="UseCompoundDrawables"
tools:visibility="gone">
<ImageView
style="@style/AppTheme.PizzaGuy"
android:src="@drawable/pizza_monster" />
<TextView
style="@style/AppTheme.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/message_no_results"
android:textColor="@color/greyDisabled" />
</LinearLayout>
<ProgressBar
android:id="@+id/progressLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/recyclerRestaurants"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/recyclerRestaurants"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
(e). dialog_rating.xml
Our
dialog_rating
layout.
Then create an xml layout file named dialog_rating.xml
and add the following:
androidx.constraintlayout.widget.ConstraintLayout
TextView
me.zhanghai.android.materialratingbar.MaterialRatingBar
EditText
View
Button
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/textView"
style="@style/AppTheme.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/filter_add_review"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<me.zhanghai.android.materialratingbar.MaterialRatingBar
android:id="@+id/restaurantFormRating"
style="@style/Widget.MaterialRatingBar.RatingBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:maxHeight="24dp"
android:minHeight="24dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView" />
<EditText
android:id="@+id/restaurantFormText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:hint="@string/hint_review"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/restaurantFormRating" />
<View
android:id="@+id/view3"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/restaurantFormCancel"
app:layout_constraintEnd_toStartOf="@+id/restaurantFormCancel"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/restaurantFormCancel" />
<Button
android:id="@+id/restaurantFormCancel"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:textColor="@color/greySecondary"
android:theme="@style/ThemeOverlay.FilterButton"
app:layout_constraintEnd_toStartOf="@+id/restaurantFormButton"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/view3"
app:layout_constraintTop_toTopOf="@+id/restaurantFormButton" />
<Button
android:id="@+id/restaurantFormButton"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/submit"
android:theme="@style/ThemeOverlay.FilterButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/restaurantFormCancel"
app:layout_constraintTop_toBottomOf="@+id/restaurantFormText" />
</androidx.constraintlayout.widget.ConstraintLayout>
(f). dialog_filters.xml
Our
dialog_filters
layout.
Create an xml layout file named dialog_filters.xml
and add these:
androidx.constraintlayout.widget.ConstraintLayout
TextView
ImageView
Spinner
View
Button
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/filters_form"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp">
<TextView
android:id="@+id/filterDialogTitle"
style="@style/AppTheme.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/header_filters"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- Food Type -->
<ImageView
android:id="@+id/icon_category"
style="@style/AppTheme.FilterIcon"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
app:layout_constraintBottom_toBottomOf="@+id/spinnerCategory"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/spinnerCategory"
app:srcCompat="@drawable/ic_fastfood_white_24dp"
app:tint="@color/greySecondary" />
<Spinner
android:id="@+id/spinnerCategory"
style="@style/AppTheme.FilterSpinner"
android:layout_width="0dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:entries="@array/categories"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon_category"
app:layout_constraintTop_toBottomOf="@+id/filterDialogTitle" />
<!-- Location -->
<ImageView
android:id="@+id/icon_city"
style="@style/AppTheme.FilterIcon"
app:layout_constraintBottom_toBottomOf="@+id/spinnerCity"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/spinnerCity"
app:srcCompat="@drawable/ic_place_white_24px"
app:tint="@color/greySecondary" />
<Spinner
android:id="@+id/spinnerCity"
style="@style/AppTheme.FilterSpinner"
android:layout_width="0dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:entries="@array/cities"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon_city"
app:layout_constraintTop_toBottomOf="@+id/spinnerCategory" />
<!-- Price -->
<ImageView
android:id="@+id/icon_price"
style="@style/AppTheme.FilterIcon"
app:layout_constraintBottom_toBottomOf="@+id/spinnerPrice"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/spinnerPrice"
app:srcCompat="@drawable/ic_monetization_on_white_24px"
app:tint="@color/greySecondary" />
<Spinner
android:id="@+id/spinnerPrice"
style="@style/AppTheme.FilterSpinner"
android:layout_width="0dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:entries="@array/prices"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon_price"
app:layout_constraintTop_toBottomOf="@+id/spinnerCity" />
<!-- Sort By -->
<ImageView
android:id="@+id/icon_sort"
style="@style/AppTheme.FilterIcon"
app:layout_constraintBottom_toBottomOf="@+id/spinnerSort"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/spinnerSort"
app:srcCompat="@drawable/ic_sort_white_24px"
app:tint="@color/greySecondary" />
<Spinner
android:id="@+id/spinnerSort"
style="@style/AppTheme.FilterSpinner"
android:layout_width="0dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:entries="@array/sort_by"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon_sort"
app:layout_constraintTop_toBottomOf="@+id/spinnerPrice" />
<!-- Cancel and apply buttons -->
<View
android:id="@+id/view2"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/buttonCancel"
app:layout_constraintEnd_toStartOf="@+id/buttonCancel"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/buttonCancel" />
<Button
android:id="@+id/buttonCancel"
style="@style/Base.Widget.AppCompat.Button.Borderless"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:textColor="@color/greySecondary"
android:theme="@style/ThemeOverlay.FilterButton"
app:layout_constraintEnd_toStartOf="@+id/buttonSearch"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/view2"
app:layout_constraintTop_toTopOf="@+id/buttonSearch" />
<Button
android:id="@+id/buttonSearch"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/apply"
android:theme="@style/ThemeOverlay.FilterButton"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/buttonCancel"
app:layout_constraintTop_toBottomOf="@+id/spinnerSort" />
</androidx.constraintlayout.widget.ConstraintLayout>
(g). activity_main.xml
Our
activity_main
layout.
androidx.constraintlayout.widget.ConstraintLayout
androidx.appcompat.widget.Toolbar
fragment
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:viewBindingIgnore="true">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:theme="@style/AppTheme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:logo="@drawable/ic_restaurant_white_24px"
app:popupTheme="@style/Theme.AppCompat.Light.DarkActionBar"
app:title="@string/app_name"
app:titleMarginStart="24dp"
app:titleTextColor="@android:color/white" />
<fragment
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:defaultNavHost="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>
Step 7. Write Code
Finally we need to write our code as follows:
(a). Restaurant.kt
Our
Restaurant
class.
Create a Kotlin file named Restaurant.kt
.
Here is the full code:
package replace_with_your_package_name
import com.google.firebase.firestore.IgnoreExtraProperties
/**
* Restaurant POJO.
*/
@IgnoreExtraProperties
data class Restaurant(
var name: String? = null,
var city: String? = null,
var category: String? = null,
var photo: String? = null,
var price: Int = 0,
var numRatings: Int = 0,
var avgRating: Double = 0.toDouble()
) {
companion object {
const val FIELD_CITY = "city"
const val FIELD_CATEGORY = "category"
const val FIELD_PRICE = "price"
const val FIELD_POPULARITY = "numRatings"
const val FIELD_AVG_RATING = "avgRating"
}
}
(b). Rating.kt
Our
Rating
class.
Create a Kotlin file named Rating.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
TextUtils
from theandroid.text
package.
Here is the full code:
package replace_with_your_package_name
import android.text.TextUtils
import com.google.firebase.auth.FirebaseUser
import com.google.firebase.firestore.ServerTimestamp
import java.util.Date
/**
* Model POJO for a rating.
*/
data class Rating(
var userId: String? = null,
var userName: String? = null,
var rating: Double = 0.toDouble(),
var text: String? = null,
@ServerTimestamp var timestamp: Date? = null
) {
constructor(user: FirebaseUser, rating: Double, text: String) : this() {
this.userId = user.uid
this.userName = user.displayName
if (TextUtils.isEmpty(this.userName)) {
this.userName = user.email
}
this.rating = rating
this.text = text
}
}
(c). RestaurantUtil.kt
Our
RestaurantUtil
class.
Create a Kotlin file named RestaurantUtil.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
Context
from theandroid.content
package.
We will be creating the following methods:
getRandom(parameter)
- Let's pass aContext
object as a parameter.getRandomImageUrl(parameter)
- Our function will take aRandom
object as a parameter.getPriceString(parameter)
- This function will take aRestaurant
object as a parameter.getPriceString(parameter)
- This function will take aInt
object as a parameter.getRandomName(parameter)
- This function will take aRandom
object as a parameter.getRandomString(array: Array<String>, random: Random): String
.getRandomInt(array: IntArray, random: Random): Int
.
(a). Our getRandomImageUrl()
function
Write the getRandomImageUrl()
function as follows:
private fun getRandomImageUrl(random: Random): String {
// Integer between 1 and MAX_IMAGE_NUM (inclusive)
val id = random.nextInt(MAX_IMAGE_NUM) + 1
return String.format(Locale.getDefault(), RESTAURANT_URL_FMT, id)
}
(b). Our getRandomName()
function
Write the getRandomName()
function as follows:
private fun getRandomName(random: Random): String {
return (getRandomString(NAME_FIRST_WORDS, random) + " " +
getRandomString(NAME_SECOND_WORDS, random))
}
(c). Our getPriceString()
function
Write the getPriceString()
function as follows:
fun getPriceString(priceInt: Int): String {
when (priceInt) {
1 -> return "$"
2 -> return "$$"
3 -> return "$$$"
else -> return "$$$"
}
}
(d). Our getRandomString()
function
Write the getRandomString()
function as follows:
private fun getRandomString(array: Array<String>, random: Random): String {
val ind = random.nextInt(array.size)
return array[ind]
}
(e). Our getRandomInt()
function
Write the getRandomInt()
function as follows:
private fun getRandomInt(array: IntArray, random: Random): Int {
val ind = random.nextInt(array.size)
return array[ind]
}
(f). Our getPriceString()
function
Write the getPriceString()
function as follows:
(g). Our getRandom()
function
Write the getRandom()
function as follows:
fun getRandom(context: Context): Restaurant {
val restaurant = Restaurant()
val random = Random()
// Cities (first elemnt is 'Any')
var cities = context.resources.getStringArray(R.array.cities)
cities = Arrays.copyOfRange(cities, 1, cities.size)
// Categories (first element is 'Any')
var categories = context.resources.getStringArray(R.array.categories)
categories = Arrays.copyOfRange(categories, 1, categories.size)
val prices = intArrayOf(1, 2, 3)
restaurant.name = getRandomName(random)
restaurant.city = getRandomString(cities, random)
restaurant.category = getRandomString(categories, random)
restaurant.photo = getRandomImageUrl(random)
restaurant.price = getRandomInt(prices, random)
restaurant.numRatings = random.nextInt(20)
// Note: average rating intentionally not set
return restaurant
}
Here is the full code:
package replace_with_your_package_name
import android.content.Context
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import java.util.Arrays
import java.util.Locale
import java.util.Random
/**
* Utilities for Restaurants.
*/
object RestaurantUtil {
private const val RESTAURANT_URL_FMT = "https://storage.googleapis.com/firestorequickstarts.appspot.com/food_%d.png"
private const val MAX_IMAGE_NUM = 22
private val NAME_FIRST_WORDS = arrayOf(
"Foo", "Bar", "Baz", "Qux", "Fire", "Sam's", "World Famous", "Google", "The Best")
private val NAME_SECOND_WORDS = arrayOf(
"Restaurant", "Cafe", "Spot", "Eatin' Place", "Eatery", "Drive Thru", "Diner")
/**
* Create a random Restaurant POJO.
*/
fun getRandom(context: Context): Restaurant {
val restaurant = Restaurant()
val random = Random()
// Cities (first elemnt is 'Any')
var cities = context.resources.getStringArray(R.array.cities)
cities = Arrays.copyOfRange(cities, 1, cities.size)
// Categories (first element is 'Any')
var categories = context.resources.getStringArray(R.array.categories)
categories = Arrays.copyOfRange(categories, 1, categories.size)
val prices = intArrayOf(1, 2, 3)
restaurant.name = getRandomName(random)
restaurant.city = getRandomString(cities, random)
restaurant.category = getRandomString(categories, random)
restaurant.photo = getRandomImageUrl(random)
restaurant.price = getRandomInt(prices, random)
restaurant.numRatings = random.nextInt(20)
// Note: average rating intentionally not set
return restaurant
}
/**
* Get a random image.
*/
private fun getRandomImageUrl(random: Random): String {
// Integer between 1 and MAX_IMAGE_NUM (inclusive)
val id = random.nextInt(MAX_IMAGE_NUM) + 1
return String.format(Locale.getDefault(), RESTAURANT_URL_FMT, id)
}
/**
* Get price represented as dollar signs.
*/
fun getPriceString(restaurant: Restaurant): String {
return getPriceString(restaurant.price)
}
/**
* Get price represented as dollar signs.
*/
fun getPriceString(priceInt: Int): String {
when (priceInt) {
1 -> return "$"
2 -> return "$$"
3 -> return "$$$"
else -> return "$$$"
}
}
private fun getRandomName(random: Random): String {
return (getRandomString(NAME_FIRST_WORDS, random) + " " +
getRandomString(NAME_SECOND_WORDS, random))
}
private fun getRandomString(array: Array<String>, random: Random): String {
val ind = random.nextInt(array.size)
return array[ind]
}
private fun getRandomInt(array: IntArray, random: Random): Int {
val ind = random.nextInt(array.size)
return array[ind]
}
}
(d). RatingUtil.kt
Our
RatingUtil
class.
Create a Kotlin file named RatingUtil.kt
.
We will be creating the following methods:
getRandomList(parameter)
- Pass to this method aInt
object as a parameter.getAverageRating(parameter)
- This function will take aList<Rating>
object as a parameter.
(a). Our getAverageRating()
function
Write the getAverageRating()
function as follows:
fun getAverageRating(ratings: List<Rating>): Double {
var sum = 0.0
for (rating in ratings) {
sum += rating.rating
}
return sum / ratings.size
}
(b). Our getRandomList()
function
Write the getRandomList()
function as follows:
fun getRandomList(length: Int): List<Rating> {
val result = ArrayList<Rating>()
for (i in 0 until length) {
result.add(random)
}
return result
}
Here is the full code:
package replace_with_your_package_name
import com.google.firebase.example.fireeats.kotlin.model.Rating
import java.util.ArrayList
import java.util.Random
import java.util.UUID
/**
* Utilities for Ratings.
*/
object RatingUtil {
private val REVIEW_CONTENTS = arrayOf(
// 0 - 1 stars
"This was awful! Totally inedible.",
// 1 - 2 stars
"This was pretty bad, would not go back.",
// 2 - 3 stars
"I was fed, so that's something.",
// 3 - 4 stars
"This was a nice meal, I'd go back.",
// 4 - 5 stars
"This was fantastic! Best ever!")
/**
* Create a random Rating POJO.
*/
private val random: Rating
get() {
val rating = Rating()
val random = Random()
val score = random.nextDouble() * 5.0
val text = REVIEW_CONTENTS[Math.floor(score).toInt()]
rating.userId = UUID.randomUUID().toString()
rating.userName = "Random User"
rating.rating = score
rating.text = text
return rating
}
/**
* Get a list of random Rating POJOs.
*/
fun getRandomList(length: Int): List<Rating> {
val result = ArrayList<Rating>()
for (i in 0 until length) {
result.add(random)
}
return result
}
/**
* Get the average rating of a List.
*/
fun getAverageRating(ratings: List<Rating>): Double {
var sum = 0.0
for (rating in ratings) {
sum += rating.rating
}
return sum / ratings.size
}
}
(e). RestaurantAdapter.kt
Our
RestaurantAdapter
class.
Create a Kotlin file named RestaurantAdapter.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
LayoutInflater
from theandroid.view
package.ViewGroup
from theandroid.view
package.RecyclerView
from theandroidx.recyclerview.widget
package.
Next create a class that derives from RecyclerView.ViewHolder(binding.root)
and add its contents as follows:
We will be overriding the following functions:
onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
.onBindViewHolder(holder: ViewHolder, position: Int)
.
We will be creating the following methods:
(a). Our bind()
function
Write the bind()
function as follows:
fun bind(
snapshot: DocumentSnapshot,
listener: OnRestaurantSelectedListener?
) {
val restaurant = snapshot.toObject<Restaurant>()
if (restaurant == null) {
return
}
val resources = binding.root.resources
// Load image
Glide.with(binding.restaurantItemImage.context)
.load(restaurant.photo)
.into(binding.restaurantItemImage)
val numRatings: Int = restaurant.numRatings
binding.restaurantItemName.text = restaurant.name
binding.restaurantItemRating.rating = restaurant.avgRating.toFloat()
binding.restaurantItemCity.text = restaurant.city
binding.restaurantItemCategory.text = restaurant.category
binding.restaurantItemNumRatings.text = resources.getString(
R.string.fmt_num_ratings,
numRatings)
binding.restaurantItemPrice.text = RestaurantUtil.getPriceString(restaurant)
// Click listener
binding.root.setOnClickListener {
listener?.onRestaurantSelected(snapshot)
}
}
Here is the full code:
package replace_with_your_package_name
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.databinding.ItemRestaurantBinding
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.ktx.toObject
/**
* RecyclerView adapter for a list of Restaurants.
*/
open class RestaurantAdapter(query: Query, private val listener: OnRestaurantSelectedListener) :
FirestoreAdapter<RestaurantAdapter.ViewHolder>(query) {
interface OnRestaurantSelectedListener {
fun onRestaurantSelected(restaurant: DocumentSnapshot)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ItemRestaurantBinding.inflate(
LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getSnapshot(position), listener)
}
class ViewHolder(val binding: ItemRestaurantBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(
snapshot: DocumentSnapshot,
listener: OnRestaurantSelectedListener?
) {
val restaurant = snapshot.toObject<Restaurant>()
if (restaurant == null) {
return
}
val resources = binding.root.resources
// Load image
Glide.with(binding.restaurantItemImage.context)
.load(restaurant.photo)
.into(binding.restaurantItemImage)
val numRatings: Int = restaurant.numRatings
binding.restaurantItemName.text = restaurant.name
binding.restaurantItemRating.rating = restaurant.avgRating.toFloat()
binding.restaurantItemCity.text = restaurant.city
binding.restaurantItemCategory.text = restaurant.category
binding.restaurantItemNumRatings.text = resources.getString(
R.string.fmt_num_ratings,
numRatings)
binding.restaurantItemPrice.text = RestaurantUtil.getPriceString(restaurant)
// Click listener
binding.root.setOnClickListener {
listener?.onRestaurantSelected(snapshot)
}
}
}
}
(f). RatingAdapter.kt
Our
RatingAdapter
class.
Create a Kotlin file named RatingAdapter.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
LayoutInflater
from theandroid.view
package.ViewGroup
from theandroid.view
package.RecyclerView
from theandroidx.recyclerview.widget
package.
Next create a class that derives from FirestoreAdapter<RatingAdapter.ViewHolder>(query)
and add its contents as follows:
We will be overriding the following functions:
onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder
.onBindViewHolder(holder: ViewHolder, position: Int)
.
We will be creating the following methods:
bind(parameter)
- Our function will take aRating?
object as a parameter.
(a). Our bind()
function
Write the bind()
function as follows:
fun bind(rating: Rating?) {
if (rating == null) {
return
}
binding.ratingItemName.text = rating.userName
binding.ratingItemRating.rating = rating.rating.toFloat()
binding.ratingItemText.text = rating.text
if (rating.timestamp != null) {
binding.ratingItemDate.text = FORMAT.format(rating.timestamp)
}
}
Here is the full code:
package replace_with_your_package_name
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.google.firebase.example.fireeats.databinding.ItemRatingBinding
import com.google.firebase.example.fireeats.kotlin.model.Rating
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.ktx.toObject
import java.text.SimpleDateFormat
import java.util.Locale
/**
* RecyclerView adapter for a list of [Rating].
*/
open class RatingAdapter(query: Query) : FirestoreAdapter<RatingAdapter.ViewHolder>(query) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(ItemRatingBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.bind(getSnapshot(position).toObject<Rating>())
}
class ViewHolder(val binding: ItemRatingBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(rating: Rating?) {
if (rating == null) {
return
}
binding.ratingItemName.text = rating.userName
binding.ratingItemRating.rating = rating.rating.toFloat()
binding.ratingItemText.text = rating.text
if (rating.timestamp != null) {
binding.ratingItemDate.text = FORMAT.format(rating.timestamp)
}
}
companion object {
private val FORMAT = SimpleDateFormat(
"MM/dd/yyyy", Locale.US)
}
}
}
(g). FirestoreAdapter.kt
Our
FirestoreAdapter
class.
Create a Kotlin file named FirestoreAdapter.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
RecyclerView
from theandroidx.recyclerview.widget
package.Log
from theandroid.util
package.
Next create a class that derives from RecyclerView.ViewHolder>(private
and add its contents as follows:
We will be overriding the following functions:
onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?)
.getItemCount(): Int
.
We will be creating the following methods:
startListening()
.stopListening()
.setQuery(parameter)
- Pass to this method aQuery
object as a parameter.onError(parameter)
- Our function will take aFirebaseFirestoreException
object as a parameter.onDataChanged()
.getSnapshot(parameter)
- This function will take aInt
object as a parameter.onDocumentAdded(parameter)
- Our function will take aDocumentChange
object as a parameter.onDocumentModified(parameter)
- Let's pass aDocumentChange
object as a parameter.onDocumentRemoved(parameter)
- This function will take aDocumentChange
object as a parameter.
(a). Our stopListening()
function
Write the stopListening()
function as follows:
fun stopListening() {
registration?.remove()
registration = null
snapshots.clear()
notifyDataSetChanged()
}
(b). Our onDocumentAdded()
function
Write the onDocumentAdded()
function as follows:
private fun onDocumentAdded(change: DocumentChange) {
snapshots.add(change.newIndex, change.document)
notifyItemInserted(change.newIndex)
}
(c). Our getSnapshot()
function
Write the getSnapshot()
function as follows:
(d). Our setQuery()
function
Write the setQuery()
function as follows:
fun setQuery(query: Query) {
// Stop listening
stopListening()
// Clear existing data
snapshots.clear()
notifyDataSetChanged()
// Listen to new query
this.query = query
startListening()
}
(e). Our onDocumentModified()
function
Write the onDocumentModified()
function as follows:
private fun onDocumentModified(change: DocumentChange) {
if (change.oldIndex == change.newIndex) {
// Item changed but remained in same position
snapshots[change.oldIndex] = change.document
notifyItemChanged(change.oldIndex)
} else {
// Item changed and changed position
snapshots.removeAt(change.oldIndex)
snapshots.add(change.newIndex, change.document)
notifyItemMoved(change.oldIndex, change.newIndex)
}
}
(f). Our onError()
function
Write the onError()
function as follows:
(g). Our onDocumentRemoved()
function
Write the onDocumentRemoved()
function as follows:
private fun onDocumentRemoved(change: DocumentChange) {
snapshots.removeAt(change.oldIndex)
notifyItemRemoved(change.oldIndex)
}
(h). Our startListening()
function
Write the startListening()
function as follows:
fun startListening() {
if (query != null && registration == null) {
registration = query!!.addSnapshotListener(this)
}
}
(i). Our onDataChanged()
function
Write the onDataChanged()
function as follows:
Here is the full code:
package replace_with_your_package_name
import androidx.recyclerview.widget.RecyclerView
import android.util.Log
import com.google.firebase.firestore.DocumentChange
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.EventListener
import com.google.firebase.firestore.FirebaseFirestoreException
import com.google.firebase.firestore.ListenerRegistration
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.QuerySnapshot
import java.util.ArrayList
/**
* RecyclerView adapter for displaying the results of a Firestore [Query].
*
* Note that this class forgoes some efficiency to gain simplicity. For example, the result of
* [DocumentSnapshot.toObject] is not cached so the same object may be deserialized
* many times as the user scrolls.
*/
abstract class FirestoreAdapter<VH : RecyclerView.ViewHolder>(private var query: Query?) :
RecyclerView.Adapter<VH>(),
EventListener<QuerySnapshot> {
private var registration: ListenerRegistration? = null
private val snapshots = ArrayList<DocumentSnapshot>()
override fun onEvent(documentSnapshots: QuerySnapshot?, e: FirebaseFirestoreException?) {
if (e != null) {
Log.w(TAG, "onEvent:error", e)
onError(e)
return
}
if (documentSnapshots == null) {
return
}
// Dispatch the event
Log.d(TAG, "onEvent:numChanges:" + documentSnapshots.documentChanges.size)
for (change in documentSnapshots.documentChanges) {
when (change.type) {
DocumentChange.Type.ADDED -> onDocumentAdded(change)
DocumentChange.Type.MODIFIED -> onDocumentModified(change)
DocumentChange.Type.REMOVED -> onDocumentRemoved(change)
}
}
onDataChanged()
}
fun startListening() {
if (query != null && registration == null) {
registration = query!!.addSnapshotListener(this)
}
}
fun stopListening() {
registration?.remove()
registration = null
snapshots.clear()
notifyDataSetChanged()
}
fun setQuery(query: Query) {
// Stop listening
stopListening()
// Clear existing data
snapshots.clear()
notifyDataSetChanged()
// Listen to new query
this.query = query
startListening()
}
open fun onError(e: FirebaseFirestoreException) {
Log.w(TAG, "onError", e)
}
open fun onDataChanged() {}
override fun getItemCount(): Int {
return snapshots.size
}
protected fun getSnapshot(index: Int): DocumentSnapshot {
return snapshots[index]
}
private fun onDocumentAdded(change: DocumentChange) {
snapshots.add(change.newIndex, change.document)
notifyItemInserted(change.newIndex)
}
private fun onDocumentModified(change: DocumentChange) {
if (change.oldIndex == change.newIndex) {
// Item changed but remained in same position
snapshots[change.oldIndex] = change.document
notifyItemChanged(change.oldIndex)
} else {
// Item changed and changed position
snapshots.removeAt(change.oldIndex)
snapshots.add(change.newIndex, change.document)
notifyItemMoved(change.oldIndex, change.newIndex)
}
}
private fun onDocumentRemoved(change: DocumentChange) {
snapshots.removeAt(change.oldIndex)
notifyItemRemoved(change.oldIndex)
}
companion object {
private const val TAG = "FirestoreAdapter"
}
}
(h). MainActivityViewModel.kt
Our
MainActivityViewModel
class.
Create a Kotlin file named MainActivityViewModel.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
ViewModel
from theandroidx.lifecycle
package.
Next create a class that derives from ViewModel
and add its contents as follows:
Here is the full code:
package replace_with_your_package_name
import androidx.lifecycle.ViewModel
import com.google.firebase.example.fireeats.kotlin.Filters
/**
* ViewModel for [com.google.firebase.example.fireeats.MainActivity].
*/
class MainActivityViewModel : ViewModel() {
var isSigningIn: Boolean = false
var filters: Filters = Filters.default
}
(i). Filters.kt
Our
Filters
class.
Create a Kotlin file named Filters.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
Context
from theandroid.content
package.TextUtils
from theandroid.text
package.
We will be creating the following methods:
hasCategory(): Boolean
.hasCity(): Boolean
.hasPrice(): Boolean
.hasSortBy(): Boolean
.getSearchDescription(parameter)
- Our function will take aContext
object as a parameter.getOrderDescription(parameter)
- Pass to this method aContext
object as a parameter.
(a). Our hasCity()
function
Write the hasCity()
function as follows:
(b). Our hasCategory()
function
Write the hasCategory()
function as follows:
(c). Our hasPrice()
function
Write the hasPrice()
function as follows:
(d). Our getSearchDescription()
function
Write the getSearchDescription()
function as follows:
fun getSearchDescription(context: Context): String {
val desc = StringBuilder()
if (category == null && city == null) {
desc.append("<b>")
desc.append(context.getString(R.string.all_restaurants))
desc.append("</b>")
}
if (category != null) {
desc.append("<b>")
desc.append(category)
desc.append("</b>")
}
if (category != null && city != null) {
desc.append(" in ")
}
if (city != null) {
desc.append("<b>")
desc.append(city)
desc.append("</b>")
}
if (price > 0) {
desc.append(" for ")
desc.append("<b>")
desc.append(RestaurantUtil.getPriceString(price))
desc.append("</b>")
}
return desc.toString()
}
(e). Our hasSortBy()
function
Write the hasSortBy()
function as follows:
(f). Our getOrderDescription()
function
Write the getOrderDescription()
function as follows:
fun getOrderDescription(context: Context): String {
return when (sortBy) {
Restaurant.FIELD_PRICE -> context.getString(R.string.sorted_by_price)
Restaurant.FIELD_POPULARITY -> context.getString(R.string.sorted_by_popularity)
else -> context.getString(R.string.sorted_by_rating)
}
}
Here is the full code:
package replace_with_your_package_name
import android.content.Context
import android.text.TextUtils
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil
import com.google.firebase.firestore.Query
/**
* Object for passing filters around.
*/
class Filters {
var category: String? = null
var city: String? = null
var price = -1
var sortBy: String? = null
var sortDirection: Query.Direction = Query.Direction.DESCENDING
fun hasCategory(): Boolean {
return !TextUtils.isEmpty(category)
}
fun hasCity(): Boolean {
return !TextUtils.isEmpty(city)
}
fun hasPrice(): Boolean {
return price > 0
}
fun hasSortBy(): Boolean {
return !TextUtils.isEmpty(sortBy)
}
fun getSearchDescription(context: Context): String {
val desc = StringBuilder()
if (category == null && city == null) {
desc.append("<b>")
desc.append(context.getString(R.string.all_restaurants))
desc.append("</b>")
}
if (category != null) {
desc.append("<b>")
desc.append(category)
desc.append("</b>")
}
if (category != null && city != null) {
desc.append(" in ")
}
if (city != null) {
desc.append("<b>")
desc.append(city)
desc.append("</b>")
}
if (price > 0) {
desc.append(" for ")
desc.append("<b>")
desc.append(RestaurantUtil.getPriceString(price))
desc.append("</b>")
}
return desc.toString()
}
fun getOrderDescription(context: Context): String {
return when (sortBy) {
Restaurant.FIELD_PRICE -> context.getString(R.string.sorted_by_price)
Restaurant.FIELD_POPULARITY -> context.getString(R.string.sorted_by_popularity)
else -> context.getString(R.string.sorted_by_rating)
}
}
companion object {
val default: Filters
get() {
val filters = Filters()
filters.sortBy = Restaurant.FIELD_AVG_RATING
filters.sortDirection = Query.Direction.DESCENDING
return filters
}
}
}
(j). FilterDialogFragment.kt
Our
FilterDialogFragment
class.
Create a Kotlin file named FilterDialogFragment.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
Context
from theandroid.content
package.Bundle
from theandroid.os
package.LayoutInflater
from theandroid.view
package.View
from theandroid.view
package.ViewGroup
from theandroid.view
package.DialogFragment
from theandroidx.fragment.app
package.
Next create a class that derives from DialogFragment
and add its contents as follows:
We will be overriding the following functions:
onDestroyView()
.onAttach(context: Context)
.onResume()
.
We will be creating the following methods:
onSearchClicked()
.onCancelClicked()
.resetFilters()
.
(a). Our onFilter()
function
Write the onFilter()
function as follows:
fun onFilter(filters: Filters)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = DialogFiltersBinding.inflate(inflater, container, false)
binding.buttonSearch.setOnClickListener { onSearchClicked() }
binding.buttonCancel.setOnClickListener { onCancelClicked() }
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (parentFragment is FilterListener) {
filterListener = parentFragment as FilterListener
}
(b). Our onCancelClicked()
function
Write the onCancelClicked()
function as follows:
(c). Our resetFilters()
function
Write the resetFilters()
function as follows:
fun resetFilters() {
_binding?.let {
it.spinnerCategory.setSelection(0)
it.spinnerCity.setSelection(0)
it.spinnerPrice.setSelection(0)
it.spinnerSort.setSelection(0)
}
}
(d). Our onSearchClicked()
function
Write the onSearchClicked()
function as follows:
Here is the full code:
package replace_with_your_package_name
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.databinding.DialogFiltersBinding
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import com.google.firebase.firestore.Query
/**
* Dialog Fragment containing filter form.
*/
class FilterDialogFragment : DialogFragment() {
private var _binding: DialogFiltersBinding? = null
private val binding get() = _binding!!
private var filterListener: FilterListener? = null
private val selectedCategory: String?
get() {
val selected = binding.spinnerCategory.selectedItem as String
return if (getString(R.string.value_any_category) == selected) {
null
} else {
selected
}
}
private val selectedCity: String?
get() {
val selected = binding.spinnerCity.selectedItem as String
return if (getString(R.string.value_any_city) == selected) {
null
} else {
selected
}
}
private val selectedPrice: Int
get() {
val selected = binding.spinnerPrice.selectedItem as String
return when (selected) {
getString(R.string.price_1) -> 1
getString(R.string.price_2) -> 2
getString(R.string.price_3) -> 3
else -> -1
}
}
private val selectedSortBy: String?
get() {
val selected = binding.spinnerSort.selectedItem as String
if (getString(R.string.sort_by_rating) == selected) {
return Restaurant.FIELD_AVG_RATING
}
if (getString(R.string.sort_by_price) == selected) {
return Restaurant.FIELD_PRICE
}
return if (getString(R.string.sort_by_popularity) == selected) {
Restaurant.FIELD_POPULARITY
} else {
null
}
}
private val sortDirection: Query.Direction
get() {
val selected = binding.spinnerSort.selectedItem as String
if (getString(R.string.sort_by_rating) == selected) {
return Query.Direction.DESCENDING
}
if (getString(R.string.sort_by_price) == selected) {
return Query.Direction.ASCENDING
}
return if (getString(R.string.sort_by_popularity) == selected) {
Query.Direction.DESCENDING
} else {
Query.Direction.DESCENDING
}
}
val filters: Filters
get() {
val filters = Filters()
filters.category = selectedCategory
filters.city = selectedCity
filters.price = selectedPrice
filters.sortBy = selectedSortBy
filters.sortDirection = sortDirection
return filters
}
interface FilterListener {
fun onFilter(filters: Filters)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = DialogFiltersBinding.inflate(inflater, container, false)
binding.buttonSearch.setOnClickListener { onSearchClicked() }
binding.buttonCancel.setOnClickListener { onCancelClicked() }
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (parentFragment is FilterListener) {
filterListener = parentFragment as FilterListener
}
}
override fun onResume() {
super.onResume()
dialog?.window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
}
private fun onSearchClicked() {
filterListener?.onFilter(filters)
dismiss()
}
private fun onCancelClicked() {
dismiss()
}
fun resetFilters() {
_binding?.let {
it.spinnerCategory.setSelection(0)
it.spinnerCity.setSelection(0)
it.spinnerPrice.setSelection(0)
it.spinnerSort.setSelection(0)
}
}
companion object {
const val TAG = "FilterDialog"
}
}
(k). RatingDialogFragment.kt
Our
RatingDialogFragment
class.
Create a Kotlin file named RatingDialogFragment.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
Context
from theandroid.content
package.Bundle
from theandroid.os
package.LayoutInflater
from theandroid.view
package.View
from theandroid.view
package.ViewGroup
from theandroid.view
package.DialogFragment
from theandroidx.fragment.app
package.
Next create a class that derives from DialogFragment
and add its contents as follows:
We will be overriding the following functions:
onDestroyView()
.onAttach(context: Context)
.onResume()
.
We will be creating the following methods:
onSubmitClicked()
.onCancelClicked()
.
(a). Our onSubmitClicked()
function
Write the onSubmitClicked()
function as follows:
private fun onSubmitClicked() {
val user = Firebase.auth.currentUser
user?.let {
val rating = Rating(
it,
binding.restaurantFormRating.rating.toDouble(),
binding.restaurantFormText.text.toString())
ratingListener?.onRating(rating)
}
dismiss()
}
(b). Our onRating()
function
Write the onRating()
function as follows:
fun onRating(rating: Rating)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = DialogRatingBinding.inflate(inflater, container, false)
binding.restaurantFormButton.setOnClickListener { onSubmitClicked() }
binding.restaurantFormCancel.setOnClickListener { onCancelClicked() }
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (parentFragment is RatingListener) {
ratingListener = parentFragment as RatingListener
}
(c). Our onCancelClicked()
function
Write the onCancelClicked()
function as follows:
Here is the full code:
package replace_with_your_package_name
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import com.google.firebase.auth.ktx.auth
import com.google.firebase.example.fireeats.databinding.DialogRatingBinding
import com.google.firebase.example.fireeats.kotlin.model.Rating
import com.google.firebase.ktx.Firebase
/**
* Dialog Fragment containing rating form.
*/
class RatingDialogFragment : DialogFragment() {
private var _binding: DialogRatingBinding? = null
private val binding get() = _binding!!
private var ratingListener: RatingListener? = null
internal interface RatingListener {
fun onRating(rating: Rating)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = DialogRatingBinding.inflate(inflater, container, false)
binding.restaurantFormButton.setOnClickListener { onSubmitClicked() }
binding.restaurantFormCancel.setOnClickListener { onCancelClicked() }
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onAttach(context: Context) {
super.onAttach(context)
if (parentFragment is RatingListener) {
ratingListener = parentFragment as RatingListener
}
}
override fun onResume() {
super.onResume()
dialog?.window?.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT)
}
private fun onSubmitClicked() {
val user = Firebase.auth.currentUser
user?.let {
val rating = Rating(
it,
binding.restaurantFormRating.rating.toDouble(),
binding.restaurantFormText.text.toString())
ratingListener?.onRating(rating)
}
dismiss()
}
private fun onCancelClicked() {
dismiss()
}
companion object {
const val TAG = "RatingDialog"
}
}
(m). RestaurantDetailFragment.kt
Our
RestaurantDetailFragment
class.
Create a Kotlin file named RestaurantDetailFragment.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
Context
from theandroid.content
package.Bundle
from theandroid.os
package.Log
from theandroid.util
package.LayoutInflater
from theandroid.view
package.View
from theandroid.view
package.ViewGroup
from theandroid.view
package.InputMethodManager
from theandroid.view.inputmethod
package.Fragment
from theandroidx.fragment.app
package.LinearLayoutManager
from theandroidx.recyclerview.widget
package.
Next create a class that derives from Fragment
and add its contents as follows:
We will be overriding the following functions:
onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
.onViewCreated(view: View, savedInstanceState: Bundle?)
.onDataChanged()
.onEvent(snapshot: DocumentSnapshot?, e: FirebaseFirestoreException?)
.onRating(rating: Rating)
.
We will be creating the following methods:
onStart()
.onStop()
.onRestaurantLoaded(parameter)
- Pass to this method aRestaurant
object as a parameter.onBackArrowClicked()
.onAddRatingClicked()
.addRating(restaurantRef: DocumentReference, rating: Rating): Task<Void>
.hideKeyboard()
.
(a). Our onStop()
function
Write the onStop()
function as follows:
public override fun onStop() {
super.onStop()
ratingAdapter.stopListening()
restaurantRegistration?.remove()
restaurantRegistration = null
}
(b). Our addRating()
function
Write the addRating()
function as follows:
private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task<Void> {
// Create reference for new rating, for use inside the transaction
val ratingRef = restaurantRef.collection("ratings").document()
// In a transaction, add the new rating and update the aggregate totals
return firestore.runTransaction { transaction ->
val restaurant = transaction.get(restaurantRef).toObject<Restaurant>()
if (restaurant == null) {
throw Exception("Resraurant not found at ${restaurantRef.path}")
}
// Compute new number of ratings
val newNumRatings = restaurant.numRatings + 1
// Compute new average rating
val oldRatingTotal = restaurant.avgRating * restaurant.numRatings
val newAvgRating = (oldRatingTotal + rating.rating) / newNumRatings
// Set new restaurant info
restaurant.numRatings = newNumRatings
restaurant.avgRating = newAvgRating
// Commit to Firestore
transaction.set(restaurantRef, restaurant)
transaction.set(ratingRef, rating)
null
}
}
(c). Our onAddRatingClicked()
function
Write the onAddRatingClicked()
function as follows:
private fun onAddRatingClicked() {
ratingDialog?.show(childFragmentManager, RatingDialogFragment.TAG)
}
(d). Our onBackArrowClicked()
function
Write the onBackArrowClicked()
function as follows:
(e). Our hideKeyboard()
function
Write the hideKeyboard()
function as follows:
private fun hideKeyboard() {
val view = requireActivity().currentFocus
if (view != null) {
(requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.hideSoftInputFromWindow(view.windowToken, 0)
}
}
(f). Our onStart()
function
Write the onStart()
function as follows:
public override fun onStart() {
super.onStart()
ratingAdapter.startListening()
restaurantRegistration = restaurantRef.addSnapshotListener(this)
}
(g). Our onRestaurantLoaded()
function
Write the onRestaurantLoaded()
function as follows:
private fun onRestaurantLoaded(restaurant: Restaurant) {
binding.restaurantName.text = restaurant.name
binding.restaurantRating.rating = restaurant.avgRating.toFloat()
binding.restaurantNumRatings.text = getString(R.string.fmt_num_ratings, restaurant.numRatings)
binding.restaurantCity.text = restaurant.city
binding.restaurantCategory.text = restaurant.category
binding.restaurantPrice.text = RestaurantUtil.getPriceString(restaurant)
// Background image
Glide.with(binding.restaurantImage.context)
.load(restaurant.photo)
.into(binding.restaurantImage)
}
Here is the full code:
package replace_with_your_package_name
import android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import com.bumptech.glide.Glide
import com.google.android.gms.tasks.Task
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.databinding.FragmentRestaurantDetailBinding
import com.google.firebase.example.fireeats.kotlin.adapter.RatingAdapter
import com.google.firebase.example.fireeats.kotlin.model.Rating
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil
import com.google.firebase.firestore.DocumentReference
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.EventListener
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.FirebaseFirestoreException
import com.google.firebase.firestore.ListenerRegistration
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.firestore.ktx.toObject
import com.google.firebase.ktx.Firebase
class RestaurantDetailFragment : Fragment(),
EventListener<DocumentSnapshot>,
RatingDialogFragment.RatingListener {
private var ratingDialog: RatingDialogFragment? = null
private lateinit var binding: FragmentRestaurantDetailBinding
private lateinit var firestore: FirebaseFirestore
private lateinit var restaurantRef: DocumentReference
private lateinit var ratingAdapter: RatingAdapter
private var restaurantRegistration: ListenerRegistration? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = FragmentRestaurantDetailBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Get restaurant ID from extras
val restaurantId = RestaurantDetailFragmentArgs.fromBundle(requireArguments()).keyRestaurantId
// Initialize Firestore
firestore = Firebase.firestore
// Get reference to the restaurant
restaurantRef = firestore.collection("restaurants").document(restaurantId)
// Get ratings
val ratingsQuery = restaurantRef
.collection("ratings")
.orderBy("timestamp", Query.Direction.DESCENDING)
.limit(50)
// RecyclerView
ratingAdapter = object : RatingAdapter(ratingsQuery) {
override fun onDataChanged() {
if (itemCount == 0) {
binding.recyclerRatings.visibility = View.GONE
binding.viewEmptyRatings.visibility = View.VISIBLE
} else {
binding.recyclerRatings.visibility = View.VISIBLE
binding.viewEmptyRatings.visibility = View.GONE
}
}
}
binding.recyclerRatings.layoutManager = LinearLayoutManager(context)
binding.recyclerRatings.adapter = ratingAdapter
ratingDialog = RatingDialogFragment()
binding.restaurantButtonBack.setOnClickListener { onBackArrowClicked() }
binding.fabShowRatingDialog.setOnClickListener { onAddRatingClicked() }
}
public override fun onStart() {
super.onStart()
ratingAdapter.startListening()
restaurantRegistration = restaurantRef.addSnapshotListener(this)
}
public override fun onStop() {
super.onStop()
ratingAdapter.stopListening()
restaurantRegistration?.remove()
restaurantRegistration = null
}
/**
* Listener for the Restaurant document ([.restaurantRef]).
*/
override fun onEvent(snapshot: DocumentSnapshot?, e: FirebaseFirestoreException?) {
if (e != null) {
Log.w(TAG, "restaurant:onEvent", e)
return
}
snapshot?.let {
val restaurant = snapshot.toObject<Restaurant>()
if (restaurant != null) {
onRestaurantLoaded(restaurant)
}
}
}
private fun onRestaurantLoaded(restaurant: Restaurant) {
binding.restaurantName.text = restaurant.name
binding.restaurantRating.rating = restaurant.avgRating.toFloat()
binding.restaurantNumRatings.text = getString(R.string.fmt_num_ratings, restaurant.numRatings)
binding.restaurantCity.text = restaurant.city
binding.restaurantCategory.text = restaurant.category
binding.restaurantPrice.text = RestaurantUtil.getPriceString(restaurant)
// Background image
Glide.with(binding.restaurantImage.context)
.load(restaurant.photo)
.into(binding.restaurantImage)
}
private fun onBackArrowClicked() {
requireActivity().onBackPressed()
}
private fun onAddRatingClicked() {
ratingDialog?.show(childFragmentManager, RatingDialogFragment.TAG)
}
override fun onRating(rating: Rating) {
// In a transaction, add the new rating and update the aggregate totals
addRating(restaurantRef, rating)
.addOnSuccessListener(requireActivity()) {
Log.d(TAG, "Rating added")
// Hide keyboard and scroll to top
hideKeyboard()
binding.recyclerRatings.smoothScrollToPosition(0)
}
.addOnFailureListener(requireActivity()) { e ->
Log.w(TAG, "Add rating failed", e)
// Show failure message and hide keyboard
hideKeyboard()
Snackbar.make(
requireView().findViewById(android.R.id.content), "Failed to add rating",
Snackbar.LENGTH_SHORT).show()
}
}
private fun addRating(restaurantRef: DocumentReference, rating: Rating): Task<Void> {
// Create reference for new rating, for use inside the transaction
val ratingRef = restaurantRef.collection("ratings").document()
// In a transaction, add the new rating and update the aggregate totals
return firestore.runTransaction { transaction ->
val restaurant = transaction.get(restaurantRef).toObject<Restaurant>()
if (restaurant == null) {
throw Exception("Resraurant not found at ${restaurantRef.path}")
}
// Compute new number of ratings
val newNumRatings = restaurant.numRatings + 1
// Compute new average rating
val oldRatingTotal = restaurant.avgRating * restaurant.numRatings
val newAvgRating = (oldRatingTotal + rating.rating) / newNumRatings
// Set new restaurant info
restaurant.numRatings = newNumRatings
restaurant.avgRating = newAvgRating
// Commit to Firestore
transaction.set(restaurantRef, restaurant)
transaction.set(ratingRef, rating)
null
}
}
private fun hideKeyboard() {
val view = requireActivity().currentFocus
if (view != null) {
(requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
.hideSoftInputFromWindow(view.windowToken, 0)
}
}
companion object {
private const val TAG = "RestaurantDetail"
const val KEY_RESTAURANT_ID = "key_restaurant_id"
}
}
(n). MainFragment.kt
Our
MainFragment
class.
Create a Kotlin file named MainFragment.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
Activity
from theandroid.app
package.Bundle
from theandroid.os
package.Log
from theandroid.util
package.LayoutInflater
from theandroid.view
package.Menu
from theandroid.view
package.MenuInflater
from theandroid.view
package.MenuItem
from theandroid.view
package.View
from theandroid.view
package.ViewGroup
from theandroid.view
package.StringRes
from theandroidx.annotation
package.AlertDialog
from theandroidx.appcompat.app
package.HtmlCompat
from theandroidx.core.text
package.Fragment
from theandroidx.fragment.app
package.ViewModelProvider
from theandroidx.lifecycle
package.findNavController
from theandroidx.navigation.fragment
package.LinearLayoutManager
from theandroidx.recyclerview.widget
package.
Next create a class that derives from Fragment
and add its contents as follows:
We will be overriding the following functions:
onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View?
.onViewCreated(view: View, savedInstanceState: Bundle?)
.onDataChanged()
.onError(e: FirebaseFirestoreException)
.onCreateOptionsMenu(menu: Menu, inflater: MenuInflater)
.onOptionsItemSelected(item: MenuItem): Boolean
.onRestaurantSelected(restaurant: DocumentSnapshot)
.onFilter(filters: Filters)
.
We will be creating the following methods:
onStart()
.onStop()
.onSignInResult(parameter)
- Pass to this method aFirebaseAuthUIAuthenticationResult
object as a parameter.onFilterClicked()
.onClearFilterClicked()
.shouldStartSignIn(): Boolean
.startSignIn()
.onAddItemsClicked()
.showSignInErrorDialog(parameter)
- We pass aInt
object as a parameter.
(a). Our startSignIn()
function
Write the startSignIn()
function as follows:
private fun startSignIn() {
// Sign in with FirebaseUI
val signInLauncher = requireActivity().registerForActivityResult(
FirebaseAuthUIActivityResultContract()
) { result -> this.onSignInResult(result)}
val intent = AuthUI.getInstance().createSignInIntentBuilder()
.setAvailableProviders(listOf(AuthUI.IdpConfig.EmailBuilder().build()))
.setIsSmartLockEnabled(false)
.build()
signInLauncher.launch(intent)
viewModel.isSigningIn = true
}
(b). Our showSignInErrorDialog()
function
Write the showSignInErrorDialog()
function as follows:
private fun showSignInErrorDialog(@StringRes message: Int) {
val dialog = AlertDialog.Builder(requireContext())
.setTitle(R.string.title_sign_in_error)
.setMessage(message)
.setCancelable(false)
.setPositiveButton(R.string.option_retry) { _, _ -> startSignIn() }
.setNegativeButton(R.string.option_exit) { _, _ -> requireActivity().finish() }.create()
dialog.show()
}
(c). Our onAddItemsClicked()
function
Write the onAddItemsClicked()
function as follows:
private fun onAddItemsClicked() {
// Add a bunch of random restaurants
val batch = firestore.batch()
for (i in 0..9) {
val restRef = firestore.collection("restaurants").document()
// Create random restaurant / ratings
val randomRestaurant = RestaurantUtil.getRandom(requireContext())
val randomRatings = RatingUtil.getRandomList(randomRestaurant.numRatings)
randomRestaurant.avgRating = RatingUtil.getAverageRating(randomRatings)
// Add restaurant
batch.set(restRef, randomRestaurant)
// Add ratings to subcollection
for (rating in randomRatings) {
batch.set(restRef.collection("ratings").document(), rating)
}
}
batch.commit().addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d(TAG, "Write batch succeeded.")
} else {
Log.w(TAG, "write batch failed.", task.exception)
}
}
}
(d). Our onStop()
function
Write the onStop()
function as follows:
(e). Our shouldStartSignIn()
function
Write the shouldStartSignIn()
function as follows:
private fun shouldStartSignIn(): Boolean {
return !viewModel.isSigningIn && Firebase.auth.currentUser == null
}
(f). Our onFilterClicked()
function
Write the onFilterClicked()
function as follows:
private fun onFilterClicked() {
// Show the dialog containing filter options
filterDialog.show(childFragmentManager, FilterDialogFragment.TAG)
}
(g). Our onSignInResult()
function
Write the onSignInResult()
function as follows:
private fun onSignInResult(result: FirebaseAuthUIAuthenticationResult) {
val response = result.idpResponse
viewModel.isSigningIn = false
if (result.resultCode != Activity.RESULT_OK) {
if (response == null) {
// User pressed the back button.
requireActivity().finish()
} else if (response.error != null && response.error!!.errorCode == ErrorCodes.NO_NETWORK) {
showSignInErrorDialog(R.string.message_no_network)
} else {
showSignInErrorDialog(R.string.message_unknown)
}
}
}
(h). Our onClearFilterClicked()
function
Write the onClearFilterClicked()
function as follows:
(i). Our onStart()
function
Write the onStart()
function as follows:
public override fun onStart() {
super.onStart()
// Start sign in if necessary
if (shouldStartSignIn()) {
startSignIn()
return
}
// Apply filters
onFilter(viewModel.filters)
// Start listening for Firestore updates
adapter.startListening()
}
Here is the full code:
package replace_with_your_package_name
import android.app.Activity
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.firebase.ui.auth.AuthUI
import com.firebase.ui.auth.ErrorCodes
import com.firebase.ui.auth.FirebaseAuthUIActivityResultContract
import com.firebase.ui.auth.data.model.FirebaseAuthUIAuthenticationResult
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.auth.ktx.auth
import com.google.firebase.example.fireeats.R
import com.google.firebase.example.fireeats.databinding.FragmentMainBinding
import com.google.firebase.example.fireeats.kotlin.adapter.RestaurantAdapter
import com.google.firebase.example.fireeats.kotlin.model.Restaurant
import com.google.firebase.example.fireeats.kotlin.util.RatingUtil
import com.google.firebase.example.fireeats.kotlin.util.RestaurantUtil
import com.google.firebase.example.fireeats.kotlin.viewmodel.MainActivityViewModel
import com.google.firebase.firestore.DocumentSnapshot
import com.google.firebase.firestore.FirebaseFirestore
import com.google.firebase.firestore.FirebaseFirestoreException
import com.google.firebase.firestore.Query
import com.google.firebase.firestore.ktx.firestore
import com.google.firebase.ktx.Firebase
class MainFragment : Fragment(),
FilterDialogFragment.FilterListener,
RestaurantAdapter.OnRestaurantSelectedListener {
lateinit var firestore: FirebaseFirestore
lateinit var query: Query
private lateinit var binding: FragmentMainBinding
private lateinit var filterDialog: FilterDialogFragment
lateinit var adapter: RestaurantAdapter
private lateinit var viewModel: MainActivityViewModel
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
setHasOptionsMenu(true)
binding = FragmentMainBinding.inflate(inflater, container, false);
return binding.root;
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// View model
viewModel = ViewModelProvider(this).get(MainActivityViewModel::class.java)
// Enable Firestore logging
FirebaseFirestore.setLoggingEnabled(true)
// Firestore
firestore = Firebase.firestore
// Get ${LIMIT} restaurants
query = firestore.collection("restaurants")
.orderBy("avgRating", Query.Direction.DESCENDING)
.limit(LIMIT.toLong())
// RecyclerView
adapter = object : RestaurantAdapter(query, this@MainFragment) {
override fun onDataChanged() {
// Show/hide content if the query returns empty.
if (itemCount == 0) {
binding.recyclerRestaurants.visibility = View.GONE
binding.viewEmpty.visibility = View.VISIBLE
} else {
binding.recyclerRestaurants.visibility = View.VISIBLE
binding.viewEmpty.visibility = View.GONE
}
}
override fun onError(e: FirebaseFirestoreException) {
// Show a snackbar on errors
Snackbar.make(binding.root,
"Error: check logs for info.", Snackbar.LENGTH_LONG).show()
}
}
binding.recyclerRestaurants.layoutManager = LinearLayoutManager(context)
binding.recyclerRestaurants.adapter = adapter
// Filter Dialog
filterDialog = FilterDialogFragment()
binding.filterBar.setOnClickListener { onFilterClicked() }
binding.buttonClearFilter.setOnClickListener { onClearFilterClicked() }
}
public override fun onStart() {
super.onStart()
// Start sign in if necessary
if (shouldStartSignIn()) {
startSignIn()
return
}
// Apply filters
onFilter(viewModel.filters)
// Start listening for Firestore updates
adapter.startListening()
}
public override fun onStop() {
super.onStop()
adapter.stopListening()
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_main, menu)
return super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_add_items -> onAddItemsClicked()
R.id.menu_sign_out -> {
AuthUI.getInstance().signOut(requireContext())
startSignIn()
}
}
return super.onOptionsItemSelected(item)
}
private fun onSignInResult(result: FirebaseAuthUIAuthenticationResult) {
val response = result.idpResponse
viewModel.isSigningIn = false
if (result.resultCode != Activity.RESULT_OK) {
if (response == null) {
// User pressed the back button.
requireActivity().finish()
} else if (response.error != null && response.error!!.errorCode == ErrorCodes.NO_NETWORK) {
showSignInErrorDialog(R.string.message_no_network)
} else {
showSignInErrorDialog(R.string.message_unknown)
}
}
}
private fun onFilterClicked() {
// Show the dialog containing filter options
filterDialog.show(childFragmentManager, FilterDialogFragment.TAG)
}
private fun onClearFilterClicked() {
filterDialog.resetFilters()
onFilter(Filters.default)
}
override fun onRestaurantSelected(restaurant: DocumentSnapshot) {
// Go to the details page for the selected restaurant
val action = MainFragmentDirections
.actionMainFragmentToRestaurantDetailFragment(restaurant.id)
findNavController().navigate(action)
}
override fun onFilter(filters: Filters) {
// Construct query basic query
var query: Query = firestore.collection("restaurants")
// Category (equality filter)
if (filters.hasCategory()) {
query = query.whereEqualTo(Restaurant.FIELD_CATEGORY, filters.category)
}
// City (equality filter)
if (filters.hasCity()) {
query = query.whereEqualTo(Restaurant.FIELD_CITY, filters.city)
}
// Price (equality filter)
if (filters.hasPrice()) {
query = query.whereEqualTo(Restaurant.FIELD_PRICE, filters.price)
}
// Sort by (orderBy with direction)
if (filters.hasSortBy()) {
query = query.orderBy(filters.sortBy.toString(), filters.sortDirection)
}
// Limit items
query = query.limit(LIMIT.toLong())
// Update the query
adapter.setQuery(query)
// Set header
binding.textCurrentSearch.text = HtmlCompat.fromHtml(filters.getSearchDescription(requireContext()),
HtmlCompat.FROM_HTML_MODE_LEGACY)
binding.textCurrentSortBy.text = filters.getOrderDescription(requireContext())
// Save filters
viewModel.filters = filters
}
private fun shouldStartSignIn(): Boolean {
return !viewModel.isSigningIn && Firebase.auth.currentUser == null
}
private fun startSignIn() {
// Sign in with FirebaseUI
val signInLauncher = requireActivity().registerForActivityResult(
FirebaseAuthUIActivityResultContract()
) { result -> this.onSignInResult(result)}
val intent = AuthUI.getInstance().createSignInIntentBuilder()
.setAvailableProviders(listOf(AuthUI.IdpConfig.EmailBuilder().build()))
.setIsSmartLockEnabled(false)
.build()
signInLauncher.launch(intent)
viewModel.isSigningIn = true
}
private fun onAddItemsClicked() {
// Add a bunch of random restaurants
val batch = firestore.batch()
for (i in 0..9) {
val restRef = firestore.collection("restaurants").document()
// Create random restaurant / ratings
val randomRestaurant = RestaurantUtil.getRandom(requireContext())
val randomRatings = RatingUtil.getRandomList(randomRestaurant.numRatings)
randomRestaurant.avgRating = RatingUtil.getAverageRating(randomRatings)
// Add restaurant
batch.set(restRef, randomRestaurant)
// Add ratings to subcollection
for (rating in randomRatings) {
batch.set(restRef.collection("ratings").document(), rating)
}
}
batch.commit().addOnCompleteListener { task ->
if (task.isSuccessful) {
Log.d(TAG, "Write batch succeeded.")
} else {
Log.w(TAG, "write batch failed.", task.exception)
}
}
}
private fun showSignInErrorDialog(@StringRes message: Int) {
val dialog = AlertDialog.Builder(requireContext())
.setTitle(R.string.title_sign_in_error)
.setMessage(message)
.setCancelable(false)
.setPositiveButton(R.string.option_retry) { _, _ -> startSignIn() }
.setNegativeButton(R.string.option_exit) { _, _ -> requireActivity().finish() }.create()
dialog.show()
}
companion object {
private const val TAG = "MainActivity"
private const val LIMIT = 50
}
}
(o). MainActivity.kt
Our
MainActivity
class.
Create a Kotlin file named MainActivity.kt
.
We will then add imports from android SDK and other packages. Here are some of the imports we will use in this class:
Bundle
from theandroid.os
package.AppCompatActivity
from theandroidx.appcompat.app
package.Navigation
from theandroidx.navigation
package.
Next create a class that derives from AppCompatActivity
and add its contents as follows:
We will be overriding the following functions:
onCreate(savedInstanceState: Bundle?)
.
Here is the full code:
package replace_with_your_package_name
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.Navigation
import com.google.firebase.example.fireeats.R
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(findViewById(R.id.toolbar))
Navigation.findNavController(this, R.id.nav_host_fragment)
.setGraph(R.navigation.nav_graph_kotlin)
}
}
Reference
Below are the reference links:
No. | Link |
---|---|
2. | Read more here. |
3. | Follow code author here. |