Как перенести приложение Android из Parse в Firebase

Если вы используете Parse и ищете альтернативное решение Backend as a Service, Firebase может стать идеальным выбором для вашего приложения Android.

В этом руководстве описано, как интегрировать в приложение определенные сервисы. Инструкции по базовой настройке Firebase приведены в руководстве по настройке для Android.

Google Analytics

Google Analytics – это бесплатное решение для сбора данных о работе приложений и взаимодействии с пользователями. Analytics интегрируется с другими функциями Firebase и позволяет создавать неограниченное количество отчетов по 500 различным событиям, которые можно задать с помощью Firebase SDK.

Дополнительную информацию можно найти в документации по Google Analytics.

Рекомендуемая стратегия переноса

Использование разных поставщиков аналитики – распространенный сценарий, который легко реализуется в Google Analytics. Просто добавьте его в приложение, чтобы получать данные о событиях и свойствах пользователей, которые Analytics собираются автоматически, например о первом запуске, обновлении приложения, модели устройства и возрасте.

Для специальных событий и свойств пользователей можно использовать стратегию двойной записи, регистрируя события и свойства как в Parse Analytics, так и в Google Analytics. Это позволит вам постепенно внедрить новое решение.

Сравнение кода

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

Firebase Realtime Database – это облачная база данных NoSQL. Данные хранятся в формате JSON и синхронизируются в режиме реального времени со всеми подключенными клиентами.

Подробную информацию можно найти в документации по Firebase Realtime Database.

Различия с данными Parse

Объекты

В Parse вы храните объект ParseObject или его подкласс, содержащий пары "ключ-значение" с данными, совместимыми с JSON. Данные не имеют схемы, поэтому вам не нужно указывать, какие ключи существуют в каждом объекте ParseObject.

Все данные Firebase Realtime Database хранятся в виде объектов JSON, а для ParseObject нет эквивалента. Вы просто записываете в дерево JSON значения типов, которые соответствуют доступным типам JSON. Для упрощения чтения и записи данных в базу можно использовать объекты Java.

Ниже приведен пример того, как можно сохранить рекорды для игры.

Обработать
@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);
Подробную информацию вы найдете в руководстве Чтение и запись данных на устройствах Android.

Взаимосвязи между данными

Объект ParseObject может быть связан с другим объектом ParseObject: любой объект может использовать другие объекты в качестве значений.

В Firebase Realtime Database связи лучше выражать с помощью плоских структур данных, которые разделяют данные на отдельные пути, чтобы их можно было эффективно скачивать в отдельных вызовах.

Ниже приведен пример того, как можно структурировать связь между записями в приложении для ведения блога и их авторами.

Синтаксический анализ
// 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);

В результате получится следующая структура данных.

{
  // 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"
    }
    ...
  }
}
Подробную информацию вы найдете в руководстве Structure Your Database (Структурирование базы данных).

Данные о чтении

В Parse данные можно читать, используя идентификатор определенного объекта Parse или выполняя запросы с помощью ParseQuery.

Чтобы получить данные в Firebase, нужно подключить асинхронный прослушиватель к ссылке на базу данных. Прослушиватель срабатывает один раз для начального состояния данных и снова, когда данные изменяются, поэтому вам не нужно добавлять какой-либо код, чтобы определить, изменились ли данные.

Ниже показано, как получить результаты определенного игрока на основе примера из раздела Объекты.

Обработать
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());
    }
});
Подробную информацию о доступных типах прослушивателей событий, а также о том, как упорядочивать и фильтровать данные, вы найдете в руководстве Чтение и запись данных на Android.

Рекомендуемая стратегия переноса

Переосмыслите свои данные

Firebase Realtime Database оптимизирован для синхронизации данных за миллисекунды на всех подключенных клиентах, и результирующая структура данных отличается от основных данных Parse. Это означает, что на первом этапе переноса вам нужно определить, какие изменения необходимо внести в данные, в том числе:

  • Как объекты Parse сопоставляются с данными Firebase
  • Если у вас есть родительско-дочерние отношения, как разделить данные по разным путям, чтобы их можно было эффективно скачивать в отдельных вызовах.

Перенос данных

После того как вы решите, как структурировать данные в Firebase, вам нужно спланировать, как обрабатывать период, в течение которого вашему приложению необходимо выполнять запись в обе базы данных. Доступны следующие варианты:

Фоновая синхронизация

