Coleções no Kotlin

1. Antes de começar

Neste codelab, você vai aprender sobre coleções e lambdas e também sobre funções de ordem superior no Kotlin.

Pré-requisitos

  • Conhecimento básico dos conceitos do Kotlin, conforme apresentado nos codelabs anteriores.
  • Ter familiaridade com o Playground Kotlin para criar e editar programas Kotlin.

O que você aprenderá

  • Como trabalhar com coleções, incluindo conjuntos e mapas.
  • Noções básicas sobre lambdas.
  • Noções básicas sobre funções de ordem superior.

O que é necessário

2. Saiba mais sobre coleções

Uma coleção (link em inglês) é um grupo de itens relacionados, como uma lista de palavras ou um conjunto de registros de funcionários. A coleção pode ter os itens ordenados ou não, e eles podem ser únicos ou não. Você já aprendeu sobre um tipo de coleção, as listas. Listas têm uma ordem para os itens, mas eles não precisam ser únicos.

Assim como nas listas, o Kotlin distingue coleções mutáveis e imutáveis. O Kotlin oferece várias funções para adicionar ou excluir itens, visualizar e manipular coleções.

Criar uma lista

Nesta tarefa, você verá como criar uma lista de números e classificá-los.

  1. Abra o Playground Kotlin.
  2. Substitua o código por este:
fun main() {
    val numbers = listOf(0, 3, 8, 4, 0, 5, 5, 8, 9, 2)
    println("list:   ${numbers}")
}
  1. Para executar o programa, toque na seta verde e veja os resultados exibidos:
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
  1. A lista contém 10 números de 0 a 9. Alguns dos números são exibidos mais de uma vez, enquanto outros não são.
  2. A ordem dos itens na lista é importante: o primeiro item é 0, o segundo item é 3 e assim por diante. Os itens permanecerão nessa ordem, a menos que você a mude.
  3. Lembre-se dos codelabs anteriores em que listas têm muitas funções integradas, como sorted(), para retornar uma cópia da lista em ordem crescente. Depois do método println(), adicione uma linha ao programa para exibir uma cópia ordenada da lista:
println("sorted: ${numbers.sorted()}")
  1. Execute o programa novamente e analise os resultados:
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
sorted: [0, 0, 2, 3, 4, 5, 5, 8, 8, 9]

Com os números ordenados, é mais fácil ver quantas vezes cada um deles aparece na lista ou se não aparecem.

Saiba mais sobre conjuntos

Outro tipo de coleção no Kotlin é um conjunto (link em inglês). Trata-se de um grupo de itens relacionados, mas, ao contrário de uma lista, não pode haver itens duplicados e a ordem não importa. Um item pode estar no conjunto ou não, mas se estiver no conjunto, haverá apenas uma cópia dele. Isso é semelhante ao conceito matemático de um conjunto. Por exemplo, há um conjunto de livros que você leu. Ler um livro várias vezes não muda o fato dele estar no conjunto de livros que você leu.

  1. Adicione estas linhas ao seu programa para converter a lista em um conjunto:
val setOfNumbers = numbers.toSet()
println("set:    ${setOfNumbers}")
  1. Execute o programa e veja os resultados:
list:   [0, 3, 8, 4, 0, 5, 5, 8, 9, 2]
sorted: [0, 0, 2, 3, 4, 5, 5, 8, 8, 9]
set:    [0, 3, 8, 4, 5, 9, 2]

O resultado tem todos os números da lista original, mas cada um aparece apenas uma vez. Observe que eles estão na mesma ordem que a lista original, mas essa ordem não importa para um conjunto.

  1. Defina um conjunto mutável e um conjunto imutável e inicialize-os com o mesmo conjunto de números em uma ordem diferente, adicionando estas linhas:
val set1 = setOf(1,2,3)
val set2 = mutableSetOf(3,2,1)
  1. Adicione uma linha para exibir os conjuntos se eles forem iguais:
println("$set1 == $set2: ${set1 == set2}")
  1. Execute o programa e veja os novos resultados:
[1, 2, 3] == [3, 2, 1]: true

