Migrar o app para Android do Parse para o Firebase

Se você é um usuário do Parse que procura um back-end alternativo como solução de serviço, o Firebase pode ser a escolha ideal para seu app Android.

Este guia descreve como integrar serviços específicos ao app. Para ver instruções básicas de configuração do Firebase, consulte o guia Configuração do Android.

Google Analytics

O Google Analytics é uma solução sem custo financeiro de análise de apps que fornece insights sobre o uso de apps e o envolvimento do usuário. O Analytics integra-se a recursos do Firebase e oferece a você geração ilimitada de relatórios para até 500 eventos distintos que podem ser definidos usando o SDK do Firebase.

Consulte os documentos do Google Analytics para saber mais.

Sugestão de estratégia de migração

O uso de diferentes provedores de análise é um cenário comum que se aplica facilmente ao Google Analytics. Basta adicioná-lo ao seu app para aproveitar eventos e propriedades de usuário coletados automaticamente pelo Analytics, como primeiro acesso, atualização do app, modelo do dispositivo ou idade.

Para eventos e propriedades de usuário personalizados, é possível empregar uma estratégia dupla usando o Parse Analytics e o Google Analytics para registrar eventos e propriedades. Isso permite que você implante a nova solução gradativamente.

Comparação de código

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

O Firebase Realtime Database é um banco de dados NoSQL hospedado na nuvem. Os dados são armazenados como JSON e sincronizados em tempo real para cada cliente conectado.

Veja os documentos do Firebase Realtime Database para saber mais.

Diferenças ao usar dados do Parse

Objetos

No Parse, é possível armazenar um ParseObject ou uma subclasse dele, que contém pares de chave-valor de dados compatíveis com JSON. Os dados não têm esquemas, e isso significa que não é preciso especificar quais chaves existem em cada ParseObject.

Todos os dados do Firebase Realtime Database são armazenados como objetos JSON e não existe equivalente para ParseObject. Você grava nos valores de árvore JSON dos tipos que correspondem aos tipos JSON disponíveis. Você pode usar objetos Java para simplificar a leitura e a gravação do banco de dados.

Veja um exemplo de como você pode salvar as maiores pontuações de um jogo.

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);
Para mais detalhes, consulte o guia Leitura e gravação de dados no Android.

Relacionamentos entre dados

Um ParseObject pode ter um relacionamento com outro ParseObject: qualquer objeto pode usar outros objetos como valores.

No Firebase Realtime Database, as relações são melhor expressas usando estruturas de dados simples, que dividem os dados em caminhos distintos para que sejam baixados de forma eficiente em chamadas separadas.

Veja um exemplo de como estruturar o relacionamento entre postagens em um app de blog e os respectivos autores.

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

O resultado é o layout de dados abaixo.

{
  // 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"
    }
    ...
  }
}
Para mais detalhes, consulte o guia Estruturar seu banco de dados.

Como ler dados

No Parse, os dados são lidos usando o código de um objeto específico do Parse ou executando consultas por meio do ParseQuery.

No Firebase, os dados são recuperados anexando um listener assíncrono a uma referência do banco de dados. O listener é acionado uma vez para o estado inicial dos dados e novamente quando os dados são alterados, assim não será preciso adicionar código algum para determinar se os dados foram alterados.

Veja um exemplo de como recuperar as pontuações de um determinado jogador com base no exemplo apresentado na seção Objetos.

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());
    }
});
Para mais detalhes sobre tipos disponíveis de listener de eventos e sobre como ordenar e filtrar dados, consulte o guia Leitura e gravação de dados no Android.

Sugestão de estratégia de migração

Reconsidere seus dados

O Firebase Realtime Database é otimizado para sincronizar dados em milissegundos em todos os clientes conectados, e a estrutura de dados resultante é diferente dos dados principais do Parse. Isso significa que a primeira etapa da migração é considerar quais alterações seus dados exigem, incluindo:

  • como os objetos do Parse devem mapear dados do Firebase;
  • se você tem relações pai-filho, como dividir os dados entre diferentes caminhos para que os downloads sejam feitos de forma eficiente em chamadas separadas.

Migre os dados

Após decidir como estruturar os dados no Firebase, você precisa planejar como lidar com o período durante o qual o app grava nos dois bancos de dados. Suas opções são:

Sincronização em segundo plano

