Convertendo para Kotlin

Nesse codelab você aprenderá como converter o seu código de Java para Kotlin. Você também aprenderá o que são as convenções da linguagem Kotlin e como garantir que o código que você está escrevendo está às seguindo.

Esse codelab é feito para qualquer pessoa desenvolvedora que usa Java e que está considerando migrar um projeto para Kotlin. Nós começaremos com algumas classes Java que você converterá para Kotlin usando a IDE. Então nós daremos uma olhada no código convertido e veremos como melhorá-lo deixando-o mais idiomático e evitando armadilhas comuns.

O que aprenderá

Você aprenderá como converter Java para Kotlin. Fazendo isso você aprenderá os seguintes recursos e conceitos da linguagem Kotlin:

  • Lidando com nulidade
  • Implementando singletons
  • Classes de dados (data classes)
  • Lidando com strings
  • Operador Elvis
  • Desestruturação
  • Propriedades e propriedades de apoio
  • Argumentos padrão e parâmetros nomeados
  • Trabalhando com coleções
  • Funções de extensão
  • Funções e parâmetros de alto nível
  • Palavras chave let, apply, with e run

Suposições

Você já deve ter familiaridade com Java.

Do que você precisará

Crie um novo projeto

Se você está utilizando o IntelliJ IDEA, crie um novo projeto Java para Kotlin/JVM.

Se você está utilizando o Android Studio, crie um novo projeto sem Activity. Escolha qualquer valor para o Minimum SDK, isso não afetará o resultado final.

O código

Nós criaremos um objeto de modelo chamado User e uma classe singleton Repository que trabalhe com objetos User e expõe listas de usuários e nomes de usuários formatados.

Crie um novo arquivo chamado User.java dentro de app/java/<nomedoseupacote> e cole o seguinte código:

public class User {

    @Nullable
    private String firstName;
    @Nullable
    private String lastName;

    public User(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

}

Você notará que sua IDE está te dizendo que @Nullable não está definido. Então importe androidx.annotation.Nullable se você estiver utilizando o Android Studio, ou org.jetbrains.annotations.Nullable se você estiver usando IntelliJ.

Crie um novo arquivo chamado Repository.java e cole o código a seguir:

import java.util.ArrayList;
import java.util.List;

public class Repository {

    private static Repository INSTANCE = null;

    private List<User> users = null;

    public static Repository getInstance() {
        if (INSTANCE == null) {
            synchronized (Repository.class) {
                if (INSTANCE == null) {
                    INSTANCE = new Repository();
                }
            }
        }
        return INSTANCE;
    }

    // keeping the constructor private to enforce the usage of getInstance
    private Repository() {

        User user1 = new User("Jane", "");
        User user2 = new User("John", null);
        User user3 = new User("Anne", "Doe");

        users = new ArrayList();
        users.add(user1);
        users.add(user2);
        users.add(user3);
    }

    public List<User> getUsers() {
        return users;
    }