Embora um dos conjuntos seja mutável e o outro não, e os conjuntos tenham os itens em uma ordem diferente, eles são considerados iguais porque contêm exatamente o mesmo conjunto de itens.

Uma das operações principais que podem ser realizadas em um conjunto é verificar se um determinado item está no conjunto ou não usando a função contains() (link em inglês). Você já viu a função contains() anteriormente, mas usada em uma lista.

  1. Adicione esta linha ao programa para exibir se o número 7 está no conjunto:
println("contains 7: ${setOfNumbers.contains(7)}")
  1. Execute o programa e veja os novos resultados:
contains 7: false

Também é possível testá-lo com um valor que esteja no conjunto.

Todo o código acima:

fun main() {
    val numbers = listOf(0, 3, 8, 4, 0, 5, 5, 8, 9, 2)
    println("list:   ${numbers}")
    println("sorted: ${numbers.sorted()}")
    val setOfNumbers = numbers.toSet()
    println("set:    ${setOfNumbers}")
    val set1 = setOf(1,2,3)
    val set2 = mutableSetOf(3,2,1)
    println("$set1 == $set2: ${set1 == set2}")
    println("contains 7: ${setOfNumbers.contains(7)}")
}

Assim como acontece com conjuntos matemáticos, no Kotlin, você também pode realizar operações como a interseção (∩) ou a união (∪) de dois conjuntos, usando as funções intersect() ou union() (links em inglês).

Saiba mais sobre mapas

O último tipo de coleção que você vai conhecer neste codelab é um mapa (link em inglês) ou dicionário. Um mapa é um conjunto de pares de chave-valor projetado para facilitar a busca de um valor específico usando uma chave específica. As chaves são exclusivas e cada uma mapeia exatamente um valor, mas os valores podem ser duplicados. Os valores em um mapa podem ser strings, números ou objetos, ou até mesmo outra coleção, como uma lista ou um conjunto.

b55b9042a75c56c0.png

O mapa é útil quando existem pares de dados e é possível identificar cada par de acordo com a chave. A chave "mapeia para" o valor correspondente.

  1. No Playground Kotlin, substitua todo o código existente por este código que cria um mapa mutável para armazenar nomes e idades de pessoas:
fun main() {
    val peopleAges = mutableMapOf<String, Int>(
        "Fred" to 30,
        "Ann" to 23
    )
    println(peopleAges)
}

Isso cria um mapa mutável de uma String (chave) para um Int (valor), inicializa o mapa com duas entradas e exibe os itens.

  1. Execute o programa e veja os resultados:
{Fred=30, Ann=23}
  1. Para adicionar mais entradas ao mapa, use a função put() (link em inglês), transmitindo a chave e o valor:
peopleAges.put("Barbara", 42)
  1. Também é possível usar uma notação abreviada para adicionar entradas:
peopleAges["Joe"] = 51

Veja os códigos acima usados em conjunto a seguir:

fun main() {
    val peopleAges = mutableMapOf<String, Int>(
        "Fred" to 30,
        "Ann" to 23
    )
    peopleAges.put("Barbara", 42)
    peopleAges["Joe"] = 51
    println(peopleAges)
}
  1. Execute o programa e veja os resultados:
{Fred=30, Ann=23, Barbara=42, Joe=51}

Como indicado acima, as chaves (nomes) são exclusivas, mas os valores (idade) podem ser duplicados. O que você acha que acontecerá quando tentar adicionar um item usando uma chave duplicada?

  1. Antes do método println(), adicione esta linha de código:
peopleAges["Fred"] = 31
  1. Execute o programa e veja os resultados:
{Fred=31, Ann=23, Barbara=42, Joe=51}

A chave "Fred" não será adicionada novamente, mas o valor que ela mapeia será atualizado para 31.

Como você pode ver, os mapas são úteis para mapear rapidamente as chaves do código.

3. Como trabalhar com coleções

Embora tenham qualidades variadas, os diferentes tipos de coleção têm muitos comportamentos em comum. Se forem mutáveis, você poderá adicionar ou remover itens. É possível enumerar todos os itens, encontrar um item específico ou, às vezes, converter um tipo de coleção em outro. Você fez isso anteriormente quando converteu uma List em um Set usando a função toSet() (link em inglês). Veja algumas funções úteis para trabalhar com coleções.

