Conheça a linguagem de programação Kotlin

Kotlin é uma linguagem de programação amplamente usados por desenvolvedores Android em todos os lugares. Este tópico funciona como um curso intensivo do Kotlin para você começar a usá-lo rapidamente.

Declaração de variável

O Kotlin usa duas palavras-chave diferentes para declarar variáveis: val e var.

  • Use val para uma variável cujo valor nunca muda. Não é possível reatribuir um valor a uma variável que tenha sido declarada usando val.
  • Use var para uma variável cujo valor possa ser mudado.

No exemplo abaixo, count é uma variável do tipo Int que recebe um valor inicial de 10:

var count: Int = 10

Int é um tipo que representa um número inteiro, um dos muitos tipos numéricos que podem ser representados em Kotlin. Assim como acontece com outras linguagens, você também pode usar Byte, Short, Long, Float e Double, dependendo dos seus dados numéricos.

A palavra-chave var significa que você pode reatribuir valores a count conforme necessário. Por exemplo, você pode mudar o valor de count de 10 para 15:

var count: Int = 10
count = 15

No entanto, alguns valores não podem ser mudados. Considere um String chamado languageName. Se você quiser garantir que languageName sempre tenha o valor "Kotlin", poderá declarar languageName usando a palavra-chave val:

val languageName: String = "Kotlin"

Essas palavras-chave permitem que você seja explícito sobre o que pode ser mudado. Use-as em seu favor conforme necessário. Se uma referência de variável precisar ser reatribuível, declare-a como var. Do contrário, use val.

Inferência de tipo

Continuando com o exemplo anterior, quando você atribui um valor inicial a languageName, o compilador Kotlin pode inferir o tipo com base no tipo do valor atribuído.

Como o valor de "Kotlin" é do tipo String, o compilador infere que languageName também é um String. O Kotlin é uma linguagem estática. Isso significa que o tipo é resolvido no momento da compilação e nunca muda.

No exemplo a seguir, languageName é inferido como String. Portanto, não é possível chamar nenhuma função que não faça parte da classe String:

val languageName = "Kotlin"
val upperCaseName = languageName.toUpperCase()

// Fails to compile
languageName.inc()

toUpperCase() é uma função que só pode ser chamada em variáveis do tipo String. Como o compilador Kotlin inferiu languageName como String, você pode chamar toUpperCase() com segurança. inc(), entretanto, é uma função de operador Int, por isso não pode ser chamada em String. A abordagem do Kotlin para a inferência de tipos oferece concisão e segurança de tipos.

Segurança nula

Em algumas linguagens, uma variável de tipo de referência pode ser declarada sem fornecer um valor explícito inicial. Nesses casos, as variáveis geralmente contêm um valor nulo. Por padrão, as variáveis do Kotlin não podem reter valores nulos. Isso significa que o snippet a seguir é inválido.

// Fails to compile
val languageName: String = null

Para que uma variável mantenha um valor nulo, ela precisa ser do tipo anulável. Você pode especificar uma variável como sendo anulável, usando um sufixo do tipo com ?, conforme mostrado neste exemplo a seguir.

val languageName: String? = null

Com um tipo String?, você pode atribuir um valor String ou null a languageName.

Você precisa lidar com variáveis anuláveis com cuidado ou corre o risco de ter um NullPointerException. Em Java, por exemplo, se você tentar invocar um método em um valor nulo, seu programa falhará.

O Kotlin fornece uma série de mecanismos para trabalhar com segurança com variáveis anuláveis. Para ver mais informações, consulte Padrões comuns do Kotlin no Android: anulação (link em inglês).

Condicionais

O Kotlin apresenta vários mecanismos para implementar a lógica condicional. O mais comum deles é uma instrução if-else. Se uma expressão entre parênteses ao lado de uma palavra-chave if for avaliada como true, o código dentro dessa ramificação (ou seja, o código imediatamente seguinte que é encapsulado entre chaves) será executado. Caso contrário, será executado o código dentro da ramificação else.

if (count == 42) {
    println("I have the answer.")
} else {
    println("The answer eludes me.")
}

Você pode representar várias condições usando else if. Isso permite representar uma lógica mais granular e complexa em uma única instrução condicional, conforme mostrado neste exemplo:

if (count == 42) {
    println("I have the answer.")
} else if (count > 35) {
    println("The answer is close.")
} else {
    println("The answer eludes me.")
}

As instruções condicionais são úteis para representar a lógica com estado, mas você pode se repetir ao gravá-las. No exemplo acima, você simplesmente imprime um String em cada ramificação. Para evitar essa repetição, o Kotlin oferece expressões condicionais. O último exemplo pode ser regravado da seguinte forma:

val answerString: String = if (count == 42) {
    "I have the answer."
} else if (count > 35) {
    "The answer is close."
} else {
    "The answer eludes me."
}

