Como converter para Kotlin

Neste codelab, você aprenderá a converter seu código de Java para Kotlin. Você também aprenderá quais são as convenções da linguagem Kotlin e como garantir que o código que está escrevendo siga essas convenções.

Este codelab é adequado para qualquer desenvolvedor que usa Java e está pensando em migrar o projeto para Kotlin. Começaremos com algumas classes Java que serão convertidas em Kotlin usando o ambiente de desenvolvimento integrado. Em seguida, analisaremos o código convertido e veremos como melhorá-lo, tornando-o mais idiomático (link em inglês), e como evitar armadilhas comuns.

O que você aprenderá

Você aprenderá a converter Java em Kotlin. Ao fazer isso, você aprenderá sobre os seguintes recursos e conceitos da linguagem Kotlin:

  • Como lidar com a nulidade
  • Como implementar Singletons
  • Classes de dados
  • Como lidar com strings
  • Operador Elvis
  • Desestruturação
  • Propriedades comuns e de apoio
  • Argumentos padrão e parâmetros nomeados
  • Como trabalhar com coleções
  • Funções de extensão
  • Funções e parâmetros de nível superior
  • Palavras-chave let, apply, with e run

Suposições

Você provavelmente já tem conhecimento sobre Java.

Pré-requisitos

Criar um novo projeto

Se você estiver usando o IntelliJ IDEA, crie um novo projeto Java usando Kotlin/JVM.

Se estiver usando o Android Studio, crie um novo projeto sem atividades. O SDK mínimo pode ter qualquer valor. Isso não afetará o resultado.

O código

Criaremos um objeto modelo User e uma classe Singleton Repository que funciona com objetos User e expõe listas de usuários e nomes de usuário formatados.

Crie um novo arquivo com o nome User.java em 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;
    }

}

O ambiente de desenvolvimento integrado avisará que @Nullable não está definido. Importe androidx.annotation.Nullable se estiver usando o Android Studio ou org.jetbrains.annotations.Nullable se estiver usando o IntelliJ.

Crie um novo arquivo com o nome Repository.java e cole o seguinte código:

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

Nosso ambiente de desenvolvimento integrado pode fazer uma boa conversão automática do código Java em código Kotlin, mas, às vezes, precisa de uma ajudinha. Vamos deixar o ambiente de desenvolvimento integrado fazer a conversão inicialmente. Depois, analisaremos o código resultante para entender como e por que ele foi convertido dessa forma.

Acesse o arquivo User.java e converta-o em Kotlin: Barra de menus -> Code -> Convert Java File to Kotlin File.

Se o ambiente de desenvolvimento integrado solicitar uma correção após a conversão, pressione Yes.

25e57db5b8e76557.png

Você verá o seguinte código Kotlin:

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

Observe que User.java foi renomeado como User.kt. Os arquivos Kotlin têm a extensão .kt.

Nossa classe Java User tinha duas propriedades: firstName e lastName. Cada uma tinha um método getter e setter, tornando o valor mutável. A palavra-chave Kotlin para variáveis mutáveis é var. Portanto, o conversor usa var para cada uma dessas propriedades. Se nossas propriedades Java tivessem apenas getters, elas seriam imutáveis e declaradas como variáveis val. val é semelhante à palavra-chave final em Java.

Uma das principais diferenças entre Kotlin e Java é que a linguagem Kotlin especifica explicitamente se uma variável pode aceitar um valor nulo. Para fazer isso, anexe um ? à declaração de tipo.

Como marcamos firstName e lastName como anuláveis, o conversor marcou automaticamente as propriedades como anuláveis com String?. Se você anotar os membros Java como não nulos (usando org.jetbrains.annotations.NotNull ou androidx.annotation.NonNull), o conversor reconhecerá isso e os campos também serão não nulos em Kotlin.

A conversão básica já foi concluída. No entanto, podemos escrever o código de maneira mais idiomática. Veja como fazer isso.

Classe de dados

A classe User apenas retém dados. O Kotlin tem uma palavra-chave para classes com essa finalidade: data. Ao marcar essa classe como data, o compilador criará getters e setters automaticamente. Ele também deriva as funções equals(), hashCode() e toString().

Vamos adicionar a palavra-chave data à classe User:

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

O Kotlin, assim como Java, pode ter um construtor principal e um ou mais construtores secundários. O usado no exemplo acima é o construtor principal da classe User. Se você estiver convertendo uma classe Java que tem vários construtores, o conversor também criará vários construtores em Kotlin automaticamente. Eles são definidos com a palavra-chave constructor.

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

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

Igualdade

O Kotlin tem 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, podemos atribuir valores padrão a argumentos em chamadas de função. O valor padrão é usado quando o argumento é omitido. Em Kotlin, os construtores também são funções. Por isso, podemos usar argumentos padrão para especificar que o valor padrão de lastName é null. Para fazer isso, basta atribuir null a lastName.

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

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

O Kotlin permite marcar argumentos quando as funções são chamadas:

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

Como um caso de uso diferente, digamos que firstName tenha null como valor padrão e lastName não. Nesse caso, como o parâmetro padrão precede um parâmetro sem valor padrão, é preciso chamar a função com argumentos nomeados:

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

// usage
val jane = User(lastName = "Doe") // same as User(null, "Doe")
val john = User("John", "Doe")

Os valores padrão são um conceito importante e frequentemente usado em códigos Kotlin. Neste codelab, como queremos sempre especificar o nome e o sobrenome na declaração de objeto User, não precisamos de valores padrão.

Antes de continuar o codelab, verifique se a classe User é uma classe data. Agora, vamos converter a classe Repository em Kotlin. O resultado da conversão automática será semelhante a este:

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
            }
    }

    // keeping the constructor private to enforce the usage of 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)
    }
}

Vejamos o que o conversor automático fez:

  • A lista de users é anulável, já que o objeto não foi instanciado no momento da declaração.
  • As 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 da lista de usuários (que, inicialmente, fazia parte de getFormattedUserNames()) tem uma sintaxe diferente da linguagem Java.
  • O campo static agora faz parte de um bloco companion object.
  • Um bloco init foi adicionado.

Antes de prosseguir, vamos limpar o código. Se observarmos o construtor, perceberemos que o conversor tornou nossa lista de users mutável que contém objetos anuláveis. A lista pode ser realmente nula, mas vamos supor que ela não possa conter usuários nulos. Então, faremos o seguinte:

  • Remova ? em User? da declaração de tipo users.
  • Remova ? em User? para o tipo de retorno de getUsers(), para que retorne List<User>?.

Bloco de inicialização

Em Kotlin, o construtor principal não pode conter nenhum código. Assim, o código de inicialização é colocado em 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 lida com propriedades de inicialização. Isso também pode ser feito na declaração da propriedade. Por exemplo, na versão Kotlin da classe Repository, vemos que a propriedade dos usuários foi inicializada na declaração.

private var users: MutableList<User>? = null

Propriedades e métodos static do Kotlin

Em Java, a palavra-chave static é usada para campos ou funções para dizer que pertencem a uma classe, mas não a uma instância da classe. Por isso, criamos o campo estático INSTANCE na classe Repository. O equivalente em Kotlin para isso é o bloco companion object. Nele, você também declararia os campos e as funções estáticas. O conversor criou o bloco de objetos complementares e moveu o campo INSTANCE para ele.

Como lidar com Singletons

Como precisamos de apenas uma instância da classe Repository, usamos o padrão Singleton em Java. Com Kotlin, é possível aplicar esse padrão no nível do compilador substituindo a palavra-chave class por object.

Remova o construtor particular e substitua a definição da classe por object Repository. Remova o objeto complementar também.

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
   }

    // keeping the constructor private to enforce the usage of 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, chamamos apenas funções e propriedades diretamente no objeto deste modo:

