Migrate your Parse Android App to Firebase

If you are a Parse user looking for an alternative Backend as a Service solution, Firebase might be the ideal choice for your Android app.

This guide describes how to integrate specific services into your app. For basic Firebase setup instructions, see the Android Setup guide.

Google Analytics

Google Analytics is a free app measurement solution that provides insight on app usage and user engagement. Analytics integrates across Firebase features and provides you with unlimited reporting for up to 500 distinct events that you can define using the Firebase SDK.

See the Google Analytics docs to learn more.

Suggested Migration Strategy

Using different analytics providers is a common scenario that easily applies to Google Analytics. Just add it to your app to benefit from events and user properties that Analytics automatically collects, like first open, app update, device model, age.

For custom events and user properties, you can employ a double write strategy using both Parse Analytics and Google Analytics to log events and properties, which allows you to gradually roll out the new solution.

Code Comparison

Parse Analytics

// Start collecting data
ParseAnalytics.trackAppOpenedInBackground(getIntent());

Map<String, String> dimensions = new HashMap<String, String>();
// Define ranges to bucket data points into meaningful segments
dimensions.put("priceRange", "1000-1500");
// Did the user filter the query?
dimensions.put("source", "craigslist");
// Do searches happen more often on weekdays or weekends?
dimensions.put("dayType", "weekday");

// Send the dimensions to Parse along with the 'search' event
ParseAnalytics.trackEvent("search", dimensions);

Google Analytics

// Obtain the FirebaseAnalytics instance and start collecting data
mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);

Bundle params = new Bundle();
// Define ranges to bucket data points into meaningful segments
params.putString("priceRange", "1000-1500");
// Did the user filter the query?
params.putString("source", "craigslist");
// Do searches happen more often on weekdays or weekends?
params.putString("dayType", "weekday");

// Send the event
mFirebaseAnalytics.logEvent("search", params);

Firebase Realtime Database

The Firebase Realtime Database is a NoSQL cloud-hosted database. Data is stored as JSON and synchronized in real time to every connected client.

See the Firebase Realtime Database docs to learn more.

Differences With Parse Data

Objects

In Parse you store a ParseObject, or a subclass of it, that contains key-value pairs of JSON-compatible data. The data is schemaless, which means you don't need to specify what keys exists on each ParseObject.

All Firebase Realtime Database data is stored as JSON objects, and there is no equivalent for ParseObject; you simply write to the JSON tree values of types that correspond to the available JSON types. You can use Java objects to simplify reading and writing from the database.

The following is an example of how you might save the high scores for a game.

Parse
@ParseClassName("GameScore")
public class GameScore {
        public GameScore() {}
        public GameScore(Long score, String playerName, Boolean cheatMode) {
            setScore(score);
            setPlayerName(playerName);
            setCheatMode(cheatMode);
        }

        public void setScore(Long score) {
            set("score", score);
        }

        public Long getScore() {
            return getLong("score");
        }

        public void setPlayerName(String playerName) {
            set("playerName", playerName);
        }

        public String getPlayerName() {
            return getString("playerName");
        }

        public void setCheatMode(Boolean cheatMode) {
            return set("cheatMode", cheatMode);
        }

        public Boolean getCheatMode() {
            return getBoolean("cheatMode");
        }
}

// Must call Parse.registerSubclass(GameScore.class) in Application.onCreate
GameScore gameScore = new GameScore(1337, "Sean Plott", false);
gameScore.saveInBackground();
Firebase
// Assuming we defined the GameScore class as:
public class GameScore {
        private Long score;
        private String playerName;
        private Boolean cheatMode;

        public GameScore() {}
        public GameScore(Long score, String playerName, Boolean cheatMode) {
            this.score = score;
            this.playerName = playerName;
            this.cheatMode = cheatMode;
        }

        public Long getScore() {
            return score;
        }

        public String getPlayerName() {
            return playerName;
        }

        public Boolean getCheatMode() {
            return cheatMode;
        }
}