println(answerString)

Implicitamente, cada ramificação condicional retorna o resultado da expressão na linha final, de modo que não é necessário usar uma palavra-chave return. Como o resultado das três ramificações é do tipo String, o resultado da expressão if-else também é do tipo String. Neste exemplo, answerString recebe um valor inicial do resultado da expressão if-else. A inferência de tipos pode ser usada para omitir a declaração de tipo explícito para answerString, mas geralmente é uma boa ideia incluí-la para fins de esclarecimento.

Conforme a complexidade da instrução condicional aumenta, é recomendável substituir a expressão if-else por uma expressão when, conforme mostrado neste exemplo:

val answerString = when {
    count == 42 -> "I have the answer."
    count > 35 -> "The answer is close."
    else -> "The answer eludes me."
}

println(answerString)

Cada ramificação em uma expressão when é representada por uma condição, uma seta (->) e um resultado. Se a condição no lado esquerdo da seta for avaliada como verdadeira, o resultado da expressão no lado direito será retornado. Observe que a execução não passa de uma ramificação para a próxima. O código no exemplo de expressão when é funcionalmente equivalente ao do exemplo anterior, mas é mais fácil de ler.

As condicionais do Kotlin destacam um dos recursos mais avançados, a transmissão inteligente. Em vez de usar o operador de chamada segura ou o operador de declaração não nulo para trabalhar com valores anuláveis, você pode verificar se uma variável contém uma referência a um valor nulo usando uma instrução condicional, conforme mostrado neste exemplo:

val languageName: String? = null
if (languageName != null) {
    // No need to write languageName?.toUpperCase()
    println(languageName.toUpperCase())
}

Na ramificação condicional, languageName pode ser tratado como não anulável. O Kotlin é inteligente o suficiente para reconhecer que a condição para executar a ramificação é que languageName não contenha um valor nulo. Portanto, você não precisa tratar languageName como anulável nessa ramificação. Essa transmissão inteligente funciona para verificações nulas, verificações de tipo ou qualquer condição que satisfaça a um contrato.

Funções

Você pode agrupar uma ou mais expressões em uma função. Em vez de repetir a mesma série de expressões sempre que precisar de um resultado, você pode unir as expressões em uma função e chamar essa função.

Para declarar uma função, use a palavra-chave fun seguida pelo nome da função. Em seguida, defina os tipos de entrada que sua função assume, se houver, e declare o tipo de saída retornada. No corpo de uma função, você define expressões que são chamadas quando sua função é invocada.

Com base nos exemplos anteriores, veja uma função completa do Kotlin:

fun generateAnswerString(): String {
    val answerString = if (count == 42) {
        "I have the answer."
    } else {
        "The answer eludes me"
    }

    return answerString
}

A função no exemplo acima tem o nome generateAnswerString. Não é necessária nenhuma entrada. Ela gera um resultado do tipo String. Para chamar uma função, use o nome dela, seguido pelo operador de invocação (()). No exemplo abaixo, a variável answerString é inicializada com o resultado de generateAnswerString().

val answerString = generateAnswerString()

As funções podem receber argumentos como entrada, conforme mostrado neste exemplo:

fun generateAnswerString(countThreshold: Int): String {
    val answerString = if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }

    return answerString
}

Ao declarar uma função, você pode especificar qualquer número de argumentos e os tipos. No exemplo acima, generateAnswerString() leva um argumento chamado countThreshold do tipo Int. Dentro da função, você pode se referir ao argumento usando o nome dele.

Ao chamar essa função, você precisa incluir um argumento nos parênteses da chamada da função:

val answerString = generateAnswerString(42)

Como simplificar declarações de função

generateAnswerString() é uma função bastante simples. A função declara uma variável e, em seguida, retorna imediatamente. Quando o resultado de uma única expressão é retornado de uma função, você pode ignorar a declaração de uma variável local retornando diretamente o resultado da expressão if-else contida na função, conforme mostrado neste exemplo:

fun generateAnswerString(countThreshold: Int): String {
    return if (count > countThreshold) {
        "I have the answer."
    } else {
        "The answer eludes me."
    }
}

Você também pode substituir a palavra-chave de retorno pelo operador de atribuição:

fun generateAnswerString(countThreshold: Int): String = if (count > countThreshold) {
        "I have the answer"
    } else {
        "The answer eludes me"
    }

Funções anônimas

Nem todas as funções precisam de um nome. Algumas funções são identificadas mais diretamente por suas entradas e saídas. Essas funções são chamadas de funções anônimas. Você pode manter uma referência a uma função anônima usando essa referência para chamar a função anônima posteriormente. Você também pode passar a referência no seu app, como acontece com outros tipos de referência.

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

Assim como as funções nomeadas, as funções anônimas podem conter qualquer número de expressões. O valor retornado da função é o resultado da expressão final.

No exemplo acima, stringLengthFunc contém uma referência a uma função anônima que usa um String como entrada e retorna o comprimento da entrada String como saída do tipo Int. Por esse motivo, o tipo da função é denotado como (String) -> Int. No entanto, esse código não invoca a função. Para recuperar o resultado da função, você precisa invocá-lo como faria com uma função nomeada. Forneça um String ao chamar stringLengthFunc, conforme mostrado neste exemplo:

val stringLengthFunc: (String) -> Int = { input ->
    input.length
}

val stringLength: Int = stringLengthFunc("Android")

Funções de ordem superior

Uma função pode usar outra função como um argumento. As funções que usam outras funções como argumentos são chamadas de funções de ordem superior. Esse padrão é útil para a comunicação entre componentes, da mesma forma que você pode usar uma interface de callback em Java.

Veja um exemplo de uma função de ordem superior:

fun stringMapper(str: String, mapper: (String) -> Int): Int {
    // Invoke function
    return mapper(str)
}

A função stringMapper() usa um String junto com uma função que deriva um valor Int de um String transmitido nela.

Você pode chamar stringMapper() passando um String e uma função que satisfaça o outro parâmetro de entrada, ou seja, uma função que usa um String como entrada e gera um Int, conforme mostrado neste exemplo:

stringMapper("Android", { input ->
    input.length
})

Se a função anônima for o último parâmetro definido em uma função, você poderá transmiti-la para fora dos parênteses usados para chamar a função, conforme mostrado neste exemplo:

stringMapper("Android") { input ->
    input.length
}

As funções anônimas podem ser encontradas em toda a biblioteca padrão do Kotlin. Para mais informações, consulte Funções de ordem superior e lambdas (link em inglês).

Classes

Todos os tipos mencionados até agora estão integrados à linguagem de programação Kotlin. Se quiser adicionar um tipo personalizado, você poderá definir uma classe usando a palavra-chave class, conforme mostrado neste exemplo:

class Car

Propriedades

As classes representam o estado usando propriedades. Uma propriedade é uma variável de nível de classe que pode incluir um getter, um setter e um campo de backup. Como um carro precisa de rodas para dirigir, você pode adicionar uma lista de objetos Wheel como uma propriedade de Car, conforme mostrado neste exemplo:

class Car {
    val wheels = listOf<Wheel>()
}

Observe que wheels é um public val, o que significa que wheels pode ser acessado de fora da classe Car e não pode ser reatribuído. Se você quiser ter uma instância de Car, primeiro é necessário chamar seu construtor. A partir daí, você pode acessar qualquer uma das propriedades acessíveis.

val car = Car() // construct a Car
val wheels = car.wheels // retrieve the wheels value from the Car

Se você quiser personalizar suas rodas, poderá definir um construtor personalizado que especifica como as propriedades de classe são inicializadas:

class Car(val wheels: List<Wheel>)

No exemplo acima, o construtor de classe usa um List<Wheel> como argumento do construtor e usa esse argumento para inicializar a propriedade wheels.

Funções de classe e encapsulamento

As classes usam funções para modelar o comportamento. As funções podem modificar o estado, ajudando você a expor somente os dados que quer expor. Esse controle de acesso faz parte de um conceito maior orientado a objetos, conhecido como encapsulamento.

No exemplo a seguir, a propriedade doorLock é mantida privada em qualquer item fora da classe Car. Para desbloquear o carro, você precisa chamar a função unlockDoor() transmitindo uma chave válida, conforme mostrado neste exemplo:

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

Se quiser personalizar a forma como uma propriedade é referenciada, você poderá fornecer getter e setter personalizados. Por exemplo, se quiser expor o getter de uma propriedade ao restringir o acesso a setter, você poderá designar esse setter como private:

class Car(val wheels: List<Wheel>) {

    private val doorLock: DoorLock = ...

    var gallonsOfFuelInTank: Int = 15
        private set

    fun unlockDoor(key: Key): Boolean {
        // Return true if key is valid for door lock, false otherwise
    }
}

Com uma combinação de propriedades e funções, você pode criar classes que modelam todos os tipos de objeto.

Interoperabilidade

Uma das características mais importantes do Kotlin é a interoperabilidade fluida com Java. Como o código Kotlin é compilado até o bytecode da JVM, seu código Kotlin pode ser chamado diretamente no código Java e vice versa. Isso significa que você pode aproveitar bibliotecas Java já existentes diretamente do Kotlin. Além disso, a maioria das APIs do Android é gravada em Java, e você pode chamá-las diretamente do Kotlin.

A seguir

Kotlin é uma linguagem flexível e pragmática com compatibilidade e dinâmica crescentes. Encorajamos você a testá-la se ainda não tiver feito isso. Para as próximas etapas, confira na documentação oficial do Kotlin além do guia sobre como se inscrever padrões comuns do Kotlin nos apps Android.