Nesse cenário, você tem duas versões do app: a versão antiga que usa o Parse e uma nova versão que usa o Firebase. As sincronizações entre os dois bancos de dados são manipuladas pelo Parse Cloud Code (de Parse para Firebase), com o seu código recebendo alterações no Firebase e sincronizando-as com o Parse. Antes de começar a usar a nova versão, você precisa:

  • converter seus dados do Parse para a nova estrutura do Firebase e gravá-los no Firebase Realtime Database;
  • escrever funções do Parse Cloud Code que usam a REST API do Firebase para gravar alterações no Firebase Realtime Database realizadas nos dados do Parse por clientes antigos;
  • gravar e implementar um código que receba alterações no Firebase e sincronize-as no banco de dados do Parse.

Esse cenário garante uma separação limpa dos códigos novos e antigos, simplificando os clientes. Os desafios desse cenário são lidar com grandes conjuntos de dados na exportação inicial e garantir que a sincronização bidirecional não gere recorrência infinita.

Gravação dupla

Nesse cenário você usa o Parse Cloud Code para escrever uma nova versão do app que usa o Firebase e o Parse e sincronizar alterações feitas por antigos clientes nos dados do Parse para o Firebase Realtime Database. Quando a quantidade de pessoas que migraram da versão somente Parse do app for suficiente, você poderá remover o código Parse da versão de gravação dupla.

Esse cenário não requer código do servidor. As desvantagens são que os dados não acessados não serão migrados e o tamanho do seu app será aumentado pelo uso de ambos os SDKs.

Firebase Authentication

O Firebase Authentication pode autenticar usuários que usam senhas e provedores populares de identidades federadas, como Google, Facebook e Twitter. Ele também fornece bibliotecas de IU para economizar o investimento necessário para implementar e manter toda a experiência de autenticação do seu app em todas as plataformas.

Veja os documentos do Firebase Authentication para saber mais.

Diferenças ao usar o Parse Auth

O Parse fornece uma classe de usuário especializada chamada ParseUser, que lida automaticamente com a funcionalidade necessária para o gerenciamento da conta de usuário. ParseUser é uma subclasse de ParseObject, e isso significa que os dados do usuário estão disponíveis nos dados do Parse e podem ser estendidos com campos adicionais, como qualquer outro ParseObject.

Um FirebaseUser tem um conjunto fixo de propriedades básicas - um ID exclusivo, um endereço de e-mail principal, um nome e um URL de foto - armazenadas em um banco de dados de usuário separado do projeto. Essas propriedades podem ser atualizadas pelo usuário. Não é possível adicionar outras propriedades diretamente ao objeto FirebaseUser, mas você tem a opção de armazenar essas outras propriedades no Firebase Realtime Database.

Veja um exemplo de como inscrever um usuário e adicionar um campo adicional de número de telefone.

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());
            }
        }
    });

Sugestão de estratégia de migração

Migrar contas

Para migrar contas de usuário do Parse para o Firebase, exporte seu banco de dados de usuários para um arquivo JSON ou CSV. Depois, importe esse arquivo para o projeto do Firebase usando o comando auth:import da CLI do Firebase.

Primeiro, exporte seu banco de dados de usuário a partir do console do Parse ou do seu banco de dados auto-hospedado. Por exemplo, um arquivo JSON exportado a partir do console do Parse pode ter a seguinte aparência:

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

Em seguida, transforme o arquivo exportado para o formato exigido pela CLI do Firebase. Use o objectId dos seus usuários do Parse como o localId dos seus usuários do Firebase. Além disso, codifique os valores bcryptPassword do Parse em base64 e use-os no campo passwordHash. Exemplo:

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

Por último, importe o arquivo transformado com a CLI do Firebase, especificando bcrypt como o algoritmo de hash:

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

Migrar dados do usuário

Se você estiver armazenando dados adicionais dos seus usuários, será possível migrá-los para o Firebase Realtime Database usando as estratégias descritas na seção Migração de dados. Se você migrar contas usando o fluxo descrito na seção Migração de contas, as contas do Firebase terão os mesmos códigos que as do Parse. Isso permite que você migre e reproduza com facilidade todas as relações usadas pelo código do usuário.

Firebase Cloud Messaging

O Firebase Cloud Messaging (FCM) é uma solução de mensagens entre plataformas que permite a entrega confiável de mensagens e notificações sem custos. O Editor do Notificações é um serviço sem custos integrado ao Firebase Cloud Messaging que ativa notificações de usuários direcionadas para desenvolvedores de aplicativos para dispositivos móveis.

Veja os documentos do Firebase Cloud Messaging para saber mais.

Diferenças ao usar notificações push do Parse

Todo aplicativo Parse instalado em um dispositivo registrado para notificações tem um objeto Installation associado, onde você armazena todos os dados necessários para segmentar as notificações. Installation é uma subclasse de ParseUser, e isso significa que é possível adicionar outros dados que quiser às instâncias de Installation.

