Storing your app’s data in the cloud is very important these days because users tend to own multiple devices and want their apps to be in sync across all of them. With Cloud Firestore, a real-time NoSQL database available on the Firebase platform, doing so is easier and more secure than ever before.
In an earlier tutorial, I introduced you to all the powerful features Cloud Firestore has to offer. Today, I’ll show you how to use it alongside other Firebase products, such as FirebaseUI Auth and Firebase Analytics, to create a simple, yet highly scalable, weight tracker app.
Prerequisites
To follow this step-by-step tutorial, you’ll need:
- the latest version of Android Studio
- a Firebase account
- and a device or emulator running Android 5.0 or higher
1. Project Setup
To be able to make use of Firebase products in your Android Studio project, you will need the Google Services Gradle plugin, a Firebase configuration file, and a few implementation
dependencies. With the Firebase Assistant, you can get them all very easily.
Open the assistant by going to Tools > Firebase. Next, select the Analytics option and click on the Log an Analytics Event link.
You can now press the Connect to Firebase button to connect your Android Studio project to a new Firebase project.
However, to actually add the plugin and the implementation
dependencies, you’ll need to also press the Add Analytics to your app button.
The weight tracker app we are creating today is going to have just two features: storing weights and displaying them as a list sorted in reverse chronological order. We will, of course, be using Firestore to store the weights. To display them as a list, however, we’ll use Firestore-related components available in the FirebaseUI library. Therefore, add the following implementation
dependency to the app
module’s build.gradle file:
implementation 'com.firebaseui:firebase-ui-firestore:3.2.2'
Users must be able to view only their own weights, not the weights of everyone who uses the app. Therefore, our app needs to have the ability to uniquely identify its users. FirebaseUI Auth offers this ability, so add the following dependency next:
implementation 'com.firebaseui:firebase-ui-auth:3.2.2'
We will also be needing a few Material Design widgets to give our app a pleasing look. So make sure you add the Design Support library and the Material Dialogs library as dependencies.
implementation 'com.android.support:design:26.1.0' implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
Finally, press the Sync Now button to update the project.
2. Configuring Firebase Authentication
Firebase Authentication supports a variety of identity providers. However, all of them are disabled by default. To enable one or more of them, you must visit the Firebase console.
In the console, select the Firebase project you created in the previous step, go to its Authentication section, and press the Set up sign-in method button.
To allow users to log in to our app using a Google account, enable Google as a provider, give a meaningful public-facing name to the project, and press the Save button.
Google is the easiest identity provider you can use. It needs no configuration, and your Android Studio project won’t need any additional dependencies for it.
3. Configuring Cloud Firestore
You must enable Firestore in the Firebase console before you start using it. To do so, go to the Database section and press the Get Started button present in the Cloud Firestore Beta card.
You will now be prompted to select a security mode for the database. Make sure you choose the Start in locked mode option and press the Enable button.
In the locked mode, by default, no one will be able to access or modify the contents of the database. Therefore, you must now create a security rule that allows users to read and write only those documents that belong to them. Start by opening the Rules tab.
Before we create a security rule for our database, we must finalize how we are going to store data in it. So let’s say we’re going to have a top-level collection named users
containing documents that represent our users. The documents can have unique IDs that are identical to the IDs that the Firebase Authentication service generates for the users.
Because the users will be adding several weight entries to their documents, using a sub-collection to store those entries is ideal. Let’s call the sub-collection weights
.
Based on the above schema, we can now create a rule for the path users/{user_id}/weights/{weight}
. The rule will be that a user is allowed to read from and write to the path only if the {user_id}
variable is equal to the Firebase Authentication ID of the user.
Accordingly, update the contents of the rules editor.
service cloud.firestore { match /databases/{database}/documents { match /users/{user_id}/weights/{weight} { allow read, write: if user_id == request.auth.uid; } } }
Finally, press the Publish button to activate the rule.
4. Authenticating Users
Our app must be usable only if the user is logged in to it using a Google account. Therefore, as soon as it opens, it must check if the user has a valid Firebase Authentication ID. If the user does have the ID, it should go ahead and render the user interface. Otherwise, it should display a sign-in screen.
To check if the user has an ID, we can simply check that the currentUser
property of the FirebaseAuth
class is not null. If it is null, we can create a sign-in intent by calling the createSignInIntentBuilder()
method of the AuthUI
class.
The following code shows you how to do so for Google as the identity provider:
if(FirebaseAuth.getInstance().currentUser == null) { // Sign in startActivityForResult( AuthUI.getInstance().createSignInIntentBuilder() .setAvailableProviders(arrayListOf( AuthUI.IdpConfig.GoogleBuilder().build() )).build(), 1 ) } else { // Already signed in showUI() }
Note that we’re calling a method named showUI()
if a valid ID is already present. This method doesn’t exist yet, so create it and leave its body empty for now.
private fun showUI() { // To do }
To catch the result of the sign-in intent, we must override the onActivityResult()
method of the activity. Inside the method, if the value of the resultCode
argument is RESULT_OK
and the currentUser
property is no longer null, it means that the user managed to sign in successfully. In this case, we must again call the showUI()
method to render the user interface.
If the user fails to sign in, we can display a toast and close the app by calling the finish()
method.
Accordingly, add the following code to the activity:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if(requestCode == 1) { if(resultCode == Activity.RESULT_OK && FirebaseAuth.getInstance().currentUser != null) { // Successfully signed in showUI() } else { // Sign in failed Toast.makeText(this, "You must sign in to continue", Toast.LENGTH_LONG).show() finish() } } }
At this point, if you run the app for the first time, you should be able to see a sign-in screen that looks like this:
On subsequent runs—thanks to Google Smart Lock, which is enabled by default—you will be signed in automatically.
5. Defining Layouts
Our app needs two layouts: one for the main activity and one for the weight entries that will be shown as items of the reverse chronologically ordered list.
The layout of the main activity must have a RecyclerView
widget, which will act as the list, and a FloatingActionButton
widget, which the user can press to create a new weight entry. After placing them both inside a RelativeLayout
widget, your activity’s layout XML file should look like this:
We have associated an on-click event handler named addWeight()
with the FloatingActionButton
widget. The handler doesn’t exist yet, so create a stub for it inside the activity.
fun addWeight(v: View) { // To do }
To keep the layout of the weight entry simple, we’re going to have just two TextView
widgets inside it: one to display the weight and the other to display the time at which the entry was created. Using a LinearLayout
widget as a container for them will suffice.
Accordingly, create a new layout XML file named weight_entry.xml and add the following code to it:
6. Creating a Model
In the previous step, you saw that each weight entry has a weight and time associated with it. To let Firestore know about this, we must create a model for the weight entry.
Firestore models are usually simple data classes with the required member variables.
data class WeightEntry(var weight: Double=0.0, var timestamp: Long=0)
Now is also a good time to create a view holder for each weight entry. The view holder, as you might have guessed, will be used by the RecyclerView
widget to render the list items. So create a new class named WeightEntryVH
, which extends the RecyclerView.ViewHolder
class, and create member variables for both the TextView
widgets. Don’t forget to initialize them using the findViewById()
method. The following code shows you how to do concisely:
class WeightEntryVH(itemView: View?) : RecyclerView.ViewHolder(itemView) { var weightView: TextView? = itemView?.findViewById(R.id.weight_view) var timeView: TextView? = itemView?.findViewById(R.id.time_view) }
7. Creating Unique User Documents
When a user tries to create a weight entry for the first time, our app must create a separate document for the user inside the users
collection on Firestore. As we decided earlier, the ID of the document will be nothing but the user’s Firebase Authentication ID, which can be obtained using the uid
property of the currentUser
object.
To get a reference to the users
collection, we must use the collection()
method of the FirebaseFirestore
class. We can then call its document()
method and pass the uid
as an argument to create the user’s document.
We will need to access the user-specific documents both while reading and creating the weight entries. To avoid coding the above logic twice, I suggest you create a separate method for it.
private fun getUserDocument():DocumentReference { val db = FirebaseFirestore.getInstance() val users = db.collection("users") val uid = FirebaseAuth.getInstance().currentUser!!.uid return users.document(uid) }
Note that the document will be created only once per user. In other words, multiple calls to the above method will always return the same document, so long as the user uses the same Google account.
8. Adding Weight Entries
When users press the floating action button of our app, they must be able to create new weight entries. To allow them to type in their weights, let us now create a dialog containing an EditText
widget. With the Material Dialog library, doing so is extremely intuitive.
Inside the addWeight()
method, which serves as the on-click event handler of the button, create a MaterialDialog.Builder
instance and call its title()
and content()
methods to give your dialog a title and a meaningful message. Similarly, call the inputType()
method and pass TYPE_CLASS_NUMBER
as an argument to it to make sure that the user can type in only numbers in the dialog.
Next, call the input()
method to specify a hint and associate an event handler with the dialog. The handler will receive the weight the user typed in as an argument.
Finally, make sure you call the show()
method to display the dialog.
MaterialDialog.Builder(this) .title("Add Weight") .content("What's your weight today?") .inputType(InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL) .input("weight in pounds", "", false, { _, weight -> // To do }) .show()
Inside the event handler, we must now add code to actually create and populate a new weight entry document. Because the document must belong to the weights
collection of the user’s unique document, in order to access the collection, you must call the collection()
method of the document that’s returned by the getUserDocument()
method.
Once you have the collection, you can call its add()
method and pass a new instance of the WeightEntry
class to it to store the entry.
getUserDocument() .collection("weights") .add( WeightEntry( weight.toString().toDouble(), Date().time ) )
In the above code, you can see that we are using the time
property of the Date
class to associate a timestamp with the entry.
If you run the app now, you should be able to add new weight entries to Firestore. You won’t see them in the app just yet, but they will be visible in the Firebase console.
9. Displaying the Weight Entries
It is now time to populate the RecyclerView
widget of our layout. So start by creating a reference for it using the findViewById()
method and assigning a new instance of the LinearLayoutManager
class to it. This must be done inside the showUI()
method we created earlier.
val weightsView = findViewById(R.id.weights) weightsView.layoutManager = LinearLayoutManager(this)
The RecyclerView
widget must display all the documents that are present inside the weights
collection of the user’s document. Furthermore, the latest documents should appear first. To meet these requirements, we must now create a query by calling the collection()
and orderBy()
methods.
For the sake of efficiency, you can limit the number of values returned by the query by calling the limit()
method.
The following code creates a query that returns the last 90 weight entries created by the user:
val query = getUserDocument().collection("weights") .orderBy("timestamp", Query.Direction.DESCENDING) .limit(90)
Using the query, we must now create a FirestoreRecyclerOptions
object, which we shall use later to configure the adapter of our RecyclerView
widget. When you pass the query
instance to the setQuery()
method of its builder, make sure you specify that the results returned are in the form of WeightEntry
objects. The following code shows you how to do so:
val options = FirestoreRecyclerOptions.Builder() .setQuery(query, WeightEntry::class.java) .setLifecycleOwner(this) .build()
You might have noticed that we are making our current activity the lifecycle owner of the FirestoreRecyclerOptions
object. Doing this is important because we want our adapter to respond appropriately to common lifecycle events, such as the user opening or closing the app.
At this point we can create a FirestoreRecyclerAdapter
object, which uses the FirestoreRecyclerOptions
object to configure itself. Because the FirestoreRecyclerAdapter
class is abstract, Android Studio should automatically override its methods to generate code that looks like this:
val adapter = object:FirestoreRecyclerAdapter(options) { override fun onBindViewHolder(holder: WeightEntryVH, position: Int, model: WeightEntry) { // To do } override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): WeightEntryVH { // To do } }
As you can see, the FirestoreRecyclerAdapter
class is very similar to the RecyclerView.Adapter
class. In fact, it is derived from it. That means you can use it the same way you would use the RecyclerView.Adapter
class.
Inside the onCreateViewHolder()
method, all you need to do is inflate the weight_entry.xml layout file and return a WeightEntryVH
view holder object based on it.
val layout = layoutInflater.inflate(R.layout.weight_entry, null) return WeightEntryVH(layout)
And inside the onBindViewHolder()
method, you must use the model
argument to update the contents of the TextView
widgets that are present inside the view holder.
While updating the weightView
widget is straightforward, updating the timeView
widget is slightly complicated because we don’t want to show the timestamp, which is in milliseconds, to the user directly.
The easiest way to convert the timestamp into a readable date and time is to use the formatDateTime()
method of the DateUtils
class. In addition to the timestamp, the method can accept several different flags, which it will use to format the date and time. You are free to use flags that match your preferences.
// Show weight holder.weightView?.text = "${model.weight} lb" // Show date and time val formattedDate = DateUtils.formatDateTime(applicationContext, model.timestamp, DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME or DateUtils.FORMAT_SHOW_YEAR) holder.timeView?.text = "On $formattedDate"
Finally, don’t forget to point the RecyclerView
widget to the adapter we just created.
weightsView.adapter = adapter
The app is ready. You should now be able to add new entries and see them appear in the list almost immediately. If you run the app on another device having the same Google account, you will see the same weight entries appear on it too.
Conclusion
In this tutorial, you saw how fast and easy it is to create a fully functional weight tracker app for Android using Cloud Firestore as a database. Feel free to add more functionality to it! I also suggest you try publishing it on Google Play. With the Firebase Spark plan, which currently offers 1 GB of data storage for free, you will have no problems serving at least a few thousand users.
And while you’re here, check out some of our other posts about Android app development!
-
Android SDKHow to Code a Navigation Drawer for an Android App
-
Android SDKGetting Started With Cloud Firestore for Android
-
Android SDKHow to Create an Android Chat App Using Firebase
Powered by WPeMatico