forEach

Vamos supor que você quer exibir os itens de peopleAges e incluir o nome e a idade da pessoa. Por exemplo, "Fred is 31, Ann is 23,...", e assim por diante. Você aprendeu sobre repetições for em um codelab anterior, então poderia escrever uma repetição usando for (people in peopleAges) { ... }.

No entanto, a enumeração de todos os objetos em uma coleção é uma operação comum, por isso o Kotlin fornece a função forEach() (link em inglês), que realiza operações em cada um dos itens de uma coleção para você.

  1. No Playground, adicione este código após o método println():
peopleAges.forEach { print("${it.key} is ${it.value}, ") }

Esse código é semelhante à repetição for, mas um pouco mais compacto. Em vez de você especificar uma variável para o item atual, a função forEach usa o identificador especial it.

Não é necessário adicionar parênteses ao chamar o método forEach(), basta transmitir o código entre chaves {}.

  1. Execute o programa e veja os novos resultados:
Fred is 31, Ann is 23, Barbara is 42, Joe is 51,

Muito parecido com o que você quer, mas há uma vírgula a mais no final.

Converter uma coleção em uma string é uma operação comum, e o separador extra no final também é um problema comum. Você aprenderá a lidar com isso nas próximas etapas.

Função map

A função map() (link em inglês), que é diferente de uma coleção de mapa ou de dicionário descrita acima, aplica uma transformação a cada item de uma coleção.

  1. No seu programa, substitua a instrução forEach pela seguinte linha:
println(peopleAges.map { "${it.key} is ${it.value}" }.joinToString(", ") )
  1. Execute o programa e veja os novos resultados:
Fred is 31, Ann is 23, Barbara is 42, Joe is 51

A saída será correta e não haverá vírgula extra. Há muitas coisas acontecendo em uma linha, veja mais detalhes.

  • A função peopleAges.map aplica uma transformação em cada item em peopleAges e cria uma nova coleção dos itens transformados
  • A parte {} entre chaves define a transformação a ser aplicada em cada item. A transformação usa um par de chave-valor em uma string, por exemplo, <Fred, 31> se transforma em Fred is 31.
  • A função joinToString(", ") adiciona cada item na coleção transformada a uma string, separada por uma , e sabe que não precisa adicioná-la ao último item.
  • Tudo isso é unido com um . (operador de ponto), como você fez com as chamadas de função e acessos de propriedade em codelabs anteriores.

Função filter

Outra operação comum em coleções é encontrar os itens que correspondem a uma condição específica. A função filter() (link em inglês) retorna os itens de uma coleção que correspondem a uma expressão.

  1. Depois do método println(), adicione estas linhas:
val filteredNames = peopleAges.filter { it.key.length < 4 }
println(filteredNames)

A chamada de função filter não precisa de parênteses e it refere-se ao item atual na lista.

  1. Execute o programa e veja os novos resultados:
{Ann=23, Joe=51}

Nesse caso, a expressão recebe o comprimento da chave (uma String) e verifica se ela é menor que 4. Todos os itens correspondentes, ou seja, os que tiverem um nome com menos de 4 caracteres, serão adicionados à nova coleção.

O tipo retornado quando você aplica o filtro a um mapa é um novo mapa (LinkedHashMap). É possível realizar mais processamentos no mapa ou convertê-lo em outro tipo de coleção, como uma lista.

4. Saiba mais sobre lambdas e funções de ordem superior

Lambdas

Vamos rever esse exemplo anterior:

peopleAges.forEach { print("${it.key} is ${it.value}") }

Há uma variável (peopleAges) com uma função (forEach) sendo chamada nela. Em vez dos parênteses após o nome da função com os parâmetros, você vai ver um código entre chaves {} após o nome da função. O mesmo padrão aparece no código que usa as funções map e filter da etapa anterior. A função forEach é chamada na variável peopleAges e usa o código entre chaves.

É como se você tivesse escrito uma pequena função nas chaves, mas ela não tem um nome. Essa ideia, uma função sem nome que pode ser usada imediatamente como uma expressão, é um conceito muito útil conhecido como expressão lambda, ou apenas lambda.

Essas expressões são parte de um tópico importante de como você pode interagir com funções de maneira eficiente usando o Kotlin. É possível armazenar funções em variáveis e classes, transmitir funções como argumentos e até mesmo retornar funções. Você pode tratá-las como variáveis de outros tipos, como Int ou String.

Tipos de função

Para ativar esse tipo de comportamento, o Kotlin oferece tipos de função em que é possível definir um tipo específico de função com base nos parâmetros de entrada e no valor de retorno. Os tipos têm este formato:

Exemplo de tipo de função: (Int) -> Int

Uma função com o tipo acima precisa ter um parâmetro do tipo Int e retornar um valor do tipo Int. Na notação do tipo de função, os parâmetros são listados entre parênteses (separados por vírgulas se houver vários parâmetros). Depois, há uma seta -> seguida pelo tipo de retorno.

Que tipo de função atenderia a este critério? É possível usar uma expressão lambda que triplica o valor de uma entrada de número inteiro, como pode ser visto no exemplo abaixo. Para a sintaxe de uma expressão lambda, os parâmetros vêm primeiro (destacados na caixa vermelha), seguidos pela seta da função, depois pelo corpo da função (destacado na caixa roxa). A última expressão na lambda é o valor de retorno.

252712172e539fe2.png

É possível até mesmo armazenar um lambda em uma variável, como mostrado no diagrama abaixo. A sintaxe é semelhante à declaração de uma variável de um tipo de dados básico, como um Int. Veja o nome da variável (na caixa amarela), o tipo de variável (na caixa azul) e o valor da variável (na caixa verde). A variável triple armazena uma função. O tipo dela é um tipo de função (Int) -> Int e o valor é uma expressão lambda { a: Int -> a * 3}.

  1. Experimente este código no Playground. Defina e chame a função triple transmitindo um número, como 5. 4d3f2be4f253af50.png
fun main() {
    val triple: (Int) -> Int = { a: Int -> a * 3 }
    println(triple(5))
}
  1. A saída resultante será:
15
  1. Dentro das chaves, você pode omitir explicitamente o parâmetro (a: Int), omitir a seta da função (->) e usar apenas o corpo da função. Atualize a função triple declarada na sua função main e execute o código.
val triple: (Int) -> Int = { it * 3 }
  1. A saída precisa ser a mesma, mas agora a lambda foi programada de forma mais concisa. Para ver mais exemplos de lambdas, confira este recurso (link em inglês).
15

Funções de ordem superior

Agora que você está começando a entender a flexibilidade de como pode manipular funções no Kotlin, vamos falar sobre outro conceito muito eficiente, uma função de ordem superior. Isso significa apenas transmitir uma função (nesse caso, uma lambda) para outra ou retornar uma função de outra.

As funções map, filter e forEach são exemplos de funções de ordem superior, porque elas recebem uma função como parâmetro. (Na lambda transmitida a essa função de ordem superior filter, não há problema em omitir o único parâmetro e o símbolo de seta, além de usar o parâmetro it)

peopleAges.filter { it.key.length < 4 }

Veja um exemplo de uma nova função de ordem superior: sortedWith().

Se você quiser ordenar uma lista de strings, use o método integrado sorted() para coleções. No entanto, se você quiser ordenar a lista pelo comprimento das strings, precisará escrever o código para analisar o comprimento de duas strings e compará-los. O Kotlin permite fazer isso transmitindo uma lambda ao método sortedWith().

  1. No Playground, crie uma lista com nomes. Mostre a lista ordenada por nome e mostre-a usando este código:
fun main() {
    val peopleNames = listOf("Fred", "Ann", "Barbara", "Joe")
    println(peopleNames.sorted())
}
  1. Agora, exiba a lista ordenada pelo comprimento dos nomes transmitindo uma lambda para a função sortedWith(). A lambda precisa receber dois parâmetros do mesmo tipo e retornar um Int. Adicione esta linha de código após a instrução println() na função main().
println(peopleNames.sortedWith { str1: String, str2: String -> str1.length - str2.length })
  1. Execute o programa e veja os resultados.
[Ann, Barbara, Fred, Joe]
[Ann, Joe, Fred, Barbara]

A lambda transmitida ao método sortedWith() tem dois parâmetros, str1, que é uma String, e str2, que também é uma String. Em seguida, você verá a seta de função, seguida pelo corpo da função.

7005f5b6bc466894.png

Lembre-se de que a última expressão na lambda é o valor de retorno. Nesse caso, ela retorna a diferença entre o comprimento da primeira string e o comprimento da segunda, que é um Int. Isso corresponde ao que é necessário para a ordenação: se a str1 for menor que a str2, a lambda retornará um valor menor que 0. Se str1 e str2 tiverem o mesmo tamanho, a lambda retornará 0. Se str1 for maior que str2, a lambda retornará um valor maior que 0. Ao fazer uma série de comparações entre duas Strings por vez, a função sortedWith() gerará uma lista em que os nomes serão ordenados em ordem crescente.

OnClickListener e OnKeyListener no Android

Vamos unir isso a tudo o que aprendemos sobre o Android até agora. Você já usou lambdas em codelabs anteriores, como ao definir um listener de clique para o botão no app Tip Calculator:

calculateButton.setOnClickListener{ calculateTip() }

Usar uma lambda para definir o listener de clique é uma abreviação conveniente. A forma longa de escrever o código acima é mostrada abaixo e comparada com a versão abreviada. Não é necessário entender todos os detalhes da versão longa do código, mas observe alguns padrões entre as duas versões.

29760e0a3cac26a2.png

A lambda tem o mesmo tipo de função que o método onClick() no OnClickListener (recebe um argumento View e retorna Unit, o que significa que não há valor de retorno).

A versão abreviada do código é possível por causa de uma conversão conhecida como SAM (Single-Abstract-Method) do Kotlin. O Kotlin converte a lambda em um objeto OnClickListener que implementa o método abstrato onClick() único. Você só precisa verificar se o tipo de função lambda corresponde ao tipo da função abstrata.

Como o parâmetro view nunca é usado na lambda, ele pode ser omitido. Temos o corpo da função na lambda.

calculateButton.setOnClickListener { calculateTip() }

Esses conceitos são desafiadores, portanto, tenha paciência, porque você precisará de um tempo e experiência com eles até se acostumar com eles. Vejamos outro exemplo. Lembre-se de quando você definiu um listener de teclas no campo de texto do "Custo de serviço" na calculadora de gorjetas para ocultar o teclado virtual quando a tecla Enter fosse pressionada.

costOfServiceEditText.setOnKeyListener { view, keyCode, event -> handleKeyEvent(view, keyCode) }

Quando você procura OnKeyListener, o método abstrato tem os seguintes parâmetros onKey(View v, int keyCode, KeyEvent event) e retorna um Boolean. Devido às conversões do SAM no Kotlin, você pode transmitir uma lambda para o setOnKeyListener(). Verifique se a lambda tem o tipo de função (View, Int, KeyEvent) -> Boolean.

Veja um diagrama da expressão lambda usada acima. Os parâmetros são "view", "keyCode" e "event". O corpo da função consiste em handleKeyEvent(view, keyCode), que usa os parâmetros transmitidos e retorna um Boolean.

f73fe767b8950123.png

5. Criar listas de palavras

Agora, vamos usar tudo o que você aprendeu sobre coleções, lambdas e funções de ordem superior e aplicar a um caso de uso realista.

Vamos supor que você quer criar um app Android para um jogo de palavras ou aprender palavras do vocabulário. O app pode ser parecido com este, com um botão para cada letra do alfabeto:

7539df92789fad47.png

Ao clicar na letra A, o app abre uma lista curta com algumas palavras que começam com essa letra, e assim por diante.

Você precisará de uma coleção de palavras, mas que tipo de coleção? Se o app incluir algumas palavras que começam com cada letra do alfabeto, você precisará encontrar ou organizar todas as palavras que comecem com uma letra específica. Para dificultar o desafio, escolha palavras diferentes da coleção sempre que o usuário executar o app.

Primeiro, comece com uma lista de palavras. Para um app real, seria recomendado usar uma lista de palavras mais longa e incluir palavras que comecem com todas as letras do alfabeto, mas uma lista curta é suficiente para este exemplo no momento.

  1. Substitua o código no Playground Kotlin por este:
fun main() {
    val words = listOf("about", "acute", "awesome", "balloon", "best", "brief", "class", "coffee", "creative")
}
  1. Para acessar uma coleção das palavras que começam com a letra B, você pode usar filter com uma expressão lambda. Adicione estas linhas:
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
println(filteredWords)

A função startsWith() (link em inglês) retornará como verdadeira se uma string começar com a string especificada. Você também pode dizer que a instrução ignora maiúsculas e minúsculas para que "b" corresponda a "b" ou "B".

  1. Execute o app e veja o resultado.
[balloon, best, brief]
  1. As palavras precisam ser aleatórias para o app. Com as coleções do Kotlin, é possível usar a função shuffled() (link em inglês) para fazer uma cópia de uma coleção com os itens escolhidos aleatoriamente. Mude também as palavras filtradas para serem aleatórias:
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
  1. Execute o programa e veja os novos resultados:
[brief, balloon, best]

Como as palavras são escolhidas aleatoriamente, você poderá vê-las em uma ordem diferente.

  1. É recomendado não usar todas as palavras (principalmente se a lista de palavras for muito longa). É possível usar a função take() (link em inglês) para acessar apenas os primeiros itens da coleção. Faça com que as palavras filtradas incluam as duas primeiras palavras aleatórias:
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
    .take(2)
  1. Execute o programa e veja os novos resultados:
[brief, balloon]

Novamente, por causa da escolha aleatória, talvez você veja palavras diferentes sempre que executar o app.

  1. Por fim, para o app, você quer uma lista aleatória de palavras para cada letra ordenada. Como antes, use a função sorted() (link em inglês) para retornar uma cópia da coleção com os itens ordenados:
val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
    .shuffled()
    .take(2)
    .sorted()
  1. Execute o programa e veja os novos resultados:
[balloon, brief]

Veja os códigos acima como um todo:

fun main() {
    val words = listOf("about", "acute", "awesome", "balloon", "best", "brief", "class", "coffee", "creative")
    val filteredWords = words.filter { it.startsWith("b", ignoreCase = true) }
        .shuffled()
        .take(2)
        .sorted()
    println(filteredWords)
}
  1. Tente mudar o código para criar uma lista de uma palavra aleatória que comece com a letra "C". O que você precisa mudar no código acima?
val filteredWords = words.filter { it.startsWith("c", ignoreCase = true) }
    .shuffled()
    .take(1)

No app real, será preciso aplicar o filtro a cada letra do alfabeto, mas agora você sabe como gerar a lista de palavras para cada letra.

As coleções são eficientes e flexíveis. Há várias coisas que elas podem fazer, e pode haver mais de uma maneira de fazer algo. Quanto mais você aprende sobre a programação, verá como descobrir qual é o tipo de coleção adequado para o problema em questão e as melhores maneiras de processá-lo.

Lambdas e funções de ordem superior tornam o trabalho com coleções mais fácil e conciso. Como esses conceitos são muito úteis, você os verá sendo usados várias vezes.

6. Resumo

  • Uma coleção é um grupo de itens relacionados.
  • As coleções podem ser mutáveis ou imutáveis.
  • As coleções podem ser ordenadas ou não.
  • As coleções podem exigir itens exclusivos ou permitir cópias.
  • O Kotlin é compatível com diferentes tipos de coleções, incluindo listas, conjuntos e mapas.
  • O Kotlin oferece muitas funções para processar e transformar coleções, incluindo forEach, map, filter, sorted, entre outras.
  • Uma lambda é uma função sem um nome que pode ser transmitida como uma expressão imediatamente. Por exemplo, { a: Int -> a * 3 }.
  • Uma função de ordem superior significa transmitir uma função para outra ou retornar uma função de outra.

7. Saiba mais