O Editor do Notificações fornece segmentos de usuários predefinidos com base em informações como app, versão do app e idioma do dispositivo. É possível criar segmentos de usuários mais complexos por meio de eventos e propriedades do Google Analytics para criar públicos. Para saber mais, consulte o guia de ajuda sobre públicos. Essas informações de segmentação não estão visíveis no Firebase Realtime Database.

Sugestão de estratégia de migração

Migração de tokens de dispositivos

No momento da gravação, o SDK para Parse do Firebase usa uma versão mais antiga dos tokens de registro do FCM, incompatível com os recursos oferecidos pelo Editor do Notificações.

Você pode receber um novo token adicionando o SDK do FCM ao app. Porém, isso pode invalidar o token usado pelo SDK para Parse para receber notificações. Se você quiser evitar isso, configure o SDK do Parse para usar o código de remetente do Parse e seu código de remetente. Desta forma, o token usado pelo Parse SDK não é invalidado, mas esteja ciente de que essa solução alternativa deixará de funcionar quando o Parse encerrar o projeto.

Migrar canais para tópicos do FCM

Se você estiver usando canais do Parse para enviar notificações, poderá migrar para tópicos do FCM que fornecem o mesmo modelo de editor-assinante. A fim de processar a transição do Parse para o FCM, grave uma nova versão do app que utiliza o SDK para Parse para cancelar a assinatura dos canais Parse e o SDK do FCM para assinar os tópicos correspondentes do FCM. Nessa versão do app, você deve desativar o recebimento de notificações no SDK para Parse, removendo do manifesto do app o seguinte:

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

Por exemplo, se o usuário estiver inscrito no tópico "Giants", você fará algo como:

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

Usando essa estratégia, você pode enviar mensagens para o canal do Parse e para o tópico correspondente do FCM, dando suporte a usuários das versões antigas e novas. Quando a quantidade de usuários que migraram da versão somente Parse do app for suficiente, desative essa versão e comece a enviar usando somente o FCM.

Veja os documentos sobre tópicos do FCM para saber mais.

Configuração remota do Firebase

O Configuração remota do Firebase é um serviço em nuvem que permite a alteração do comportamento e da aparência do app sem exigir que os usuários façam download de uma atualização do aplicativo. Ao usar a Configuração remota, você cria valores padrão no app que controlam o comportamento e a aparência dele. Em seguida, use o Console do Firebase caso queira modificar os valores padrão no app para todos os usuários do app ou para segmentos da sua base de usuários.

O Configuração remota do Firebase pode ser muito útil durante as migrações nos casos em que você queira testar diferentes soluções e transferir dinamicamente mais clientes para um provedor diferente. Por exemplo, se você tem uma versão do app que utiliza o Firebase e o Parse para os dados, pode usar uma regra percentual aleatória para determinar quais clientes leem os dados do Firebase e gradativamente aumentar o percentual.

Para saber mais sobre o Configuração remota do Firebase, veja Introdução ao Configuração remota.

Diferenças ao usar a configuração do Parse

Com a configuração do Parse, é possível adicionar pares de chave-valor ao seu app no painel de configuração do Parse e, depois, buscar ParseConfig no cliente. Cada instância do ParseConfig obtida sempre é imutável. Quando você recupera um novo ParseConfig no futuro a partir da rede, as instâncias existentes do ParseConfig não são modificadas, mas, em vez disso, uma nova instância é criada e disponibilizada por meio do getCurrentConfig().

Com o Configuração remota do Firebase, você cria padrões no aplicativo para pares de chave-valor que podem ser modificados no Console do Firebase e pode usar regras e condições que oferecem variações da experiência do usuário do app para segmentos diferentes da base de usuários. A Configuração remota do Firebase implementa uma classe singleton que disponibiliza os pares de chave-valor no seu app. Inicialmente, o singleton retorna os valores padrão definidos no app. Busque um novo conjunto de valores no servidor quando for conveniente para o app. Depois que o novo conjunto for buscado, será possível escolher quando ativá-lo para disponibilizar os novos valores para o app.

Sugestão de estratégia de migração

Você pode mudar para a Configuração remota do Firebase copiando os pares de chave-valor da configuração do Parse para o Console do Firebase e implementando uma nova versão do app que utiliza a Configuração remota do Firebase.

Se você quer fazer uma experiência com a configuração do Parse e com o Configuração remota do Firebase, pode implementar uma nova versão do app que utiliza ambos os SDKs até a quantidade de usuários que migraram da versão somente Parse ser suficiente.

Comparação de código

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