Day 2: Firebase Database¶
Right now, everytime we restart the app, we need to rechoose our pets and the hunger level of our pets get reset. We could try to save the pet's data on our phone and retrieve that data everytime we login, but what if we signed in from a different device? Our pet data woudl be lost!
One good solution to this is to store our pets' data online in a database! We can retrieve our pet's data anywhere from this database. Luckily for us, Firebase supports databases!
Firebase Database stores data in a JSON-like structure. Meaning something like that
{
"pets_database": {
"someuserid1": {
"pet1": {
"type": "dog",
"hunger": 100
},
"pet2": {
"type": "cat",
"hunger": 80
}
},
"someuserid2": {
"pet1": {
"type": "cat",
"hunger": 88
},
"pet2": {
"type": "fish",
"hunger": 66
}
}
}
}
Note that this is a tree-like data structure with parent and child elements. We shall be using this JSON structure to store our pet data!
Creating a Firebase Database¶
-
Head back to the Firebase Console website. Select Realtime Database from the left panel.
-
Click on Create Database and just select United States for the location.
-
Select Start in Test Mode to allow all read/writes to our database, for our convenience for now. The page should now look like this:
-
You can actually interact with the database like this graphically. But we will not do it for now
Adding Firebase Realtime Database to our Android app¶
We will need to add the dependency for Realtime Database. Go to the module-level build.gradle
file and add the following line under dependencies:
implementation 'com.google.firebase:firebase-database'
Then press Sync now,
Writing to Firebase Database¶
Let us save our data to Firebase Database when we choose pets. Head over to MyPetActivity.java
.
-
First, we need to get a reference to our Firebase Database. Copy the following code into
MyPetActivity.java
, before the onCreate function. (Remember to import using alt-enter)DatabaseReference mDatabase; DatabaseReference mUserRef;
-
Before the
onCreate
function, below the part where we retrieved our pet names fromSharedPreferences
, copy the following codemDatabase = FirebaseDatabase.getInstance().getReference();
-
We now need to create references to where we want to put our pet data. We want to put our data under our userId. So get the userId from Firebase Auth using the following code
String userId = FirebaseAuth.getInstance().getCurrentUser().getUid();
-
Now to get a reference where we want to put our own user's pet data, use the following code
mUserRef = mDatabase.child("pets_database").child(userId);
-
Now that we have a reference of where to write our data, we just need to write the data in the respective fields. We have 4 data to write (The type of pet1, the hunger value of pet1, the type of pet2 and the hunger value of pet2). We shall do this using the following code
mUserRef.child("pet1").child("type").setValue(pet1); mUserRef.child("pet1").child("hunger").setValue(100); mUserRef.child("pet2").child("type").setValue(pet2); mUserRef.child("pet2").child("hunger").setValue(100);
-
We will also need to do some modifications to allow the feed button to automatically write the new hunger value to database as well. First, we need to modify the
initialiseWidgetsForPet
function to take in the petId (i.e. "pet1" or "pet2"), so that we know which pet value we are changing.private void initialiseWidgetsForPet(String petId, ImageView petImage, TextView hungerText, Button petButton, String pet, int petHungerValue) { ...
-
Right after the part where we set the Textview to the new value in the
initaliseWidgetsForPet
function, we need to write to the database the new pet hunger valuemUserRef.child(petId).child("hunger").setValue(oldHunger + 1);
-
Try out the app, try selecting your 2 pets.
-
Now check back with your Firebase Realtime Database on the website. You should now see there is new data. Click on the big "+" sign to expand all data, and you should see your data like this:
-
If you check the long string under the
pets_database
, you will realise it corresponds to your UserID in Firebase Auth (you can see it on the website)! - If you feed your pets, you should also see the pet's hunger value increasing in the Firebase Database.
Final Code for MyPetActivity.java
package com.example.virtualpets;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
public class MyPetActivity extends AppCompatActivity {
DatabaseReference mDatabase;
DatabaseReference mUserRef;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_pet);
// Set references to widgets in layout file
ImageView pet1Image = findViewById(R.id.pet1Image);
TextView pet1HungerText = findViewById(R.id.pet1HungerText);
Button pet1Button = findViewById(R.id.pet1Button);
ImageView pet2Image = findViewById(R.id.pet2Image);
TextView pet2HungerText = findViewById(R.id.pet2HungerText);
Button pet2Button = findViewById(R.id.pet2Button);
// Get copy of sharedpreferences
SharedPreferences sharedPref = getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE);
// Get Pet1, default to cat
String pet1 = sharedPref.getString("pet1", "cat");
// Get Pet2, default to dog
String pet2 = sharedPref.getString("pet2", "dog");
// Get a reference to our Firebase Realtime Database
mDatabase = FirebaseDatabase.getInstance().getReference();
// Get our user's Id
String userId = FirebaseAuth.getInstance().getCurrentUser().getUid();
// Get reference to our user's pet data
mUserRef = mDatabase.child("pets_database").child(userId);
mUserRef.child("pet1").child("type").setValue(pet1);
mUserRef.child("pet1").child("hunger").setValue(100);
mUserRef.child("pet2").child("type").setValue(pet2);
mUserRef.child("pet2").child("hunger").setValue(100);
initialiseWidgetsForPet("pet1", pet1Image, pet1HungerText, pet1Button, pet1);
initialiseWidgetsForPet("pet2", pet2Image, pet2HungerText, pet2Button, pet2);
}
private void initialiseWidgetsForPet(String petId, ImageView petImage, TextView hungerText,
Button petButton, String pet) {
// This function runs for each pet we have
// Set the image based on which pet it is
if (pet.equals("cat")) {
petImage.setImageResource(R.drawable.cat);
} else if (pet.equals("dog")) {
petImage.setImageResource(R.drawable.dog);
} else { // Else it probably is a fish
petImage.setImageResource(R.drawable.fish);
}
// Set the hungerText to 100 at first
hungerText.setText(Integer.toString(100));
// Set onclicklistener to increment hunger by 1 for each button click
petButton.setOnClickListener(view -> {
int oldHunger = Integer.parseInt(hungerText.getText().toString());
hungerText.setText(Integer.toString(oldHunger + 1));
mUserRef.child(petId).child("hunger").setValue(oldHunger + 1);
});
}
}
Reading from Firebase Realtime Database Once¶
Now that we are able to store data on the Database, we also need a way to read the data. This will allow us to access our stored pet data anytime from any device! More specifically, if we already have pet data stored on Database, we do not want to go through the choosing pet sequence all over again, and we want to immediately see our current pets and their hunger values.
In technical terms, after User Sign-in in MainActivity
, we want to:
- Check if the user already has pre-existing pet data in database
- If yes, bring them to
MyPetActivity
directly - Replace the ImageViews' image with their pets
- Replace the TextViews with their pets' actual hunger values
- We also want to constantly listen to hunger value changes and update them (e.g. we feed our pet from another device)
Let's start!
- Let's start off by going to
MainActivity
'sonActivityResult
function, when the user successfully signs in -
We will need the user's id, get that by pasting the below code, into the part right after the
Toast.makeText
where we welcome the user.String userId = user.getUid();
-
Then, we will to set up reference to our database and where we expect our user's data to be.
DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference(); DatabaseReference mUser = mDatabase.child("pets_database").child(userId);
-
Now we will need to attempt to read data from where we expect the user's data to be. We use
addListenerForSingleValueEvent()
, which lets us send a single request for the user's data. The result will be returned as aDataSnapshot
in theonDataChange
functionmUser.addListenerForSingleValueEvent(new ValueEventListener() { @Override public void onDataChange(@NonNull DataSnapshot snapshot) { } @Override public void onCancelled(@NonNull DatabaseError error) { } });
-
We need to fill in the
onDataChange
function. To check whether the data actually exists, we can useif (snapshot.exists())
. Shift the part where we navigate to theChoosePetActivity1
to theelse
statement (i.e. user pet data does not exist)public void onDataChange(@NonNull DataSnapshot snapshot) { if (snapshot.exists()) { } else { // Go to ChoosePetActivity Intent intent = new Intent(MainActivity.this, ChoosePetActivity1.class); startActivity(intent); } }
-
If the snapshot exists, we will extract out the data and put it into SharedPreferences. Note that previously we did not put hunger values in SharedPreferences. We will have to create a new key for it. Recall that we can get SharedPreferences with
SharedPreferences sharedPref = getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE);
-
Recall we can edit SharedPreferences with
sharedPref.edit()
- To extract pet 1's type, we use
snapshot.child("pet1").child("type").getValue(String.class);
. Note thatString.class
is used to tell Firebase we know we are extracting a string - To extract pet 1's hunger value, we use
snapshot.child("pet1").child("hunger").getValue(int.class);
. Note thatint.class
is used since hunger value is an int. - We repeat the same for pet 2, and commit these changes into SharedPreferences, using
apply()
. -
To sum up Steps 7-10, the code looks like this
sharedPref.edit() .putString("pet1", snapshot.child("pet1").child("type").getValue(String.class)) .putInt("pet1hunger", snapshot.child("pet1").child("hunger").getValue(int.class)) .putString("pet2", snapshot.child("pet2").child("type").getValue(String.class)) .putInt("pet2hunger", snapshot.child("pet2").child("hunger").getValue(int.class)) .apply();
-
Don't forget that we need to navigate the user to
MyPetActivity
instead now.Intent intent = new Intent(MainActivity.this, MyPetActivity.class); startActivity(intent);
-
We need to modify the code in
MyPetActivity.java
a little as well, to accept our newpet1hunger
andpet2hunger
we are passing into SharedPreferences. At the part where we get the pet types from SharedPreferences, we will get these 2 values, and set them to a default of100
if they did not exist before (i.e. pet data did not previously exist).int pet1HungerValue = sharedPref.getInt("pet1hunger", 100); int pet2HungerValue = sharedPref.getInt("pet2hunger", 100);
-
We will also need to update the part where we set the initial values for the pet's hunger in the Firebase Database. Recall that we previously set it to a constant 100, but this value may change depdending on the value we get from sharedPreferences.
mUserRef.child("pet1").child("type").setValue(pet1); mUserRef.child("pet1").child("hunger").setValue(pet1HungerValue); mUserRef.child("pet2").child("type").setValue(pet2); mUserRef.child("pet2").child("hunger").setValue(pet2HungerValue);
-
We need to modify the
initialiseWidgetsForPet
function to accept our new initial hunger values as a parameter.private void initialiseWidgetsForPet(String petId, ImageView petImage, TextView hungerText, Button petButton, String pet, int petHungerValue) { ...
-
Then within
initaliseWidgetsForPet
, we need to modify thehungerText.setText()
to set the initial hunger value to thepetHungerValue
.hungerText.setText(Integer.toString(petHungerValue));
-
Phew, we're done! If you test your app now, you should realise that if you have signed in before, you should automatically get brought to the
MyPetActivity
with your previous data.
Final code for MainActivity.java
package com.example.virtualpets;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
import com.firebase.ui.auth.AuthUI;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
import java.util.Arrays;
import java.util.List;
public class MainActivity extends AppCompatActivity {
List<AuthUI.IdpConfig> providers = Arrays.asList(
new AuthUI.IdpConfig.GoogleBuilder().build());
private static final int RC_SIGN_IN = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.startButton);
button.setOnClickListener(view -> {
startActivityForResult(
AuthUI.getInstance()
.createSignInIntentBuilder()
.setAvailableProviders(providers)
.build(),
RC_SIGN_IN);
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == RC_SIGN_IN) {
// Asserts that this result came from our FirebaseUI
if (resultCode == RESULT_OK) {
// Means the user successfully signed in
// We can now get the details of the Firebase User
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
// Get the name of the user
String userName = user.getDisplayName();
// Show a message welcoming the user
Toast.makeText(this, "Welcome " + userName, Toast.LENGTH_SHORT).show();
// Get Signed-in user's id
String userId = user.getUid();
// Get Reference to database
DatabaseReference mDatabase = FirebaseDatabase.getInstance().getReference();
// Get Reference to where we expect user's data to be
DatabaseReference mUser = mDatabase.child("pets_database").child(userId);
// Use this function to attempt to read data from our reference
mUser.addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
// Check whether user has does indeed have existing pet data
if (snapshot.exists()) {
// If data exists, we want to extract the pet types and hunger values
// And store them into sharedPreferences
SharedPreferences sharedPref = getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE);
sharedPref.edit()
.putString("pet1", snapshot.child("pet1").child("type").getValue(String.class))
.putInt("pet1hunger", snapshot.child("pet1").child("hunger").getValue(int.class))
.putString("pet2", snapshot.child("pet2").child("type").getValue(String.class))
.putInt("pet2hunger", snapshot.child("pet2").child("hunger").getValue(int.class))
.apply();
// Go to MyPetActivity
Intent intent = new Intent(MainActivity.this, MyPetActivity.class);
startActivity(intent);
} else {
// Go to ChoosePetActivity
Intent intent = new Intent(MainActivity.this, ChoosePetActivity1.class);
startActivity(intent);
}
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
}
});
} else {
Toast.makeText(this, "Sign in Failed!", Toast.LENGTH_SHORT).show();
}
}
}
}
Final code for MyPetActivity.java
package com.example.virtualpets;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;
public class MyPetActivity extends AppCompatActivity {
DatabaseReference mDatabase;
DatabaseReference mUserRef;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my_pet);
// Set references to widgets in layout file
ImageView pet1Image = findViewById(R.id.pet1Image);
TextView pet1HungerText = findViewById(R.id.pet1HungerText);
Button pet1Button = findViewById(R.id.pet1Button);
ImageView pet2Image = findViewById(R.id.pet2Image);
TextView pet2HungerText = findViewById(R.id.pet2HungerText);
Button pet2Button = findViewById(R.id.pet2Button);
// Get copy of sharedpreferences
SharedPreferences sharedPref = getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE);
// Get Pet1, default to cat
String pet1 = sharedPref.getString("pet1", "cat");
int pet1HungerValue = sharedPref.getInt("pet1hunger", 100);
// Get Pet2, default to dog
String pet2 = sharedPref.getString("pet2", "dog");
int pet2HungerValue = sharedPref.getInt("pet2hunger", 100);
// Get a reference to our Firebase Realtime Database
mDatabase = FirebaseDatabase.getInstance().getReference();
// Get our user's Id
String userId = FirebaseAuth.getInstance().getCurrentUser().getUid();
// Get reference to our user's pet data
mUserRef = mDatabase.child("pets_database").child(userId);
mUserRef.child("pet1").child("type").setValue(pet1);
mUserRef.child("pet1").child("hunger").setValue(pet1HungerValue);
mUserRef.child("pet2").child("type").setValue(pet2);
mUserRef.child("pet2").child("hunger").setValue(pet2HungerValue);
initialiseWidgetsForPet("pet1", pet1Image, pet1HungerText, pet1Button, pet1, pet1HungerValue);
initialiseWidgetsForPet("pet2", pet2Image, pet2HungerText, pet2Button, pet2, pet2HungerValue);
}
private void initialiseWidgetsForPet(String petId, ImageView petImage, TextView hungerText,
Button petButton, String pet, int petHungerValue) {
// This function runs for each pet we have
// Set the image based on which pet it is
if (pet.equals("cat")) {
petImage.setImageResource(R.drawable.cat);
} else if (pet.equals("dog")) {
petImage.setImageResource(R.drawable.dog);
} else { // Else it probably is a fish
petImage.setImageResource(R.drawable.fish);
}
hungerText.setText(Integer.toString(petHungerValue));
// Set onclicklistener to increment hunger by 1 for each button click
petButton.setOnClickListener(view -> {
int oldHunger = Integer.parseInt(hungerText.getText().toString());
hungerText.setText(Integer.toString(oldHunger + 1));
mUserRef.child(petId).child("hunger").setValue(oldHunger + 1);
});
}
}
Continously listening for updates to Firebase Realtime Database¶
Now, we need our MyPetActivity
to continuously listen for updates from the Firebase Realtime Database! This is to accomodate for the case where the user is logged in from multiple devices at the same time.
To do that, instead of addListenerForSingleValueEvent()
, we can use addValueEventListener()
, which constantly listens for changes in values. The syntax is almost the same as addListenerForSingleValueEvent()
. Think about where to put this code, how to get the correct reference, and what you need to do when the hunger value changes.
Hint: you can get the value we want with
snapshot.getValue(int.class)
<fillInWithYourRef>.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
// What should you do when the hunger value changes?
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
}
});
Answer
mUserRef.child(petId).child("hunger").addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(@NonNull DataSnapshot snapshot) {
hungerText.setText(Integer.toString(snapshot.getValue(int.class)));
}
@Override
public void onCancelled(@NonNull DatabaseError error) {
}
});