    public List<String> getFormattedUserNames() {
        List<String> userNames = new ArrayList<>(users.size());
        for (User user : users) {
            String name;

            if (user.getLastName() != null) {
                if (user.getFirstName() != null) {
                    name = user.getFirstName() + " " + user.getLastName();
                } else {
                    name = user.getLastName();
                }
            } else if (user.getFirstName() != null) {
                name = user.getFirstName();
            } else {
                name = "Unknown";
            }
            userNames.add(name);
        }
        return userNames;
    }
}



        

Nossa IDE consegue fazer um ótimo trabalho refatorando automaticamente código Java para código Kotlin, mas às vezes ela precisa de uma ajudinha. Vamos deixar nossa IDE realizar o passo inicial da conversão. Então nós passaremos pelo código convertido para entender como e porque ele foi convertido desta forma.

Vá para o arquivo User.java e o converta para Kotlin: Menu -> Code -> Convert Java File to Kotlin File.

Se a sua IDE pedir para corrigir após a conversão, pressione Yes.

Você deve ver o código Kotlin a seguir:

class User(var firstName: String?, var lastName: String?)

Note que o User.java foi renomeado para User.kt. Arquivos Kotlin tem a extensão .kt.

Na nossa classe Java User nós temos duas propriedades firstName e lastName. Cada uma tinha um método getter e setter, tornando o seu valor mutável. A palavra chave em Kotlin para variáveis mutáveis é var, então o conversor usa var para cada uma dessas propriedades. Se as propriedades Java só tivessem getters, elas seriam imutáveis e teriam sido declaradas como val. val é similar a palavra chave final em Java.

Uma das principais diferenças entre Kotlin e Java é que Kotlin especifica explicitamente se uma variável pode aceitar um valor nulo. Isso é feito acrescentando um `?` à declaração do tipo.

Já que nós marcamos firstName e lastName como permitindo valores nulo (nullable), o conversor automático marcou as propriedades como nullable com String?. Se você anotar seus membros Java como não-nulos (usando org.jetbrains.annotations.NotNull ou androidx.annotation.NonNull), o conversor reconhecerá automaticamente isso e tornará os campos como não-nulos no Kotlin também.

A conversão básica já está pronta. Mas nós podemos escrever isso de uma maneira mais idiomática. Vamos ver como.

Classe de dados (Data Classes)

Nossa classe User contém apenas dados. Kotlin tem uma palavra chave para classes com esse papel: data. Ao marcá-la como uma classe data, o compilador criará automaticamente getters e setters para nós. Ela também derivará as funções equals(), hashCode() e toString().

Vamos adicionar a palavra chave data na nossa classe User:

data class User(var firstName: String, var lastName: String)

Kotlin, assim como Java, pode ter um construtor primário e um ou mais construtores secundários. O construtor no exemplo acima é o primário da classe User. Se você está convertendo uma classe Java que possui múltiplos construtores, o conversor irá criá-los automaticamente em Kotlin também. Eles são definidos usando a palavra chave constructor.

Se quisermos criar uma instância dessa classe, podemos fazer assim:

val user1 = User("Jane", "Doe")

Igualdade

Kotlin possui dois tipos de igualdade:

  • A igualdade estrutural usa o operador == e chama equals() para determinar se duas instâncias são iguais.
  • A igualdade referencial usa o operador === e verifica se duas referências apontam para o mesmo objeto.

As propriedades definidas no construtor principal da classe de dados serão usadas para verificações de igualdade estrutural.

val user1 = User("Jane", "Doe")
val user2 = User("Jane", "Doe")
val structurallyEqual = user1 == user2 // true
val referentiallyEqual = user1 === user2 // false

Em Kotlin, nós podemos atribuir valores padrão para argumentos em chamadas de função. O valor padrão é usado quando o argumento é omitido. Em Kotlin, construtores também são funções, então podemos usar argumentos padrão para especificar que o valor padrão de lastName é null. Para fazer isso, nós apenas atribuímos null a lastName.

data class User(var firstName: String?, var lastName: String? = null)

// Uso
val jane = User("Jane") // same as User("Jane", null)
val joe = User("Joe", "Doe")

Kotlin permite que você nomeie seus argumentos quando suas funções são chamadas:

val john = User(firstName = "John", lastName = "Doe") 

Digamos que o firstName tenha null como valor padrão e lastName não. Nesse caso, como o parâmetro padrão precederia um parâmetro sem valor padrão, teria que chamar a função com argumentos nomeados:

data class User(var firstName: String? = null, var lastName: String?)

// Uso
val jane = User(lastName = "Doe") // é o mesmo que User(null, "Doe")
val john = User("John", "Doe")

Valores padrão são importantes e é um conceito utilizado com muita frequência no código Kotlin. No nosso Codelab nós queremos sempre especificar o primeiro e o último nome na declaração de um objeto User, então nós não precisamos de valores padrão.

Antes de continuar o codelab, verifique se sua classe User é uma classe de dados (data). Agora vamos converter a classe Repository para Kotlin. O resultado da conversão automática deve ficar da seguinte forma:

import java.util.*

class Repository private constructor() {
    private var users: MutableList<User?>? = null
    fun getUsers(): List<User?>? {
        return users
    }

