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);
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" } ... } }
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());
}
});
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@example.com");
// 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@example.com", "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": "user@example.com", "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": "user@example.com", "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");