В этом сценарии у вас есть две версии приложения: старая, в которой используется Parse, и новая, в которой используется Firebase. Синхронизация между двумя базами данных выполняется с помощью Parse Cloud Code (из Parse в Firebase), а ваш код отслеживает изменения в Firebase и синхронизирует их с Parse. Чтобы начать использовать новую версию, необходимо:

  • Преобразуйте существующие данные Parse в новую структуру Firebase и запишите их в Firebase Realtime Database.
  • Напишите функции Parse Cloud Code, которые используют REST API Firebase для записи в Firebase Realtime Database изменений, внесенных в данные Parse старыми клиентами.
  • Напишите и разверните код, который отслеживает изменения в Firebase и синхронизирует их с базой данных Parse.

Этот сценарий обеспечивает четкое разделение старого и нового кода и упрощает работу клиентов. Сложность этого сценария заключается в обработке больших наборов данных при первоначальном экспорте и обеспечении того, чтобы двусторонняя синхронизация не привела к бесконечной рекурсии.

Двойная запись

В этом случае вы создаете новую версию приложения, в которой используются Firebase и Parse. С помощью Parse Cloud Code вы синхронизируете изменения, внесенные старыми клиентами, из Parse Data в Firebase Realtime Database. Когда достаточное количество пользователей перейдет на версию приложения, в которой используется только Parse, вы сможете удалить код Parse из версии с двойной записью.

В этом случае код на стороне сервера не требуется. Недостатки этого подхода заключаются в том, что данные, к которым не осуществляется доступ, не переносятся, а размер приложения увеличивается из-за использования обоих SDK.

Firebase Authentication

Firebase Authentication может выполнять аутентификацию пользователей с помощью паролей и популярных поставщиков федеративной идентификационной информации, таких как Google, Facebook и Twitter. Кроме того, в Firebase есть библиотеки пользовательского интерфейса, которые позволяют сэкономить значительные средства, необходимые для реализации и поддержки полной аутентификации в приложении на всех платформах.

Подробную информацию можно найти в документации по Firebase Authentication.

Различия с Parse Auth

Parse предоставляет специализированный класс пользователей ParseUser, который автоматически обрабатывает функции, необходимые для управления аккаунтами пользователей. ParseUser – это подкласс ParseObject, то есть пользовательские данные доступны в Parse Data и могут быть расширены дополнительными полями, как и любой другой ParseObject.

У FirebaseUser есть фиксированный набор основных свойств: уникальный идентификатор, основной адрес электронной почты, имя и URL фото профиля. Они хранятся в отдельной базе данных пользователей проекта и могут быть изменены пользователем. Нельзя добавлять другие свойства непосредственно в объект FirebaseUser. Вместо этого можно хранить дополнительные свойства в объекте Firebase Realtime Database.

Ниже приведен пример того, как можно зарегистрировать пользователя и добавить дополнительное поле для номера телефона.

Обработать
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());
            }
        }
    });

Рекомендуемая стратегия переноса

Перенос аккаунтов

Чтобы перенести аккаунты пользователей из Parse в Firebase, экспортируйте базу данных пользователей в файл JSON или CSV, а затем импортируйте его в проект Firebase с помощью команды auth:import в интерфейсе командной строки Firebase.

Сначала экспортируйте базу данных пользователей из консоли Parse или собственной базы данных. Например, JSON-файл, экспортированный из консоли Parse, может выглядеть следующим образом:

{ // 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",
  ...
}

Затем преобразуйте экспортированный файл в формат, требуемый Firebase CLI. Используйте objectId пользователей Parse в качестве localId пользователей Firebase. Также закодируйте в Base64 значения bcryptPassword из Parse и используйте их в поле passwordHash. Пример:

{
  "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
        }
      ]
    }
  ]
}

Импортируйте преобразованный файл с помощью интерфейса командной строки Firebase, указав bcrypt в качестве алгоритма хеширования:

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

Перенос пользовательских данных

Если вы храните дополнительные данные пользователей, вы можете перенести их в Firebase Realtime Database, используя стратегии, описанные в разделе Перенос данных. Если вы перенесете аккаунты, как описано в разделе Перенос аккаунтов, у аккаунтов Firebase будут те же идентификаторы, что и у аккаунтов Parse. Это позволит вам легко перенести и воспроизвести любые связи, основанные на идентификаторе пользователя.

Firebase Cloud Messaging

Firebase Cloud Messaging (FCM) – это кроссплатформенное решение для обмена сообщениями, которое позволяет бесплатно и надежно доставлять сообщения и уведомления. Компоновщик уведомлений – это бесплатный сервис на основе Firebase Cloud Messaging, который позволяет разработчикам мобильных приложений отправлять пользователям таргетированные уведомления.

Дополнительную информацию можно найти в документации по Firebase Cloud Messaging .

Различия с push-уведомлениями Parse

С каждым приложением Parse, установленным на устройстве, зарегистрированном для получения уведомлений, связан объект Installation, в котором хранятся все данные, необходимые для таргетинга уведомлений. Installation – это подкласс ParseUser, поэтому вы можете добавить в экземпляры Installation любые дополнительные данные.

В Компоновщике уведомлений есть стандартные сегменты пользователей, созданные на основе информации о приложении, его версии и языке устройства. Вы можете создавать более сложные сегменты пользователей, используя события и свойства Google Analytics для создания аудиторий. Подробную информацию вы найдете в руководстве по аудиториям. Эти данные о таргетинге не видны в Firebase Realtime Database.

Рекомендуемая стратегия переноса

Перенос токенов устройств

На момент написания этой статьи в Parse Android SDK используется более старая версия токенов регистрации FCM, несовместимая с функциями, предлагаемыми в Компоновщике уведомлений.

Вы можете получить новый токен, добавив в приложение FCM SDK, но это может привести к тому, что токен, используемый Parse SDK для получения уведомлений, станет недействительным. Чтобы избежать этого, вы можете настроить Parse SDK на использование идентификатора отправителя Parse и вашего идентификатора отправителя. Таким образом вы не аннулируете токен, используемый Parse SDK. Однако помните, что это решение перестанет работать, когда Parse закроет свой проект.

Перенос каналов в темы FCM

Если вы используете каналы Parse для отправки уведомлений, вы можете перейти на темы FCM, которые поддерживают ту же модель издателя и подписчика. Чтобы перейти с Parse на FCM, можно написать новую версию приложения, в которой SDK Parse будет использоваться для отмены подписки на каналы Parse, а SDK FCM – для подписки на соответствующие темы FCM. В этой версии приложения необходимо отключить получение уведомлений в Parse SDK, удалив из манифеста приложения следующие строки:

<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" />;

Например, если пользователь подписан на тему Giants, вы можете сделать следующее:

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

Благодаря этому вы сможете отправлять сообщения как в канал Parse, так и в соответствующую тему FCM, поддерживая пользователей обеих версий. Когда достаточное количество пользователей перейдет на версию приложения, в которой используется только Parse, вы можете прекратить поддержку старой версии и начать отправлять уведомления только с помощью FCM.

Дополнительную информацию можно найти в документации по темам FCM.

Firebase Remote Config

Firebase Remote Config – это облачный сервис, который позволяет менять поведение и внешний вид приложения без выпуска обновлений приложения, которые пользователям приходится скачивать. При использовании Remote Config достаточно задать значения параметров по умолчанию, определяющие поведение и внешний вид приложения, а потом с помощью консоли Firebase переопределять эти значения для всех пользователей приложения или для отдельных сегментов пользовательской базы.

Firebase Remote Config может быть очень полезен при переносе, если вы хотите протестировать разные решения и динамически переключать клиентов на другого поставщика. Например, если у вас есть версия приложения, которая использует для данных и Firebase, и Parse, вы можете использовать правило случайного процентиля, чтобы определить, какие клиенты читают из Firebase, и постепенно увеличивать процент.

Подробнее Remote Configо Firebase Remote Config…

Различия с Parse Config

С помощью Parse Config вы можете добавлять пары "ключ-значение" в приложение на панели управления Parse Config, а затем получать ParseConfig на клиенте. Каждый экземпляр ParseConfig, который вы получаете, всегда является неизменяемым. Когда вы в следующий раз получите новый объект ParseConfig из сети, он не изменит существующий экземпляр ParseConfig, а создаст новый и сделает его доступным через getCurrentConfig().

С помощью Firebase Remote Config вы можете создавать в приложении пары "ключ-значение" по умолчанию, которые можно переопределять в консоли Firebase, а также использовать правила и условия, чтобы предлагать разные варианты приложения разным сегментам пользовательской базы. Firebase Remote Config реализует класс-одиночку, который делает пары "ключ-значение" доступными для вашего приложения. Изначально одиночка возвращает значения по умолчанию, которые вы определяете в приложении. Вы можете получить новый набор значений с сервера в любой удобный для вашего приложения момент. После успешного получения нового набора вы можете выбрать, когда его активировать, чтобы сделать новые значения доступными для приложения.

Рекомендуемая стратегия переноса

Чтобы перейти на Firebase Remote Config, скопируйте пары "ключ-значение" из конфигурации Parse в консоль Firebase, а затем разверните новую версию приложения, использующую Firebase Remote Config.

Если вы хотите поэкспериментировать с Parse Config и Firebase Remote Config, вы можете развернуть новую версию приложения, в которой используются оба SDK, пока достаточное количество пользователей не перейдет с версии, в которой используется только 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");