    val formattedUserNames: List<String?>
        get() {
            val userNames: MutableList<String?> =
                ArrayList(users!!.size)
            for (user in users) {
                var name: String
                name = if (user!!.lastName != null) {
                    if (user!!.firstName != null) {
                        user!!.firstName + " " + user!!.lastName
                    } else {
                        user!!.lastName
                    }
                } else if (user!!.firstName != null) {
                    user!!.firstName
                } else {
                    "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

    companion object {
        private var INSTANCE: Repository? = null
        val instance: Repository?
            get() {
                if (INSTANCE == null) {
                    synchronized(Repository::class.java) {
                        if (INSTANCE == null) {
                            INSTANCE =
                                Repository()
                        }
                    }
                }
                return INSTANCE
            }
    }

    // mantendo o construtor privado para forçar o uso do  getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Vamos ver o que o conversor automático fez:

  • A lista de usuários (users) pode ser nula pois o objeto não foi instanciado no momento da declaração
  • Funções em Kotlin como getUsers() são declaradas com o modificador fun.
  • O método getFormattedUserNames() é agora uma propriedade chamada formattedUserNames
  • A iteração sob a lista de usuários (que inicialmente era parte de getFormattedUserNames() ) possui uma sintaxe diferente da de Java
  • O campo static agora faz parte de um bloco companion object
  • Um bloco init foi adicionado

Antes de avançarmos, vamos limpar o código um pouco. Se olharmos para o construtor, perceberemos que o conversor tornou nossa lista de usuários (users) uma lista mutável que permite armazenar objetos nulos. Enquanto que a lista pode de fato ser nula, vamos assumir que ela não pode armazenar usuários nulos. Então, vamos fazer o seguinte:

  • Remova o ? em User? dentro da declaração do tipo de users
  • Remova o ? em User? no tipo de retorno de getUsers() então ele retorna List<User>?

Bloco init

No Kotlin, o construtor principal não pode counter nenhum código, então o código de inicialização é colocado dentro de blocos init. A funcionalidade é a mesma.

class Repository private constructor() {
    ...
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Grande parte do código init manipula as propriedades de inicialização. Isso também pode ser feito na declaração da propriedade. Por exemplo, na versão Kotlin de nossa classe Repository, vemos que a propriedade users foi inicializada na declaração.

private var users: MutableList<User>? = null

Propriedades e métodos estáticos (static) em Kotlin

Em Java, usamos a palavra-chave static para campos ou funções para dizer que eles pertencem a uma classe, mas não a uma instância da classe. É por isso que criamos o campo estático INSTANCE na nossa classe Repository. O equivalente Kotlin para isso é o bloco companion object. Aqui você também declararia os campos e funções estáticas. O conversor criou e moveu o campo INSTANCE para aqui.

Lidando com singletons

Como precisamos apenas de uma instância da classe Repository, usamos o padrão singleton em Java. Com o Kotlin, você pode forçar esse padrão no nível do compilador substituindo a palavra-chave class por object.

Remova o construtor privado e substitua a definição da classe com object Repository. Remova também o objeto complementar (companion object).

object Repository {

    private var users: MutableList<User>? = null
    fun getUsers(): List<User>? {
       return users
    }

    val formattedUserNames: List<String>
        get() {
            val userNames: MutableList<String> =
                ArrayList(users!!.size)
        for (user in users) {
            var name: String
            name = if (user!!.lastName != null) {
                if (user!!.firstName != null) {
                    user!!.firstName + " " + user!!.lastName
                } else {
                    user!!.lastName
                }
            } else if (user!!.firstName != null) {
                user!!.firstName
            } else {
                "Unknown"
            }
            userNames.add(name)
       }
       return userNames
   }

    // mantendo o construtor privado para forçar o uso do  getInstance
    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")
        users = ArrayList<Any?>()
        users.add(user1)
        users.add(user2)
        users.add(user3)
    }
}

Ao usar a classe object, apenas chamamos funções e propriedades diretamente no objeto, assim:

val formattedUserNames = Repository.formattedUserNames

Note que se a propriedade não tem um modificador de visibilidade, ela é pública por padrão, este é o caso da propriedade formattedUserNames do objeto Repository.

Ao converter a classe Repository para Kotlin, o conversor automático tornou a lista de usuários nullable (ou seja, que pode ser nula), porque não foi inicializado em um objeto quando foi declarado. Como resultado, todos os usos do objeto users, o operador de asserção não-nulo !! precisou ser usado (você verá users!! e user!! por todo o código convertido). O operador !! converte qualquer variável para um tipo não nulo, então você pode acessar propriedades ou chamar funções com esse objeto. Entretanto, uma exceção é lançada se a variável for de fato nula. Ao usar !!, você está arriscando ter exceções sendo lançadas em tempo de execução.

Em vez disso, prefira lidar com nulidade usando um destes métodos:

  • Fazendo uma verificação de nulidade (if (users != null) {...} )
  • Usando o operador elvis ?: (que será abordado posteriormente no codelab)
  • Usando algumas das funções padrão do Kotlin (que serão abordadas posteriormente no codelab)

No nosso caso, sabemos que a lista de usuários não precisa ser nula (nullable), já que é inicializada logo após o objeto ser construído (no bloco init). Assim, podemos instanciar diretamente o objeto users quando o declaramos.

Ao criar instâncias de tipos de coleção, o Kotlin fornece várias funções auxiliares para tornar seu código mais legível e flexível. Aqui estamos usando um MutableList para users:

private var users: MutableList<User>? = null

Para simplificar, podemos usar a função mutableListOf(), e fornecer o tipo de elemento da lista. mutableListOf<User>() cria uma lista vazia que pode armazenar objetos do tipo User. Uma vez que o tipo da variável pode ser inferido pelo compilador, remova a declaração explícita de tipo da propriedade users.

private val users = mutableListOf<User>()

Nós também podemos mudar de var para val porque users conterá uma referência imutável para a lista de usuários. Perceba que a referência é imutável, mas a lista em si é mutável (você consegue adicionar ou remover elementos).

Uma vez que nossa variável users já está inicializada, remova essa inicialização do bloco init.

users = ArrayList<Any?>()

Então o bloco init deve ficar como a seguir:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")

    users.add(user1)
    users.add(user2)
    users.add(user3)
}

Com essas mudanças, nossa propriedade users agora é não-nula e nós podemos remover todos as ocorrências desnecessárias do operador !!. Perceba que você ainda verá erros de compilação no Android Studio, mas continue com os próximos passos do codelab para resolvê-los.

val userNames: MutableList<String?> = ArrayList(users.size)
for (user in users) {
    var name: String
    name = if (user.lastName != null) {
        if (user.firstName != null) {
            user.firstName + " " + user.lastName
        } else {
            user.lastName
        }
    } else if (user.firstName != null) {
        user.firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

Para o valor de userNames, se você especificar o tipo do ArrayList como armazenando Strings, então você pode remover a declaração explícita de tipo porque ela será inferida.

val userNames = ArrayList<String>(users.size)

Desestruturação

Kotlin permite desestruturar um objeto em diversas variáveis, usando uma sintaxe chamada declaração desestruturada. Criamos múltiplas variáveis e podemos usá-las independentemente.

Por exemplo, as data classes suportam desestruturação então podemos desestruturar o objeto User do laço for em (firstName, lastName). Isso nos permite trabalhar diretamente com os valores de firstName and lastName. Atualize o laço for como mostrado abaixo. Substitua todas as instâncias de user.firstName com firstName e substitua user.lastName por lastName.

 
for ((firstName, lastName) in users) {
    var name: String
    name = if (lastName != null) {
        if (firstName != null) {
            firstName + " " + lastName
        } else {
            lastName
        }
    } else if (firstName != null) {
        firstName
    } else {
        "Unknown"
    }
    userNames.add(name)
}

Expressões if

Os nomes na lista de userNames não estão no formato que nós queremos ainda. Uma vez que lastName quanto firstName podem ser nulos, precisamos lidar com a nulidade quando construímos a lista de nomes de usuários formatados. Nós queremos exibir "Unknown" se algum dos nomes estiver ausente. Uma vez que a variável name não será modificada após ser ser atribuída, podemos usar val ao invés de var. Faça essa mudança primeiro.

val name: String

Dê uma olhada no código que atribui o nome à variável. Pode parecer novo para você ver uma variável sendo atribuída com um bloco if / else. Isso é permitido porque em Kotlin if, when, for, e while são expressões—eles retornam um valor. A última linha da instrução if será atribuída à name. O único propósito deste bloco é inicializar o valor de name.

Essencialmente, essa lógica apresentada aqui é se lastName é nulo, name é atribuído com firstName ou "Unknown".

Se o lastName é nulo, name é o firstName ou "Unknown":

name = if (lastName != null) {
    if (firstName != null) {
        firstName + " " + lastName
    } else {
        lastName
    }
} else if (firstName != null) {
    firstName
} else {
    "Unknown"
}

Elvis operator

Esse código pode ser escrito de forma mais idiomática usando o operador elvis ?:. O operador elvis retornará a expressão do lado esquerdo se não for nula, ou a expressão do lado direito se o lado esquerdo for nulo.

Portanto, no código a seguir, firstName é retornado se não for nulo. Se firstName for nulo, a expressão retorna o valor à direita, "Unknown":

name = if (lastName != null) {
    ...
} else {
    firstName ?: "Unknown"
}

Kotlin facilita o trabalho com strings utilizando templates de string. Templates de string permitem que você faça referência à variáveis dentro de declarações de string usando o símbolo $ antes da variável. Você também pode colocar uma expressão dentro da declaração de uma string, basta colocar a espressão entre { } e o usando o símbolo $ antes. Por exemplo: ${user.firstName}.

Seu código no momento usa concatenação de string para combinar o firstName e o lastName para formar o nome do usuário.

if (firstName != null) {
    firstName + " " + lastName
}

Ao invés disso, substitua a concatenação de String por:

if (firstName != null) {
    "$firstName $lastName"
}

Usar templates de string pode simplificar seu código.

Sua IDE apresentará avisos para você se houver uma forma mais idiomática para escrever seu código. Você notará um sublinhado chatinho no seu código, e quando pausar o cursor sobre ela, você verá uma sugestão de como refatorar seu código.

Neste momento, você deve estar vendo um aviso de que a declaração de name pode ficar junto da atribuição. Vamos fazer isso. Como o tipo da variável name pode ser deduzido, nós podemos remover a declaração explícita do tipo String. Agora, nosso formattedUserNames deve parecer com isso:

val formattedUserNames: List<String?>
    get() {
        val userNames = ArrayList<String>(users.size)
        for ((firstName, lastName) in users) {
            val name = if (lastName != null) {
                if (firstName != null) {
                    "$firstName $lastName"
                } else {
                    lastName
                }
            } else {
                firstName ?: "Unknown"
            }
            userNames.add(name)
        }
        return userNames
    }

Nós podemos fazer um ajuste adicionar. Nossa lógica de UI exibe "Unknown" caso o primeiro e o útlimo nome estejam faltando, então nós não estamos suportando objetos nulos. Sendo assim, substitua o tipo de dados de formattedUserNames de List<String?> para List<String>.

val formattedUserNames: List<String>

Vamos dar uma olhada mais de perto no getter formattedUserNames e ver como podemos torná-lo mais idiomático. Neste momento o código faz o seguinte:

  • Cria uma nova lista de strings;
  • Itera a lista de usuários;
  • Constrói o nome formatado para cada usuário, com base no nome e sobrenome do usuário;
  • Retorna a lista recém criada
    val formattedUserNames: List<String>
        get() {
            val userNames = ArrayList<String>(users.size)
            for ((firstName, lastName) in users) {
                val name = if (lastName != null) {
                    if (firstName != null) {
                        "$firstName $lastName"
                    } else {
                        lastName
                    }
                } else {
                    firstName ?: "Unknown"
                }
                userNames.add(name)
            }
            return userNames
        }

O Kotlin fornece uma extensa lista de transformações de coleção que tornam o desenvolvimento mais rápido e seguro, expandindo os recursos da API de coleções do Java. Uma delas é a função de map. Essa função retorna uma nova lista contendo os resultados da aplicação da função de transformação fornecida para cada elemento na lista original. Portanto, em vez de criar uma nova lista e iterar a lista de usuários manualmente, podemos usar a função map e mover a lógica que temos no laço for para dentro do corpo do map. Por padrão, o nome do item de lista atual usado no map é it, mas para facilitar a leitura, você pode substituir o it pelo nome de sua preferência. No nosso caso, vamos chamá-la de user:

    
val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                val name = if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
                name
            }
        }

Perceba que nós usamos o operador Elvis para retornar "Unknown" se user.lastName for nulo, uma vez que user.lastName é do tipo String? e uma String é requerida para o name.

...
else {
    user.lastName ?: "Unknown"
}
...

Para simplificar ainda mais, podemos remover completamente a variável name:

    
val formattedUserNames: List<String>
        get() {
            return users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

Vimos que o conversor automático substituiu a função getFormattedUserNames() por uma propriedade chamada formattedUserNames que possui um getter personalizado. Por baixo dos panos o Kotlin ainda gera um método getFormattedUserNames() que retorna um List.

Em Java, nossas propriedades de classe seriam expostas por meio de funções getter e setter. O Kotlin nos permite ter uma melhor diferenciação entre propriedades de uma classe, expressa com campos, e funcionalidades, ações que uma classe pode fazer, expressas com funções. No nosso caso, a classe Repository é muito simples e não realiza nenhuma ação, por isso só tem campos.

A lógica que foi definida na função Java getFormattedUserNames() agora é acionada ao chamar o getter da propriedade formattedUserNames do Kotlin.

Embora não tenhamos explicitamente um campo correspondente à propriedade formattedUserNames, o Kotlin nos fornece um campo de apoio automático denominado field, que podemos acessar, se necessário, de getters e setters personalizados.

Às vezes, no entanto, queremos alguma funcionalidade extra que o campo de apoio automático não forneça.

Vamos passar por um exemplo a seguir.

Dentro de nossa classe Repository temos uma lista mutável de usuários que está sendo exposta na função getUsers que foi gerada a partir do nosso código Java:

fun getUsers(): List<User>? {
    return users
}

O problema aqui é que, ao retornar users, qualquer consumidor da classe Repository pode modificar nossa lista de usuários - isso não é uma boa ideia! Vamos consertar isso usando uma propriedade de apoio.

Primeiro, vamos renomear users para _users. Selecione o nome da variável, clique com o botão direito e selecione Refactor > Rename para renomear a variável. Agora, adicione uma propriedade pública imutável que retorne uma lista de usuários. Vamos chamá-lo de users:

private val _users = mutableListOf<User>()
val users: List<User>
    get() = _users

Com isso, você pode excluir o método getUsers().

Com essa alteração acima, a propriedade privada de _users se torna a propriedade de suporte para a propriedades pública de users. Fora da classe Repository, a lista _users não é modificável, pois os consumidores da classe só podem acessar a lista por meio de users.

Código completo:

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user ->
                if (user.lastName != null) {
                    if (user.firstName != null) {
                        "${user.firstName} ${user.lastName}"
                    } else {
                        user.lastName ?: "Unknown"
                    }
                }  else {
                    user.firstName ?: "Unknown"
                }
            }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

No momento, a classe Repository sabe como formatar um nome de usuário para um objeto User. Mas se quisermos reutilizar a mesma lógica de formatação em outras classes, precisamos copiar e colar ou movê-la para a classe User.

O Kotlin fornece a capacidade de declarar funções e propriedades fora de qualquer classe, objeto ou interface. Por exemplo, a função mutableListOf() que usamos para criar uma nova instância de uma List é definida diretamente em Collections.kt da Biblioteca Padrão.

Em Java, sempre que você precisar de alguma funcionalidade de utilitário, você provavelmente criará uma classe Util e declarará essa funcionalidade como uma função estática. Em Kotlin você pode declarar funções de alto nível, sem ter uma classe. No entanto, o Kotlin também fornece a capacidade de criar funções de extensão. Essas são funções que estendem um certo tipo, mas são declaradas fora do tipo.

A visibilidade das funções e propriedades de extensão pode ser restrita usando modificadores de visibilidade. Eles restringem o uso somente a classes que precisam das extensões e não poluem o namespace.

Para a classe User, podemos adicionar uma função de extensão que computa o nome formatado ou podemos manter o nome formatado em uma propriedade de extensão. Pode ser adicionado fora da classe Repository, no mesmo arquivo:

// função de extensão
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

// propriedade de extensão
val User.userFormattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

// uso:
val user = User(...)
val name = user.getFormattedName()
val formattedName = user.userFormattedName

Podemos então usar as funções de extensão e as propriedades como se fizessem parte da classe User.

Como o nome formatado é uma propriedade de User e não uma funcionalidade da classe Repository, vamos usar a propriedade de extensão. Nosso arquivo Repository agora se parece com isto:

val User.formattedName: String
    get() {
        return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf<User>()
    val users: List<User>
      get() = _users

    val formattedUserNames: List<String>
        get() {
            return _users.map { user -> user.formattedName }
        }

    init {
        val user1 = User("Jane", "")
        val user2 = User("John", null)
        val user3 = User("Anne", "Doe")

        _users.add(user1)
        _users.add(user2)
        _users.add(user3)
    }
}

A Biblioteca Padrão do Kotlin usa funções de extensão para estender a funcionalidade de várias APIs do Java; muitas das funcionalidades do Iterable e Collection são implementadas como funções de extensão. Por exemplo, a função map que usamos anteriormente é uma função de extensão em Iterable.

Em nosso código de classe Repository, estamos adicionando vários objetos User à lista _users. Essas chamadas podem ser mais idiomáticas com a ajuda de funções de escopo.

Para executar código apenas no contexto de um objeto específico, sem precisar acessar o objeto com base em seu nome, o Kotlin criou 5 funções de escopo: let, apply, with, run e also. Essas funções deixam o seu código fácil de ler e mais conciso. Todas essas funções têm um receptor (this), podem ter um argumento (it) e podem retornar um valor. Você decidirá qual usar, dependendo do que deseja alcançar.

Aqui está uma folha de dicas (cheat sheet) útil para ajudá-lo a lembrar disso:

Como estamos configurando nosso objeto _users em nosso Repository, podemos tornar o código mais idiomático usando a função apply:

init {
    val user1 = User("Jane", "")
    val user2 = User("John", null)
    val user3 = User("Anne", "Doe")
   
    _users.apply {
       // this == _users
       add(user1)
       add(user2)
       add(user3)
    }
 }

Neste codelab, cobrimos as noções básicas que você precisa para começar a refatorar seu código de Java para Kotlin. Essa refatoração é independente da sua plataforma de desenvolvimento e ajuda a garantir que o código que você escreve seja idiomático.

O Kotlin idiomático torna a escrita de código curta e prazerosa. Com todas as funcionalidades que o Kotlin oferece, existem muitas formas de tornar o seu código mais seguro, mais conciso e mais legível. Por exemplo, podemos até otimizar nossa classe Repository instanciando a lista de _users com usuários diretamente na declaração, livrando-se do bloco init:

private val users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))

Cobrimos uma grande variedade de tópicos, desde lidar com capacidade de nulidade, singletons, Strings e coleções até tópicos como funções de extensão, funções de alto nível, propriedades e funções de escopo. Nós fomos de duas classes Java para duas do Kotlin que agora se parecem com isso:

User.kt

data class User(var firstName: String?, var lastName: String?)

Repository.kt

val User.formattedName: String
    get() {
       return if (lastName != null) {
            if (firstName != null) {
                "$firstName $lastName"
            } else {
                lastName ?: "Unknown"
            }
        } else {
            firstName ?: "Unknown"
        }
    }

object Repository {

    private val _users = mutableListOf(User("Jane", ""), User("John", null), User("Anne", "Doe"))
    val users: List<User>
        get() = _users

    val formattedUserNames: List<String>
        get() = _users.map { user -> user.formattedName }
}

Aqui está um resumo das funcionalidades em Java e seu mapeamento para Kotlin:

Java

Kotlin

Objeto final

Objeto val

equals()

==

==

===

Classe que só mantém os dados

Classe data

Initialização no constructor

Initialização no bloco init

Campos e funções static

Campos e funções declaradas em um companion object

Classe singleton

object

Para saber mais sobre o Kotlin e como usá-lo em sua plataforma, confira estes materiais: