Build an Android App Using Firebase and the App Engine Flexible Environment

This tutorial demonstrates how to write a mobile app with backend data storage, real-time synchronization, and user-event logging using Firebase. Java servlets running in the Google Cloud Platform (GCP) App Engine flexible environment listen for new user logs stored in Firebase and process them.

The instructions show how to do this using Firebase and the App Engine flexible environment.

If you want your app to process user data or orchestrate events, you can extend Firebase with the App Engine flexible environment to perform automatic real-time data synchronization.

The sample app, Playchat, stores chat messages in the Firebase Realtime Database, which automatically synchronizes that data across devices. Playchat also writes user-event logs to Firebase. To learn more about how the database synchronizes data, see How does it work? in the Firebase documentation.

The following diagram shows the Playchat client architecture.

Playchat client architecture

A set of Java servlets running in the App Engine flexible environment register as listeners with Firebase. The servlets respond to new user-event logs and process the log data. The servlets use transactions to ensure that only one servlet handles each user-event log.

The following diagram shows the Playchat server architecture.

Playchat server architecture

Communication between the app and the servlet happens in three parts:

  • When a new user logs into Playchat, the app requests a logging servlet for that user by adding an entry under /inbox/ in the Firebase Realtime Database.

  • One of the servlets accepts the assignment by updating value of the entry to its servlet identifier. The servlet uses a Firebase transaction to guarantee that it is the only servlet can update the value. After the value is updated, all other servlets ignore the request.

  • When the user logs in, logs out, or changes to a new channel, Playchat logs the action in /inbox/[SERVLET_ID]/[USER_ID]/, where [SERVLET_ID] is the identifier of the servlet instance and [USER_ID] is a hash value representing the user.

  • The servlet watches the inbox for new entries and collects the log data.

In this sample app, the servlets copy the log data locally and display it on a web page. In a production version of this app, the servlets could process the log data or copy it to Cloud Storage, Cloud Bigtable, or BigQuery for storage and analysis.

Objectives

This tutorial demonstrates how to:

  • Build an Android app, Playchat, that stores data in the Firebase Realtime Database.

  • Run a Java servlet in the App Engine flexible environments that connects to Firebase and receives notifications when the data stored in Firebase changes.

  • Use these two components to build a distributed, streaming, backend service to collect and process log data.

Costs

Firebase has a free level of usage. If your usage of these services is less than the limits specified in the Firebase free plan there is no charge for using Firebase.

Instances within the App Engine flexible environment are charged the cost of the underlying Google Compute Engine Virtual Machines.

Before you begin

Install the following software:

Install the App Engine Java component of the Cloud SDK by running the following command from the command line.

gcloud components install app-engine-java

Cloning the sample code

  1. Clone the front end client app code.

    git clone https://github.com/GoogleCloudPlatform/firebase-android-client
    
  2. Clone the backend servlet code.

    git clone https://github.com/GoogleCloudPlatform/firebase-appengine-backend
    

Generating a SHA-1 fingerprint for the app

To authenticate your client app for Google Sign-in, you need to provide a SHA-1 fingerprint of the certificate. This tutorial uses the debug keystore. For information about creating release versions of the keystore fingerprint, see Authenticating Your Client.

  • Build a SHA-1 of your debug keystore.

    keytool -exportcert -list -v \
    -alias androiddebugkey -keystore ~/.android/debug.keystore
    

Creating a Firebase project

  1. Create a Firebase account or log into an existing account.

  2. Click Add project.

  3. In Project name, enter: Playchat

  4. Follow the remaining setup steps and click Create project.

  5. After the wizard provisions your project, click Continue.

  6. In the Overview page of your project, click the Settings gear and then click Project settings.

  7. Click Add Firebase to your Android app.

  8. In Android package name, enter: com.google.cloud.solutions.flexenv.

  9. In Debug signing certificate SHA-1, enter the SHA-1 value you generated in the previous section.

  10. Click Register app.

  11. Follow the steps in the Download config file section to add the google-services.json file to your project.

  12. Click Next in the Download config file section.

  13. Make a note of the suggested changes to the project- and app-level build.gradle files.

  14. Click Next in the Add Firebase SDK section.

  15. Click Skip this step in the Run your app to verify installation section.

Creating a Realtime Database

  1. From the left menu of the Firebase console, select Database in the Develop group.

  2. In the Database page, go to the Realtime Database section and click Create database.

  3. In the Security rules for Realtime Database dialog, select Start in test mode and click Enable.

    This step displays the data you’ve stored in Firebase. In later steps of this tutorial, you can revisit this web page to see data added and updated by the client app and backend servlet.

  4. Make a note of the Firebase URL for your project, which is in the form https://[FIREBASE_PROJECT_ID].firebaseio.com/ and appears next to a link icon.

Enabling Google authentication for the Firebase project

There are a variety of sign-in providers you can configure to connect to your Firebase project. This tutorial walks you through setting up authentication so that the user can sign in using a Google Account.

  1. From the left menu of the Firebase console, click Authentication in the Develop group.

  2. Click Set up sign-in method.

  3. Select Google, turn the Enable toggle on, and click Save.

Adding a service account to the Firebase project

The backend servlet doesn't use a Google Account to sign-in with. Instead, it uses a service account to connect to Firebase. The following steps walk you through creating a service account that can connect to Firebase and adding the service account credentials to the servlet code.

  1. From the left menu of the Firebase console, next to the Playchat project home, select the Settings gear and then Project settings.

  2. Select Service accounts and then Manage all service accounts.

  3. Click CREATE SERVICE ACCOUNT.

  4. Configure the following settings:

    1. In Service account name, enter playchat-servlet.
    2. In Role, select Project > Owner.

    3. Check Furnish a new private key.

    4. Select JSON for Key Type.
  5. Click Create.

  6. Download the JSON key file for the service account and save to the backend service project, firebase-appengine-backend, in the src/main/webapp/WEB-INF/ directory. The filename will be in the form Playchat-[UNIQUE_ID].json.

  7. Edit src/main/webapp/WEB-INF/web.xml and edit the initialization parameters as follows:

    • Replace JSON_FILE_NAME with the name of the JSON key file you downloaded.

    • Replace FIREBASE_URL with the Firebase URL you noted earlier.

      <init-param>
        <param-name>credential</param-name>
        <param-value>/WEB-INF/JSON_FILE_NAME</param-value>
      </init-param>
      <init-param>
        <param-name>databaseUrl</param-name>
        <param-value>FIREBASE_URL</param-value>
      </init-param>
      

Enabling billing and APIs for the Cloud Platform project

In order for the backend service to run on Cloud Platform, you need to enable billing and APIs for the project. The Cloud Platform project is the same project you created in Create a Firebase project and has the same project identifier.

  1. In the Cloud Platform Console, select the Playchat project.

    Go to the Projects page

  2. Make sure that billing is enabled for your project.

    Learn how to enable billing

  3. Enable the App Engine Admin API and Compute Engine API APIs.

    Enable the APIs

Building and deploying the backend service

The backend service in this sample uses a Docker configuration to specify its hosting environment. This customization means you must use the App Engine flexible environment instead of App Engine standard environment.

To build the backend servlet and deploy it in the App Engine flexible environment, you can use the Google App Engine Maven plugin. This plugin is already specified in the Maven build file included with this sample.

Setting the project

For Maven to build the backend servlet correctly, you must provide it the Google Cloud Platform (GCP) project to launch the servlet resources into. The GCP project identifier and the Firebase project identifier are the same.

  1. Provide the credentials that the gcloud tool uses to access GCP.

    gcloud auth login
    
  2. Set the project to your Firebase project with the following command, replacing [FIREBASE_PROJECT_ID] with the name of the Firebase project ID you noted previously.

    gcloud config set project [FIREBASE_PROJECT_ID]
    
  3. Verify that the project has been set by listing the configuration.

    gcloud config list
    

(Optional) Running the service on the local server

When you develop a new backend service, run the service locally before you deploy it to App Engine to rapidly iterate changes without the overhead of a full deployment to App Engine.

When you run the server locally, it doesn’t use a Docker configuration or run in an App Engine environment. Instead, Maven guarantees all dependent libraries are installed locally and the app runs on Jetty.

  1. In the firebase-appengine-backend directory, build and run the backend module locally with the following command:

    mvn clean package appengine:run
    
    mvn clean package appengine:run -Dgcloud.gcloud_directory=[PATH_TO_TOOL]
    
  2. If you are prompted Do you want the application "Python.app" to accept incoming network connections? select Allow.

When the deployment ends, open http://localhost:8080/printLogs to verify that your backend service is running. You should see a web page that displays “Inbox :” followed by a 16-digit identifier. This is the inbox identifier for the servlet running on your local machine.

As you refresh the page, this identifier does not change; your local server spins up a single servlet instance. This is useful for testing; there is only one servlet identifier stored in the Firebase Realtime Database.

To shut down the local server, click Ctrl+C.

Deploying the service to the App Engine flexible environment

When you run the backend service in the App Engine flexible environment, App Engine uses the configuration in /firebase-appengine-backend/src/main/webapp/Dockerfiles to build the hosting environment that the service runs in. The flexible environment spins up several servlet instances and scales them up and down to meet demand.

  • In the firebase-appengine-backend directory, build and run the backend module locally with the following command:

    mvn clean package appengine:deploy
    
    mvn clean package appengine:deploy -Dgcloud.gcloud_directory=[PATH_TO_GCLOUD]
    

As the build runs, you see the lines “Sending build context to Docker daemon…”. The previous command uploads your Docker configuration and sets it up in the App Engine flexible environment.

When the deployment ends, open https://[FIREBASE_PROJECT_ID].appspot.com/printLogs, where [FIREBASE_PROJECT_ID] is the identifier from Create a Firebase project. You should see a web page that displays “Inbox :” followed by a 16-digit identifier. This is the inbox identifier for a servlet running in the App Engine flexible environment.

As you refresh the page, this identifier should periodically change as App Engine spins up multiple servlet instances to handle incoming client requests.

Adding Firebase and Google Play Services to the Android app

The client app uses the Firebase Realtime Database to store and sync messages and to record user-event logs. The client app uses Google Play Services to authenticate users with their Google account.

  1. In Android Studio, select Tools > SDK Manager.

  2. At the top of the right pane, select SDK Tools.

  3. Select Google Play services if it's not already selected.

  4. Click OK.

  5. Select File > Open… and choose the firebase-android-client directory.

  6. Wait for the Gradle project information to finish building. If you are prompted to use the Gradle wrapper, click OK.

  7. The changes to the project- and app-level build.gradle files that you noted in Create a Firebase project have already been made in the sample code.

Running and testing the Android app

  1. In Android Studio, with the firebase-android-client project open, select Run > Run 'app'.

  2. Select a device or emulator running Android 6.0 with Google APIs as your test device.

  3. When the app is loaded onto the device, sign in with your Google Account.

    Sign in to Playchat

  4. Click the menu to the left of the PlayChat title and select the books channel.

    Select a channel

  5. Enter a message.

    Send a message

  6. When you do, the Playchat app stores your message in the Firebase Realtime Database. Firebase synchronizes the data stored in the database across devices. Devices running Playchat will display the new message when a user selects the books channel.

    Send a message

Verifying the data

After you’ve used the Playchat app to generate some user events using the Playchat app, you can verify that the servlets are registering as listeners and collecting user-event logs.

Open the Firebase Realtime Database for your app, where [FIREBASE_PROJECT_ID] is the identifier from Create a Firebase project.

https://console.firebase.google.com/project/[FIREBASE_PROJECT_ID]/database/data

At the bottom of the Firebase Realtime Database, under the /inbox/ data location, there are a group of nodes prefixed by client- and followed by a randomly generated key that represents a user’s account login. The last entry in this example, client-1240563753, is followed by a 16-digit identifier of the servlet currently listening to log events from that user, in this example 0035806813827987.

Data stored in Firebase Realtime Database

Immediately above, under the /inbox/ data location, are servlet identifiers for all currently assigned servlets. In this example, only one servlet is collecting logs. Under /inbox/[SERVLET_IDENTIFIER] are the user logs written by the app to that servlet.

Open the App Engine page for your backend service: https://[FIREBASE_PROJECT_ID].appspot.com/printLogs, where [FIREBASE_PROJECT_ID] is the identifier from Create a Firebase project. When the page displays the identifier for the servlet that recorded the user events you generated, you will see log entries for those events below the servlet’s inbox identifier.

Exploring the code

The Playchat Android app defines a class, FirebaseLogger, that it uses to write user-event logs to the Firebase Realtime Database.

import com.google.firebase.database.DatabaseReference;
import com.google.cloud.solutions.flexenv.common.LogEntry;

/*
 * FirebaseLogger pushes user event logs to a specified path.
 * A backend Servlet instance listens to
 * the same key and keeps track of event logs.
 */
class FirebaseLogger {
    private final DatabaseReference logRef;

    FirebaseLogger(DatabaseReference firebase, String path) {
        logRef = firebase.child(path);
    }

    public void log(String tag, String message) {
        LogEntry entry = new LogEntry(tag, message);
        logRef.push().setValue(entry);
    }

}

When a new user logs in, Playchat calls the requestLogger function to add a new entry to the /inbox/ location in the Firebase Realtime Database and set a listener so Playchat can respond when a servlet updates the value of that entry, accepting the assignment.

When a servlet updates the value, Playchat removes the listener and writes the “Signed in” log to the servlet’s inbox.

/*
 * Request that a Servlet instance be assigned.
 */
private void requestLogger() {
    databaseReference = FirebaseDatabase.getInstance().getReference();
    databaseReference.child(IBX + "/" + inbox).removeValue();
    databaseReference.child(IBX + "/" + inbox).addValueEventListener(new ValueEventListener() {
        public void onDataChange(@NonNull DataSnapshot snapshot) {
            if (snapshot.exists() && snapshot.getValue(String.class) != null) {
                fbLog = new FirebaseLogger(
                        databaseReference,
                        IBX + "/" + snapshot.getValue(String.class) + "/logs"
                );
                databaseReference.child(IBX + "/" + inbox).removeEventListener(this);
            }
        }

        public void onCancelled(@NonNull DatabaseError error) {
            Log.e(TAG, error.getDetails());
        }
    });

    databaseReference.child(REQLOG).push().setValue(inbox);
}

On the backend service side, when a servlet instance starts, the init(ServletConfig config) function in MessageProcessorServlet.java connects to the Firebase Realtime Database and adds a listener to the /inbox/ data location.

When a new entry is added to the /inbox/ data location, the servlet updates the value with its identifier, a signal to the Playchat app that the servlet accepts the assignment to process logs for that user. The servlet uses Firebase transactions to ensure that only one servlet can update the value and accept the assignment.

/*
 * Receive a request from an Android client and reply back its inbox ID.
 * Using a transaction ensures that only a single Servlet instance replies
 * to the client. This lets the client knows to which Servlet instance
 * to send consecutive user event logs.
 */
firebase.child(REQLOG).addChildEventListener(new ChildEventListener() {
  public void onChildAdded(DataSnapshot snapshot, String prevKey) {
    firebase.child(IBX + "/" + snapshot.getValue()).runTransaction(new Transaction.Handler() {
      public Transaction.Result doTransaction(MutableData currentData) {
        // The only first Servlet instance will write
        // its ID to the client inbox.
        if (currentData.getValue() == null) {
          currentData.setValue(inbox);
        }
        return Transaction.success(currentData);
      }

      public void onComplete(DatabaseError error, boolean committed, DataSnapshot snapshot) {}
    });
    firebase.child(REQLOG).removeValue();
  }
  // ...
});

After a servlet has accepted an assignment to process a user’s event logs, it adds a listener that detects when the Playchat app writes a new log file to the servlet’s inbox. The servlet responds by retrieving the new log data from the Firebase Realtime Database.

/*
 * Initialize user event logger. This is just a sample implementation to
 * demonstrate receiving updates. A production version of this application
 * should transform, filter or load to other data store such as Google BigQuery.
 */
private void initLogger() {
  String loggerKey = IBX + "/" + inbox + "/logs";
  purger.registerBranch(loggerKey);
  firebase.child(loggerKey).addChildEventListener(new ChildEventListener() {
    public void onChildAdded(DataSnapshot snapshot, String prevKey) {
      if (snapshot.exists()) {
        LogEntry entry = snapshot.getValue(LogEntry.class);
        logs.add(entry);
      }
    }

    public void onCancelled(DatabaseError error) {
      localLog.warning(error.getDetails());
    }

    public void onChildChanged(DataSnapshot arg0, String arg1) {}

    public void onChildMoved(DataSnapshot arg0, String arg1) {}

    public void onChildRemoved(DataSnapshot arg0) {}
  });
}

Cleaning up

To avoid incurring charges to your Google Cloud Platform account for the resources used in this tutorial:

Delete the Cloud Platform and Firebase project

The simplest way to stop billing charges is to delete the project you created for this tutorial. Although you created the project in the Firebase console, you can also delete it in the Cloud Platform console, since the Firebase and Cloud Platform projects are one and the same.

  1. In the GCP Console, go to the Projects page.

    Go to the Projects page

  2. In the project list, select the project you want to delete and click Delete project. After selecting the checkbox next to the project name, click
      Delete project
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

Delete non-default versions your App Engine app

If you don't want to delete your Cloud Platform and Firebase project, you can reduce costs by deleting the non-default versions of your App Engine flexible environment app.

  1. In the GCP Console, go to the App Engine Versions page.

    Go to the Versions page

  2. Click the checkbox next to the non-default app version you want to delete.
  3. Click the Delete button at the top of the page to delete the app version.

What's next

  • Analyze and archive data — In this sample, the servlets store log data only in memory. To extend this sample, you could have the servlets archive, transform, and analyze the data, using services such as Cloud Storage, Cloud Bigtable, Google Cloud Dataflow, and BigQuery.

  • Evenly distribute the workload across servlets — App Engine provides both automatic and manual scaling. With automatic scaling, the flexible environmment detects increases or decreases in workload and responds by adding or removing VMs in the cluster. With manual scaling, you specify a static number of VMs to handle traffic. For more information about how to configure scaling, see Service scaling settings in the App Engine documentation.

    Because user activity logs are assigned to the servlets through accessing the Firebase Realtime Database, the workload might not be evenly distributed. For example, one servlet might process more user-event logs than other servlets.

    You can improve efficiency by implementing a workload manager that independently controls workload on each VM. This workload balancing could be based on metrics like logging requests per second or number of concurrent clients.

  • Recover unprocessed user-event logs — In this sample implementation, if a servlet instance crashes, the Android app associated with that instance continues to send log events to the servlet’s inbox in the Firebase Realtime Database. In a production version of this app, the backend service should detect this situation to recover the unprocessed user-event logs.

Was this page helpful? Let us know how we did:

Send feedback about...