Kotlin Extension Functions e Coroutines no Firebase

“Quanto mais simples e conciso for o código, mais rápido você entenderá o que está acontecendo.” - Paulo Enoque

É por causa da simplicidade e concisão do Kotlin que muitos desenvolvedores têm optado por esta linguagem de programação para realizarem o seu trabalho. Devo recordar também que, em Maio de 2017, a Google anunciou que Kotlin passava a ser uma linguagem oficial para desenvolvimento de aplicações Android. De lá para cá, a adoção desta linguagem e, consequentemente, o número de desenvolvedores Kotlin também aumentaram. Se você ainda não é um destes desenvolvedores, mas gostaria de conhecer o Kotlin, recomendo que leia esta série de artigos do Paulo Enoque:

  1. Introdução a Kotlin
  2. Sintaxe básica do Kotlin
  3. Detalhes avançados sobre Kotlin
  4. Kotlin Android Extensions

Versão em Inglês disponível em dev.to/rosariopfernandes.

Só um ano e meio depois do Kotlin ser adoptado como linguagem oficial para o Android é que o Firebase incluiu esta linguagem na sua documentação oficial :

Mas como diz o ditado popular: “Mais vale tarde do que nunca”. Esta alteração trouxe várias melhorias para a plataforma: o código Java do Firebase Android SDK foi alterado para melhor acomodar a interoperabilidade que o Kotlin oferece, “Extension Functions” foram criadas para tornar o uso do SDK mais conciso no Android, desenvolvedores criaram bibliotecas para melhorar a forma como o Firebase é utilizado na linguagem Kotlin, e muito mais.

Eu juntei-me à criação de bibliotecas para ajudar a utilizar o Firebase em Kotlin, desenvolvi a biblioteca fireXtensions que oferece um conjunto de Extension Functions para o Firebase.

Kotlin Extension Functions

De forma resumida, Extension Functions é uma funcionalidade do Kotlin que permite que você adicione novos métodos à classes que não são suas, sem ter de herdar (utilizar o famoso extends do java) dessa classe. Você pode saber mais sobre Extension Functions no meu artigo Apresentando o fireXtensions.

Depois de adicionar suporte para o Kotlin na Documentação Oficial, a equipa do Firebase decidiu adicionar também algumas Extension Functions no SDK Android do Firebase (pela mesma razão que eu criei o fireXtensions).

As primeiras extensions foram as bibliotecas Common KTX e Firestore KTX.

Common KTX

O módulo firebase-common-ktx contém extension functions usadas para obter a instância do Firebase. Se você já trabalhou com o Firebase tanto em java, como em Kotlin, provavelmente está familiarizado com o método: FirebaseApp.getInstance(). Pois é, se você utilizar o módulo firebase-common-ktx, a chamada do método torna-se mais simples: Firebase.app.

Para utilizar este módulo, certifique-se que o seu build.gradle(project) tem a dependencia do Kotlin Plugin 1.3.20 ou superior:

dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.20"
}

E de seguida adicione a dependência no ficheiro build.gradle (app) do seu projeto:

dependencies {
    // ... Outras dependencias ...
    implementation 'com.google.firebase:firebase-common-ktx:16.1.0'
}

Veja a lista de extensions disponíveis.

Firestore KTX

Como o nome indica, este módulo contém extensions que facilitam o uso do Android SDK do Firestore. Este módulo pode ser adicionado ao seu projecto da mesma forma que o Common KTX, basta utilizar a dependência:

dependencies {
    // ... Outras dependencias ...
    implementation 'com.google.firebase:firebase-firestore-ktx:18.2.0'
}

Veja a lista de extensions disponíveis.

Apesar de estes serem os únicos módulos disponíveis atualmente, existem mais módulos que serão certamente adicionados ao SDK. Irei atualizar este artigo sempre que forem adicionados novos módulos.

Novo módulo (29/08/2019): Functions KTX

Novos módulos (26/09/2019): Remote Config KTX e Storage KTX

Novo módulo (18/10/2019): Realtime Database KTX

Novos módulos (12/11/2019): In-App Messaging KTX e In-App Messaging Display KTX

Kotlin Coroutines

Como você já deve saber, o Firebase funciona de forma assíncrona. Isso leva-nos a utilizar listeners no nosso código para ler os dados da base de dados. E um erro bastante comum entre programadores iniciantes é tentar ler estes dados sem listeners, ou usá-los fora dos listeners, como mostra o exemplo 1:

Exemplo 1: Carregar a lista de todos utilizadores guardados no Firestore.

// O que gostariamos de fazer
// Note que este código não funciona.
val db = FirebaseFirestore.getInstance()
var utilizadores = db.collection("utilizadores").get()
actualizarUI(utilizadores)

// (Note que este código também não funciona)
// O que algumas pessoas fazem erradamente:
var utilizadores: List<Utilizador>()? = null
utilizadoresRef.get().addOnSuccessListener { querySnapshot ->
    utilizadores = querySnapshot.toObjects(Utilizador::class.java)
}
if (utilizadores == null) {
    mostrarErro()
    // O mostrarErro() sempre será chamado porque
    // a lista de utilizadores é carregada de forma
    // assíncrona no listener.
    // O if é chamado enquanto a lista ainda não foi carregada.
} else {
    actualizarUI(utilizadores)
}



// O que elas deveriam fazer:
utilizadoresRef.get()
    .addOnSuccessListener { querySnapshot ->
        val utilizadores = querySnapshot.toObjects(Utilizador::class.java)
        actualizarUI(utilizadores)
        // O método está sendo chamado depois da
        // lista de utilizadores ter sido carregada.
    }.addOnFailureListener { e ->
        mostrarErro()
    }

Olhando para o código correto acima, você pode achar que não tem nenhum problema em utilizar listeners, até porque fica tudo bem separado. Mas imagine que precisamos ler dados de collections diferentes e depois juntá-los para mostrá-los na UI da aplicação. Isso implicaria colocar um listener dentro de outro listener, que não fica muito fácil de ler, como podemos ver no exemplo a seguir:

Exemplo 2: Carregar o perfil do utilizador João e a lista de amigos dele que está guardada em uma collection diferente.

// Aproveito para mandar um abraço a todos desenvolvedores
// JavaScript que já estiveram no famoso CallBack hell
utilizadoresRef.document("joao").get().addOnSuccessListener { querySnapshot ->
            val utilizadorJoao = querySnapshot.toObject(Utilizador::class.java)
                                                             
            amigosRef.get().addOnSuccessListener { amigoSnapshot ->
                val amigos = amigoSnapshot.toObjects(Amigo::class.java)
                mostrarJoaoEAmigos(utilizadorJoao, amigos)
            }.addOnFailureListener {
                mostrarErro()
            }
                                                             
        }.addOnFailureListener { e ->
            mostrarErro()
        }

Coroutines vieram para mudar isso. Elas permitem que você escreva código assíncrono como se fosse síncrono, tornando-o mais conciso e legível. Para utilizar Coroutines no seu projecto Android, certifique-se que está a utilizar o plugin Kotlin com versão superior a 1.3.x e adicione as seguintes dependências no ficheiro build.gradle(app):

dependencies {
    // ... Outras dependencias ...
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.1.1'
}

Utilizando Coroutines, o nosso exemplo 1 torna-se algo como:

try {
    val snapshot = utilizadoresRef.get().await()
    val utilizadores = snapshot.toObjects(Utilizador::class.java)
    actualizarUI(utilizadores)
} catch (e: FirebaseFirestoreException) {
    mostrarErro()
}

Repare que este código é muito parecido com o da secção “O que gostariamos de fazer” do exemplo 1.

O exemplo 2 também fica mais simples:

try {
    val querySnapshot = utilizadoresRef.document("joao").get().await()
    val utilizadorJoao = querySnapshot.toObject(Utilizador::class.java)
    
    val amigoSnapshot = amigosRef.get().await()
    val amigos = amigoSnapshot.toObjects(Amigo::class.java)
    mostrarJoaoEAmigos(utilizadorJoao, amigos)
} catch (e: FirebaseFirestoreException) {
    mostrarErro()
}

O código agora parece síncrono e fica mais fácil de ler, não fica?


Por hoje é tudo. Espero que você tenha gostado de conhecer estas funcionalidades da linguagem Kotlin e espero também que elas lhe ajudem durante o desenvolvimento de aplicações.

Se você estiver tentando usar as Kotlin Extensions ou Coroutines e teve um problema, coloque ele no StackOverflow, explicando o que você fez e qual foi o erro que teve. De certeza que você obterá ajuda de mim ou de alguém da comunidade.