// We would save it to our list of high scores as follows:
DatabaseReference mFirebaseRef = FirebaseDatabase.getInstance().getReference();
GameScore score = new GameScore(1337, "Sean Plott", false);
mFirebaseRef.child("scores").push().setValue(score);
For more details, check the Read and Write Data on Android guide.

Relationships Between Data

A ParseObject can have a relationship with another ParseObject: any object can use other objects as values.

In the Firebase Realtime Database, relations are better expressed using flat data structures that split the data into separate paths, so that they can be efficiently downloaded in separate calls.

The following is an example of how you might structure the relationship between posts in a blogging app and their authors.

Parse
// Create the author
ParseObject myAuthor = new ParseObject("Author");
myAuthor.put("name", "Grace Hopper");
myAuthor.put("birthDate", "December 9, 1906");
myAuthor.put("nickname", "Amazing Grace");

// Create the post
ParseObject myPost = new ParseObject("Post");
myPost.put("title", "Announcing COBOL, a New Programming Language");

// Add a relation between the Post and the Author
myPost.put("parent", myAuthor);

// This will save both myAuthor and myPost
myPost.saveInBackground();
Firebase
DatabaseReference firebaseRef = FirebaseDatabase.getInstance().getReference();
// Create the author
Map<String, String> myAuthor = new HashMap<String, String>();
myAuthor.put("name", "Grace Hopper");
myAuthor.put("birthDate", "December 9, 1906");
myAuthor.put("nickname", "Amazing Grace");

// Save the author
String myAuthorKey = "ghopper";
firebaseRef.child('authors').child(myAuthorKey).setValue(myAuthor);

// Create the post
Map<String, String> post = new HashMap<String, String>();
post.put("author", myAuthorKey);
post.put("title", "Announcing COBOL, a New Programming Language");
firebaseRef.child('posts').push().setValue(post);

The following data layout is the result.

{
  // Info about the authors
  "authors": {
    "ghopper": {
      "name": "Grace Hopper",
      "date_of_birth": "December 9, 1906",
      "nickname": "Amazing Grace"
    },
    ...
  },
  // Info about the posts: the "author" fields contains the key for the author
  "posts": {
    "-JRHTHaIs-jNPLXOQivY": {
      "author": "ghopper",
      "title": "Announcing COBOL, a New Programming Language"
    }
    ...
  }
}
For more details, check the Structure Your Database guide.

Reading Data

In Parse you read data using either the ID of a specific Parse object, or executing queries using ParseQuery.

In Firebase, you retrieve data by attaching an asynchronous listener to a database reference. The listener is triggered once for the initial state of the data and again when the data changes, so you won't need to add any code to determine if the data changed.

The following is an example of how you can retrieve scores for a particular player, based on the example presented in the "Objects" section.

Parse
ParseQuery<ParseObject> query = ParseQuery.getQuery("GameScore");
query.whereEqualTo("playerName", "Dan Stemkoski");
query.findInBackground(new FindCallback<ParseObject>() {
    public void done(List<ParseObject> scoreList, ParseException e) {
        if (e == null) {
            for (ParseObject score: scoreList) {
                Log.d("score", "Retrieved: " + Long.toString(score.getLong("score")));
            }
        } else {
            Log.d("score", "Error: " + e.getMessage());
        }
    }
});
Firebase
DatabaseReference mFirebaseRef = FirebaseDatabase.getInstance().getReference();
Query mQueryRef = mFirebaseRef.child("scores").orderByChild("playerName").equalTo("Dan Stemkoski");

// This type of listener is not one time, and you need to cancel it to stop
// receiving updates.
mQueryRef.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot snapshot, String previousChild) {
        // This will fire for each matching child node.
        GameScore score = snapshot.getValue(GameScore.class);
        Log.d("score", "Retrieved: " + Long.toString(score.getScore());
    }
});
For more details on available types of event listeners and on how to order and filter data, check the Read and Write Data on Android guide.

Suggested Migration Strategy

Rethink Your Data

The Firebase Realtime Database is optimized to sync data in milliseconds across all connected clients, and the resulting data structure is different from the Parse core data. This means that the first step of your migration is to consider what changes your data requires, including:

  • How your Parse objects should map to Firebase data
  • If you have parent-child relations, how to split your data across different paths so that it can be efficiently downloaded in separate calls.

Migrate Your Data

After you decide how to structure your data in Firebase, you need to plan how to handle the period during which your app needs to write to both databases. Your choices are:

Background Sync

In this scenario, you have two versions of the app: the old version that uses Parse and a new version that uses Firebase. Syncs between the two databases are handled by Parse Cloud Code (Parse to Firebase), with your code listening to changes on Firebase and syncing those changes with Parse. Before you can start using the new version, you must:

  • Convert your existing Parse Data to the new Firebase structure, and write it to the Firebase Realtime Database.
  • Write Parse Cloud Code functions that use the Firebase REST API to write to the Firebase Realtime Database changes made in the Parse Data by old clients.
  • Write and deploy code that listens to changes on Firebase and syncs them to the Parse database.

This scenario ensures a clean separation of old and new code, and keeps the clients simple. The challenges of this scenario are handling big datasets in the initial export, and ensuring that the bidirectional sync doesn't generate infinite recursion.

Double Write

In this scenario, you write a new version of the app that uses both Firebase and Parse, using Parse Cloud Code to sync changes made by old clients from the Parse Data to the Firebase Realtime Database. When enough people have migrated from the Parse-only version of the app, you can remove the Parse code from the double write version.

This scenario doesn't require any server side code. Its disadvantages are that data that is not accessed is not migrated, and that the size of your app is increased by the usage of both SDKs.

Firebase Authentication

Firebase Authentication can authenticate users using passwords and popular federated identity providers like Google, Facebook and Twitter. It also provides UI libraries to save you the significant investment required to implement and maintain a full authentication experience for your app across all platforms.

See the Firebase Authentication docs to learn more.

Differences With Parse Auth

Parse provides a specialized user class called ParseUser that automatically handles the functionality required for user account management. ParseUser is a subclass of the ParseObject, which means user data is available in the Parse Data and can be extended with additional fields like any other ParseObject.

A FirebaseUser has a fixed set of basic properties—a unique ID, a primary email address, a name and a photo URL—stored in a separate project's user database; those properties can be updated by the user. You cannot add other properties to the FirebaseUser object directly; instead, you can store the additional properties in your Firebase Realtime Database.

The following is an example of how you might sign up a user and add an additional phone number field.

Parse
ParseUser user = new ParseUser();
user.setUsername("my name");
user.setPassword("my pass");
user.setEmail("[email protected]");

// other fields can be set just like with ParseObject
user.put("phone", "650-253-0000");

user.signUpInBackground(new SignUpCallback() {
    public void done(ParseException e) {
        if (e == null) {
            // Hooray! Let them use the app now.
        } else {
            // Sign up didn't succeed. Look at the ParseException
            // to figure out what went wrong
        }
    }
});
Firebase
FirebaseAuth mAuth = FirebaseAuth.getInstance();

mAuth.createUserWithEmailAndPassword("[email protected]", "my pass")
    .continueWithTask(new Continuation<AuthResult, Task<Void>> {
        @Override
        public Task<Void> then(Task<AuthResult> task) {
            if (task.isSuccessful()) {
                FirebaseUser user = task.getResult().getUser();
                DatabaseReference firebaseRef = FirebaseDatabase.getInstance().getReference();
                return firebaseRef.child("users").child(user.getUid()).child("phone").setValue("650-253-0000");
            } else {
                // User creation didn't succeed. Look at the task exception
                // to figure out what went wrong
                Log.w(TAG, "signInWithEmail", task.getException());
            }
        }
    });

Suggested Migration Strategy

Migrate Accounts

To migrate user accounts from Parse to Firebase, export your user database to a JSON or CSV file, then import the file into your Firebase project using the Firebase CLI's auth:import command.

First, export your user database from the Parse console or your self-hosted database. For example, a JSON file exported from the Parse console might look like the following:

{ // Username/password user
  "bcryptPassword": "$2a$10$OBp2hxB7TaYZgKyTiY48luawlTuYAU6BqzxJfpHoJMdZmjaF4HFh6",
  "email": "[email protected]",
  "username": "testuser",
  "objectId": "abcde1234",
  ...
},
{ // Facebook user
  "authData": {
    "facebook": {
      "access_token": "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
      "expiration_date": "2017-01-02T03:04:05.006Z",
      "id": "1000000000"
    }
  },
  "username": "wXyZ987654321StUv",
  "objectId": "fghij5678",
  ...
}

Then, transform the exported file into the format required by the Firebase CLI. Use the objectId of your Parse users as the localId of your Firebase users. Also, base64 encode the bcryptPassword values from Parse and use them in the passwordHash field. For example:

{
  "users": [
    {
      "localId": "abcde1234",  // Parse objectId
      "email": "[email protected]",
      "displayName": "testuser",
      "passwordHash": "JDJhJDEwJE9CcDJoeEI3VGFZWmdLeVRpWTQ4bHVhd2xUdVlBVTZCcXp4SmZwSG9KTWRabWphRjRIRmg2",
    },
    {
      "localId": "fghij5678",  // Parse objectId
      "displayName": "wXyZ987654321StUv",
      "providerUserInfo": [
        {
          "providerId": "facebook.com",
          "rawId": "1000000000",  // Facebook ID
        }
      ]
    }
  ]
}

Finally, import the transformed file with the Firebase CLI, specifying bcrypt as the hash algorithm:

firebase auth:import account_file.json --hash-algo=BCRYPT

Migrate User Data

If you are storing additional data for your users, you can migrate it to Firebase Realtime Database using the strategies described in the data migration section. If you migrate accounts using the flow described in the accounts migration section, your Firebase accounts have the same ids of your Parse accounts, allowing you to easily migrate and reproduce any relations keyed by the user id.

Firebase Cloud Messaging

Firebase Cloud Messaging (FCM) is a cross-platform messaging solution that lets you reliably deliver messages and notifications at no cost. The Notifications composer is a no-cost service built on Firebase Cloud Messaging that enables targeted user notifications for mobile app developers.

See the Firebase Cloud Messaging docs to learn more.

Differences With Parse Push Notifications

Every Parse application installed on a device registered for notifications has an associated Installation object, where you store all the data needed to target notifications. Installation is a subclass of ParseUser, which means you can add any additional data you want to your Installation instances.

The Notifications composer provides predefined user segments based on information like app, app version and device language. You can build more complex user segments using Google Analytics events and properties to build audiences. See the audiences help guide to learn more. These targeting informations are not visible in the Firebase Realtime Database.

Suggested Migration Strategy

Migrating Device Tokens

At the time of writing, the Parse Android SDK uses an older version of the FCM registration tokens, not compatible with the features offered by the Notifications composer.

You can get a new token by adding the FCM SDK to your app; however, this might invalidate the token used by the Parse SDK to receive notifications. If you want to avoid that, you can set up the Parse SDK to use both Parse's sender ID and your sender ID. In this way you don't invalidate the token used by the Parse SDK, but be aware that this workaround will stop working when Parse shuts down its project.

Migrating Channels To FCM Topics

If you are using Parse channels to send notifications, you can migrate to FCM topics, which provide the same publisher-subscriber model. To handle the transition from Parse to FCM, you can write a new version of the app that uses the Parse SDK to unsubscribe from Parse channels and the FCM SDK to subscribe to corresponding FCM topics. In this version of the app you should disable receiving notifications on the Parse SDK, removing the following from your app's manifest:

<service android:name="com.parse.PushService" />
<receiver android:name="com.parse.ParsePushBroadcastReceiver"
  android:exported="false">
<intent-filter>
<action android:name="com.parse.push.intent.RECEIVE" />
<action android:name="com.parse.push.intent.DELETE" />
<action android:name="com.parse.push.intent.OPEN" />
</intent-filter>
</receiver>
<receiver android:name="com.parse.GcmBroadcastReceiver"
  android:permission="com.google.android.c2dm.permission.SEND">
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<action android:name="com.google.android.c2dm.intent.REGISTRATION" />

<!--
IMPORTANT: Change "com.parse.starter" to match your app's package name.
-->
<category android:name="com.parse.starter" />
</intent-filter>
</receiver>

<!--
IMPORTANT: Change "YOUR_SENDER_ID" to your GCM Sender Id.
-->
<meta-data android:name="com.parse.push.gcm_sender_id"
  android:value="id:YOUR_SENDER_ID" />;

For example, if your user is subscribed to the "Giants" topic, you would do something like:

ParsePush.unsubscribeInBackground("Giants", new SaveCallback() {
    @Override
    public void done(ParseException e) {
        if (e == null) {
            FirebaseMessaging.getInstance().subscribeToTopic("Giants");
        } else {
            // Something went wrong unsubscribing
        }
    }
});

Using this strategy, you can send messages to both the Parse channel and the corresponding FCM topic, supporting users of both old and new versions. When enough users have migrated from the Parse-only version of the app, you can sunset that version and start sending using FCM only.

See the FCM topics docs to learn more.

Firebase Remote Config

Firebase Remote Config is a cloud service that lets you change the behavior and appearance of your app without requiring users to download an app update. When using Remote Config, you create in-app default values that control the behavior and appearance of your app. Then, you can later use the Firebase console to override in-app default values for all app users or for segments of your userbase.

Firebase Remote Config can be very useful during your migrations in cases where you want to test different solutions and be able to dynamically shift more clients to a different provider. For example, if you have a version of your app that uses both Firebase and Parse for the data, you could use a random percentile rule to determine which clients read from Firebase, and gradually increase the percentage.

To learn more about Firebase Remote Config, see the Remote Config introduction.

Differences With Parse Config

With Parse config you can add key/value pairs to your app on the Parse Config Dashboard, and then fetch the ParseConfig on the client. Every ParseConfig instance that you get is always immutable. When you retrieve a new ParseConfig in the future from the network, it will not modify any existing ParseConfig instance, but will instead create a new one and make it available via getCurrentConfig().

With Firebase Remote Config you create in-app defaults for key/value pairs that you can override from the Firebase console, and you can use rules and conditions to provide variations on your app's user experience to different segments of your userbase. Firebase Remote Config implements a singleton class that makes the key/value pairs available to your app. Initially the singleton returns the default values that you define in-app. You can fetch a new set of values from the server at any moment convenient for your app; after the new set is successfully fetched, you can choose when to activate it to make the new values available to the app.

Suggested Migration Strategy

You can move to Firebase Remote Config by copying the key/value pairs of your Parse config to the Firebase console, and then deploying a new version of the app that uses Firebase Remote Config.

If you want to experiment with both Parse Config and Firebase Remote Config, you can deploy a new version of the app that uses both SDKs until enough users have migrated from the Parse only version.

Code Comparison

Parse

ParseConfig.getInBackground(new ConfigCallback() {
    @Override
    public void done(ParseConfig config, ParseException e) {
        if (e == null) {
            Log.d("TAG", "Yay! Config was fetched from the server.");
        } else {
            Log.e("TAG", "Failed to fetch. Using Cached Config.");
            config = ParseConfig.getCurrentConfig();
        }

        // Get the message from config or fallback to default value
        String welcomeMessage = config.getString("welcomeMessage", "Welcome!");
    }
});

Firebase

mFirebaseRemoteConfig = FirebaseRemoteConfig.getInstance();
// Set defaults from an XML resource file stored in res/xml
mFirebaseRemoteConfig.setDefaults(R.xml.remote_config_defaults);

mFirebaseRemoteConfig.fetch()
    .addOnSuccessListener(new OnSuccessListener<Void>() {
        @Override
        public void onSuccess(Void aVoid) {
            Log.d("TAG", "Yay! Config was fetched from the server.");
            // Once the config is successfully fetched it must be activated before newly fetched
            // values are returned.
            mFirebaseRemoteConfig.activateFetched();
        }
    })
    .addOnFailureListener(new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception exception) {
            Log.e("TAG", "Failed to fetch. Using last fetched or default.");
        }
    })

// ...

// When this is called, the value of the latest fetched and activated config is returned;
// if there's none, the default value is returned.
String welcomeMessage = mFirebaseRemoteConfig.getString("welcomeMessage");