val formattedUserNames = Repository.formattedUserNames

Se uma propriedade não tiver um modificador de visibilidade, ela será pública por padrão, como no caso da propriedade formattedUserNames no objeto Repository.

Ao converter a classe Repository em Kotlin, o conversor automático tornou a lista de usuários anulável porque ela não foi inicializada em um objeto quando foi declarada. Como resultado, para todos os usos do objeto users, o operador de declaração não nulo !! precisa ser usado. users!! e user!! serão usados em todo o código convertido. O operador !! converte qualquer variável em um tipo não nulo. Assim, você pode acessar propriedades ou chamar funções com ele. No entanto, uma exceção será lançada se o valor da variável for realmente nulo. Ao usar !!, você corre o risco de gerar exceções no tempo de execução.

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

  • Faça uma verificação de valores nulos (if (users != null) {...}).
  • Use o operador Elvis (link em inglês) ?: que será abordado posteriormente no codelab.
  • Use algumas das funções padrão do Kotlin, abordadas posteriormente no codelab.

No nosso caso, sabemos que a lista de usuários não precisa ser anulável, porque ela é inicializada após a criação do objeto (no bloco init). Assim, podemos instanciar o objeto users diretamente quando ele for declarado.

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. Estamos usando uma MutableList para users:

private var users: MutableList<User>? = null

Para simplificar, podemos usar a função mutableListOf() (link em inglês) e fornecer o tipo de elemento de lista. mutableListOf<User>() cria uma lista vazia que pode conter objetos User. Como o tipo de dados da variável agora pode ser inferido pelo compilador, remova a declaração de tipo explícito da propriedade users.

private val users = mutableListOf<User>()

Também mudamos var para val porque os usuários terão uma referência imutável da lista de usuários. A referência é imutável, mas a lista é mutável (é possível adicionar ou remover elementos).

Como a variável users já foi inicializada, remova essa inicialização do bloco init:

users = ArrayList<Any?>()

Depois, o bloco init ficará assim:

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 alterações, a propriedade users agora não é nula, e podemos remover todas as ocorrências desnecessárias do operador !!. Os erros de compilação ainda acontecerão no Android Studio. Siga as próximas etapas dos codelabs 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)
}

Além disso, para o valor userNames, se você especificar que o tipo ArrayList contém Strings, poderá remover o tipo explícito na declaração porque isso será inferido.

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

Desestruturação

O Kotlin permite desestruturar um objeto em diversas variáveis, usando uma sintaxe chamada declaração de desestruturação. Criamos diversas variáveis e podemos usá-las de forma independente.

Por exemplo, as classes data são compatíveis com a desestruturação. Dessa forma, podemos desestruturar o objeto User no loop for como (firstName, lastName). Isso permite trabalhar diretamente com os valores firstName e lastName. Atualize o loop for conforme mostrado abaixo. Substitua todas as instâncias de user.firstName por 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ão if

Os nomes na lista userNames ainda não estão no formato que queremos. Como lastName e firstName podem ser null, precisamos lidar com a nulidade quando criamos a lista de nomes de usuário formatados. Queremos exibir "Unknown" se faltar um nome. Como a variável name não será alterada depois de ser definida uma vez, podemos usar val em vez de var. Faça essa alteração primeiro.

val name: String

Veja o código que define a variável de nome. Pode parecer novidade para você ver uma variável definida como igual e como bloco de código if / else. Isso é permitido porque, em Kotlin, if, when, for e while são expressões, ou seja, retornam um valor. A última linha da instrução if será atribuída a name. A única finalidade desse bloco é inicializar o valor name.

Basicamente, a lógica apresentada aqui é a seguinte: se lastName for nulo, name será definido como firstName ou "Unknown".

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

Operador Elvis

Esse código pode ser programado de maneira mais idiomática com o operador Elvis ?: (link em inglês). 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.

No código a seguir, firstName será retornado se não for nulo. Se firstName for nulo, a expressão retornará o valor do lado direito, "Unknown":

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

O Kotlin facilita trabalhar usando Strings com modelos de string (link em inglês). Os modelos de string permitem referenciar variáveis nas declarações de string usando o símbolo $ antes da variável. Também é possível colocar uma expressão em uma declaração de string, colocando a expressão dentro de { } e usando o símbolo $ antes dela. Exemplo: ${user.firstName}.

No momento, seu código usa a concatenação de strings para combinar firstName e lastName com o nome do usuário.

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

Em vez disso, substitua a concatenação de strings por:

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

Usar modelos de string pode simplificar o código.

O ambiente de desenvolvimento integrado mostrará avisos se houver uma maneira mais idiomática de programar o código. Você verá um sublinhado no código. Ao passar o cursor sobre ele, será exibida uma sugestão de como refatorar o código.

No momento, você verá um aviso informando que a declaração name pode ser mesclada na atribuição. Vamos fazer isso. Como o tipo da variável name pode ser deduzido, podemos remover a declaração de tipo String explícita. Nosso código para formattedUserNames ficará assim:

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
    }

Podemos fazer outro ajuste. Nossa lógica de IU exibirá "Unknown" caso o nome e o sobrenome estejam ausentes. Por isso, não oferecemos compatibilidade com objetos nulos. Portanto, para o tipo de dados de formattedUserNames, substitua List<String?> por List<String>.

val formattedUserNames: List<String>

Vamos ver em detalhes o getter de formattedUserNames e ver como podemos torná-lo mais idiomático. Agora, o código faz o seguinte:

  • Cria uma nova lista de strings.
  • Itera a lista de usuários.
  • Cria o nome formatado de cada usuário com base no nome e no sobrenome dele.
  • 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 oferece uma lista extensa de transformações de coleções (link em inglês) que tornam o desenvolvimento mais rápido e seguro, expandindo os recursos da API Java Collections. Uma delas é a função map (link em inglês). Essa função retorna uma nova lista que contém os resultados da aplicação de uma determinada função de transformação em cada elemento da lista original. Então, 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 tínhamos no loop for para o corpo de map. Por padrão, o nome do item atual da lista usado em map é it (link em inglês), mas, para facilitar a legibilidade, você pode substituir it pelo nome da sua variável. No nosso caso, vamos nomeá-lo 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
            }
        }

Usaremos o operador Elvis para retornar "Unknown" se user.lastName for nulo, porque user.lastName é do tipo String? e uma String é necessária para o name.

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

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

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

Notamos que o conversor automático substituiu a função getFormattedUserNames() por uma propriedade chamada formattedUserNames, que tem um getter personalizado. Internamente, o Kotlin ainda gera um método getFormattedUserNames() que retorna uma List.

Em Java, exporíamos as propriedades de classe usando funções getter e setter. O Kotlin nos permite uma diferenciação melhor entre as propriedades de uma classe, expressas com campos e funcionalidades, e as ações que uma classe pode fazer, expressas em funções. No nosso caso, a classe Repository é muito simples e não executa nenhuma ação. Portanto, ela tem apenas campos.

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

Embora não tenhamos um campo explícito correspondente à propriedade formattedUserNames, o Kotlin fornece um campo de apoio automático com o nome field, que pode ser acessado por getters e setters personalizados, se necessário.

No entanto, às vezes, você quer adicionar funcionalidades extras que não são fornecidas pelo campo de apoio automático.

Veja o seguinte exemplo.

Na classe Repository, temos uma lista mutável de usuários que está sendo exposta na função getUsers(), que foi gerada do código Java:

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

O problema é que, ao retornar users, qualquer consumidor da classe do repositório pode modificar a lista de usuários, o que não é uma boa ideia. Para corrigir isso, use uma propriedade de apoio.

Primeiro, vamos renomear users como _users. Destaque o nome da variável e clique com o botão direito do mouse em Refactor > Rename. Em seguida, adicione uma propriedade pública imutável que retorna uma lista de usuários. Vamos dar o nome users a ela:

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

Nesse ponto, é possível excluir o método getUsers().

Com a alteração acima, a propriedade _users particular se torna a propriedade de apoio da propriedade users pública. Fora da classe Repository, a lista _users não pode ser modificada, porque os consumidores da classe só podem acessar a lista dos 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)
    }
}

Agora, a classe Repository sabe como calcular o nome de usuário formatado para um objeto User. No entanto, se quisermos reutilizar a mesma lógica de formatação em outras classes, será necessário copiá-la e colá-la ou movê-la para a classe User.

O Kotlin oferece a possibilidade 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 já está definida em Collections.kt (link em inglês) na biblioteca padrão do Kotlin.

Em Java, sempre que você precisar de alguma função utilitária, provavelmente criará uma classe Util e declarará essa funcionalidade como uma função estática. Em Kotlin, você pode declarar funções de nível superior sem uma classe. No entanto, o Kotlin também permite criar funções de extensão. São funções que estendem um determinado tipo, mas são declaradas fora dele.

A visibilidade de funções e propriedades de extensão pode ser restrita usando modificadores de visibilidade. Eles restringem o uso apenas 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 para calcular o nome formatado ou mantê-lo em uma propriedade de extensão. Ela pode ser adicionada fora da classe Repository, no mesmo arquivo:

// extension function
fun User.getFormattedName(): String {
    return if (lastName != null) {
        if (firstName != null) {
            "$firstName $lastName"
        } else {
            lastName ?: "Unknown"
        }
    } else {
        firstName ?: "Unknown"
    }
}

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

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

Em seguida, podemos usar as funções e propriedades de extensão como se fizessem parte da classe User.

Como o nome formatado é uma propriedade da classe User, não uma funcionalidade da classe Repository, vamos usar a propriedade de extensão. Nosso arquivo Repository ficará assim:

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 (link em inglês) usa funções de extensão para estender a funcionalidade de várias APIs Java. Muitas funcionalidades em Iterable e Collection são implementadas como funções de extensão. Por exemplo, a função map que usamos em uma etapa anterior é uma função de extensão em Iterable.

No código da classe Repository, adicionamos vários objetos User à lista _users. Essas chamadas podem ser mais idiomáticas com a ajuda das funções de escopo do Kotlin.

Para executar o código somente no contexto de um objeto específico, sem precisar acessar o objeto com base no nome, o Kotlin oferece cinco funções de escopo: let, apply, with, run e also. Essas funções tornam o código mais fácil de ler e mais conciso. Todas as funções de escopo têm um receptor (this), podem ter um argumento (it) e podem retornar um valor.

Veja uma folha de referência útil para ajudar você a lembrar quando usar cada função:

6b9283d411fb6e7b.png

Como estamos configurando o objeto _users no 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, abordamos os princípios básicos necessários para você começar a converter seu código Java para Kotlin. Essa conversão é independente da plataforma de desenvolvimento e ajuda a garantir que o código programado esteja em Kotlin idiomático.

O Kotlin idiomático torna a escrita do código curta e agradável. Com todos os recursos que o Kotlin oferece, há muitas maneiras de tornar seu código mais seguro, conciso e legível. Por exemplo, podemos otimizar a classe Repository instanciando a lista _users com usuários diretamente na declaração, eliminando o bloco init:

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

Falamos sobre uma grande variedade de tópicos, desde como lidar com nulidade, Singletons, strings e coleções a tópicos como funções de extensão, funções de nível superior, propriedades e funções de escopo. Passamos de duas classes Java para duas outras Kotlin que agora são assim:

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 }
}

Este é um resumo das funcionalidades Java e do mapeamento para Kotlin:

Java

Kotlin

Objeto final

Objeto val

equals()

==

==

===

Classe que apenas retém dados

Classe data

Inicialização no construtor

Inicialização no bloco init

Campos e funções static

Campos e funções declarados em um companion object

Classe Singleton

object

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