Refatoração 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ê vai 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 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;
    }

}

Dependendo do tipo de projeto, importe androidx.annotation.Nullable se estiver usando um projeto Android. Caso contrário, importe org.jetbrains.annotations.Nullable.

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 refatoração automática do código Java em código Kotlin, mas, às vezes, precisa de uma ajudinha. Faremos isso primeiro e, em seguida, veremos o código refatorado para entender como e por que ele foi refatorado dessa maneira.

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.

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. Ele faz isso anexando 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 refatoraçã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("John", "Doe")

Os parâmetros da função podem ser nomeados ao chamar funções:

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, você precisa 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")

Antes de continuar, confira se a classe do User é uma data. 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:

  • Um bloco init foi adicionado (Repository.kt#L50)
  • O campo static agora faz parte de um bloco companion object (Repository.kt#L33)
  • A lista de users é anulável, já que o objeto não foi instanciado no momento da declaração (Repository.kt#L7).
  • O método getFormattedUserNames() agora é uma propriedade chamada formattedUserNames (Repository.kt#L11).
  • A iteração na lista de usuários (que inicialmente fazia parte de getFormattedUserNames() tem uma sintaxe diferente da Java (Repository.kt#L15).

Antes de prosseguir, vamos limpar o código. 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 pode conter usuários nulos. Então, faremos o seguinte:

  • Remova ? em User? da declaração de tipo users.
  • getUsers precisa retornar List<User>?

O conversor automático também se divide em duas linhas de forma desnecessária as declarações de variáveis do usuário e as definidas no bloco init. Vamos colocar cada declaração de variável em uma linha. Veja como ficará nosso código:

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

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 do static 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 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 o objeto complementar e substitua a definição da classe por object Repository.

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 users = Repository.users

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 de dados 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. Vamos atualizar o loop for desta forma:

 
for ((firstName, lastName) in users!!) {
       val name: String?

       if (lastName != null) {
          if (firstName != null) {
                name = "$firstName $lastName"
          } else {
                name = lastName
          }
       } else if (firstName != null) {
            name = firstName
       } else {
            name = "Unknown"
       }
       userNames.add(name)
}

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. Para todos os usos do objeto users, é usado o operador de declaração não nulo !!. Ele converte qualquer variável para um tipo não nulo e gera uma exceção se o valor for 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.

Em nosso caso, sabemos que a lista de usuários não precisa ser anulável, já que ela é inicializada logo após a criação do objeto. Assim, podemos instanciá-lo diretamente quando o declararmos.

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(), fornecer o tipo de elemento de lista, remover a chamada de construtor ArrayList do bloco init e remover 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).

Com essas alterações, a propriedade users agora não é nula, e podemos remover todas as ocorrências desnecessárias do operador !!.

val userNames: MutableList<String?> = ArrayList(users.size)
for ((firstName, lastName) in users) {
    ...
}

Além disso, como a variável de usuários já foi inicializada, precisamos remover a inicialização do bloco init:

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

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

Como lastName e firstName podem ser null, precisamos lidar com a nulidade quando criamos a lista de nomes de usuário formatados. Como queremos exibir "Unknown" se faltar um nome, podemos tornar o nome não nulo removendo ? da declaração de tipo.

val name: String

Se lastName for nulo, name será firstName ou "Unknown":

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

Isso 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, user.firstName será retornado se não for nulo. Se user.firstName for nulo, a expressão retornará o valor do lado direito, "Unknown":

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

O Kotlin facilita trabalhar com Strings com modelos de string (link em inglês). Os modelos de string permitem referenciar variáveis nas declarações de string.

O conversor automático atualizou a concatenação do nome e do sobrenome para fazer referência ao nome da variável diretamente na string usando o símbolo $ e colocar a expressão entre { }.

// Java
name = user.getFirstName() + " " + user.getLastName();

// Kotlin
name = "${user.firstName} ${user.lastName}"

No código, substitua a concatenação de strings por:

name = "$firstName $lastName"

Em Kotlin, if, when, for e while são expressões, ou seja, retornam um valor. O ambiente de desenvolvimento integrado está mostrando um aviso de que a atribuição precisa ser retirada da if:

Vamos seguir a sugestão do ambiente de desenvolvimento integrado e levantar a atribuição para as duas instruções if. A última linha da instrução if será atribuída. Dessa forma, fica mais claro que a única finalidade desse bloco é inicializar o valor do nome:

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

Em seguida, enviaremos um aviso informando que a declaração name pode ser mesclada na atribuição. Vamos aplicar isso também. Como o tipo da variável de nome pode ser deduzido, podemos remover a declaração de tipo explícito. Agora, a formattedUserNames ficará assim:

val formattedUserNames: List<String?>
   get() {
       val userNames: MutableList<String?> = ArrayList(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
   }

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

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 exemplo abaixo.

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. Agora, adicione uma propriedade pública imutável que retorna uma lista de usuários. Vamos chamá-lo de users:

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

Com essa mudança, 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 um List é definida diretamente em Collections.kt (link em inglês) na biblioteca padrão.

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. Assim sendo, eles têm uma afinidade com esse tipo.

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 do usuário, e 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 de usuário à lista _users. Essas chamadas podem ser mais idiomáticas com a ajuda de funções de escopo.

Para executar o código apenas no contexto de um objeto específico, sem precisar acessar o objeto com base no nome, o Kotlin criou cinco funções de escopo: let, apply, with, run e also. Curtos e eficientes, todas essas funções têm um receptor (this), podem ter um argumento (it) e podem retornar um valor. Você precisa decidir qual usar, dependendo do que deseja alcançar.

Veja uma folha de referência útil para ajudar você a se lembrar disso:

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 começar a refatorar seu código do Java para o Kotlin. Essa refatoração é independente da plataforma de desenvolvimento e ajuda a garantir que o código programado seja 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

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: