HW5: Trying out Firebase Firestore cloud storage
This homework is based on the base project we used in Lab 5 . It's recommended to complete the lab before proceeding. The goal is to get to know some basics of Firestore and switch out the Room local database (done in the lab) with a Firestore DB.
Setting up Firebase (1 pt)
We will need to create a new Firebase project and use Firestore database for it. This requires a Google account.
Show steps for creating the Firebase project online
Go to https://console.firebase.google.com/ and create a new project
- You can turn off Analytics when asked
- When the project has been created, select the Android icon under "Get started by adding Firebase to your app ". At this stage you should also create a new Android Studio project or have the lab project handy.
- Provide the package name of the new App (ee.ut.cs.<<myAppName>> , for example). The one provided to Firebase MUST match the actual one used in the app!
- After proceeding, you will be presented a google-services.json file. This contains necessary data for your app to securely connect to Firebase.
- The registration wizard on the webpage provides shows guidelines how to add the .json to your Android Studio project. Follow the instructions.
- Follow the the next steps instructions regarding necessary SDK / Gradle dependencies. Note that you have to modify 2 build.gradle files! (Project-level build.gradle and Module-level build.gradle).
- For the Module-level build.gradle dependencies{ .. },
implementation 'com.google.firebase:firebase-firestore-ktx:24.3.1'
is sufficient.
- For the Module-level build.gradle dependencies{ .. },
Once you finish the above steps, on Firebase website / web console, add Firestore to your app (E.g. left-hand side menu Build -> Firestore Database -> Create Database )
- When asked about production or test mode, you can choose test mode, which gives read & write access to everybody who can reach your DB! Check here for more details on security in firestore.
- For region (firestore location), you can choose something in Europe.
Try to create a collection and add some documents from the web project manager.
Verifying your App can use Firestore (show steps)
Just for testing, let's try adding some data, e.g. from our App's MainActivity (onCreate or similar).
In ViewModel (or Activity), try to access the database:
// Get instance of database val db = FirebaseFirestore.getInstance()
- We reference a collection named "cities" (it will be created if it doesn't exist) and add 2 documents to it with this code: (Show Code)
val cities = db.collection("cities") val dataTallinn = hashMapOf( "name" to "Tallinn", "population" to 434562, "area_km2" to 159 ) cities.document("TLL").set(dataTallinn) .addOnSuccessListener { Log.d(TAG, "Document successfully written!") } .addOnFailureListener { e -> Log.w(TAG, "Error writing document", e) } val dataTartu = hashMapOf( "name" to "Tartu", "population" to 93865, "area_km2" to 39 ) cities.document("TRT").set(dataTartu)
(You may comment out the code once it has been created on the first run, Firestore doesn't raise exceptions by default when inserting duplicate data).
After running your app with the above steps, the data should become visible in the web dashboard of Firestore. This part was just to test you have set up Firestore correctly, you should delete it for the final HW solution.
Task Requirements
Now, study the Firestore Getting Started Guide and other Docs and try to update the project so that the following functionality works and is using Firestore:
- Listing all recipes in the DB in MainActivity (2 pts)
- Should use a query that queries for all recipes (all Firestore documents in the recipes collection), but sorts them by recipe title!
- Creation of new Recipes ( 1pt)
- Insert 1 new document based on the user-entered values, should use a query similar to the 'cities' example above
- Display of Recipe Details view ( 1 pts)
- Should work based on a query that selects/filters by ID! (get a document by its ID)
- Note that Firestore document ID-s are Strings, not Ints
- Should work based on a query that selects/filters by ID! (get a document by its ID)
Tips
Some pointers
- How to query/read data and log it?
- How to sort results?
- What are documents and collections?
- Lecture 5 slides PDF has some basic examples of Firestore usage at the end.
Tip: Updating the UI / Activity / Adapter after getting Firebase query results
The Firebase library makes requests to the DB asynchronously and you get the results in callback methods (e.g. via .addOnSuccessListener { result -> /** do something with the query result */
} .
The UI should be notified that new data is available from inside the callback, not right after the code block defining the request. In the lab, we set Room not to make requests asynchronously. This means that code like this:
// MainActivity override fun onResume() { super.onResume() model.refresh() // uses DB to update model.recipeArray recipesAdapter.data = model.recipeArray recipesAdapter.notifyDataSetChanged() }
will not work well out of the box for Firestore, since model.refresh() with firebase will do most of its work asynchronously in the background, meaning that the next lines of code (which update the adapter) will execute before the background DB query has finished. As a result, you may not immediately see the latest data in your RecyclerView with this approach.
To overcome this, here are some options:
- If you are unsure, go ahead and make all Firestore queries in your Activities and ignore using ViewModel completely for this homework.
- A nicer approach would be to define your own callback / interface
- Recall how we defined and used a MessageHandler interface in Lab4. There, it allowed the BroadcastReceiver to trigger MainActivity's adapter.
- In this homework, you could introduce a similar interface as an extra argument to the refresh() method, for instance. Then the ViewModel, after pulling data from the DB, can trigger some logic in MainActivity through the interface, just like MessageHandler in Lab4.
In week 6, we will learn more about how to manage background work.
Tip: Data Type returned by Firestore
Firestore returns documents as (Hash)Maps, so a straight-forward way is to manually construct RecipeEntities based on the various values (title, author, etc) contained in the HashMap.
Alternatively you could replace RecipeEntity entirely by HashMap. So, instead of having Array<RecipeEntity> in viewmodel, you might have Array<HashMap> instead.
Another option is to use tell Firebase which Custom data class (e.g. RecipeEntitiy) to translate the querie responses to, for more info see here: https://firebase.google.com/docs/firestore/query-data/get-data#custom_objects
- You should remove all previous Room-related classes and code (Dao, LocalRecipeDb). You can re-use the RecipeEntity data class, but remove the room @-annotations.
Other Troubleshooting
If you get an error about no of method references exceeding 65,536 methods after adding the Google services libraries, you probably have to enable multidex:
https://developer.android.com/studio/build/multidex#mdex-gradle