Перенесите 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 для регистрации событий и свойств, что позволяет постепенно развертывать новое решение.

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

Анализ аналитики

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

Чтение данных

В 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());
        }
    }
});
Огневая база
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 to Firebase), при этом ваш код прослушивает изменения в Firebase и синхронизирует эти изменения с Parse. Прежде чем вы сможете начать использовать новую версию, вам необходимо:

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

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

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

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

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

Firebase Authentication

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

Дополнительную информацию см. в документации Firebase Authentication .

Различия с аутентификацией синтаксического анализа

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
        }
    }
});
Огневая база
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 CLI.

Сначала экспортируйте свою пользовательскую базу данных из консоли 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. Используйте 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 CLI, указав 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, установленное на устройстве, зарегистрированном для уведомлений, имеет связанный объект 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, вы можете написать новую версию приложения, которая использует Parse SDK для отказа от подписки на каналы Parse и FCM SDK для подписки на соответствующие темы 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" />;

Например, если ваш пользователь подписан на тему «Гиганты», вы должны сделать что-то вроде:

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, и постепенно увеличивать процент.

Чтобы узнать больше о Firebase Remote Config , см. введение Remote Config .

Различия с конфигурацией синтаксического анализа

С помощью конфигурации Parse вы можете добавить пары ключ/значение в свое приложение на панели управления 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!");
    }
});

Огневая база

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