Kotlin Collections: Getting Started

In this tutorial, you’ll learn how to work with Kotlin Collections. You’ll transform data, filter it out, and use different types of collections in Kotlin! By Filip Babić.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Concrete Collection Types

Kotlin collections have to be interoperable with Java. Because of this, they revolve around the usage of the same concrete types when it comes to each collection type. For example, maps can be HashMap or a LinkedHashMap.

Concrete types are a huge topic and one tutorial won’t do them justice. It’s better to check out this link, which briefly explains the various types. Alternatively, check out the official documentation for Java collection types, starting with the List.

Collection Operators

The best feature Kotlin collections offers is the ability to transform between Kotlin collection types using collection operators. You can use them to:

  • Transform from a list to a set.
  • Turn a list of strings to a list of integers.
  • Group the data according to certain conditions.

You’ll learn several usages of the operators including:

  • filtering elements.
  • looking up data.
  • grouping data.
  • transforming the elements.
  • validating the element.s

Open Operations.kt and add the following code:

data class Product(
    val id: Int,
    val name: String,
    val price: Double
)

class Receipt(
    val id: Int,
    val seller: Worker,
    val products: List<Product>,
    val isPaid: Boolean = false
)

class Store(
    val receipts: List<Receipt>,
    val workers:List<Worker>
)

data class Worker(
    val id: Int,
    val name: String
)

fun beer() = Product(id = 2, name = "Beer, light, 0.5l", price = 7.5)
fun coffee() = Product(id = 3, name = "Ground coffee 1kg", price = 5.0)
fun bread() = Product(id = 1, name = "Gluten-free bread, 1kg", price = 5.0)

fun main() {
    val firstWorker = Worker(id = 1, name = "Filip")
    val secondWorker = Worker(id = 2, name = "Chris")

    val store = Store(
        // 1
        receipts = listOf(
            Receipt(
                //2
                id = 1,
                seller = firstWorker,
                products = listOf(bread(), bread(), bread(), coffee(), beer()),
                isPaid = true
            ),

            Receipt(
                id = 2,
                seller = secondWorker,
                products = listOf(coffee(), coffee(), beer(), beer(), beer(), beer(), beer()),
                isPaid = false
            ),

            Receipt(
                id = 3,
                seller = secondWorker,
                products = listOf(beer(), beer(), bread()),
                isPaid = false
            )
        ),
        // 3
        workers = listOf(firstWorker, secondWorker)
    )
}

Here’s a play-by-play of what’s happening above:

  1. First, you create a store with a list of receipts from purchases.
  2. Each receipt has a list of products, whether it’s paid or not, an id and the worker which handled the receipt.
  3. You then add a list of workers to the store, because someone has to sell the products.

You can transform the data to create something more meaningful than this huge construct.

Transforming Data

Transforming is the process of taking one type and converting it to another. For example, you can transform a String into an Int by calling string.length on it.

In your store example, you need to get all the products you sold. Add the following code to main():

val receipts = store.receipts // fetch the receipts
val productsLists = receipts.map { it.products } // List<List<Product>>
println(productsLists) 

By using the map operator, you can transform all the receipts collection elements to their respective products. Map iterates over each element of type T and passes it into a provided lambda function. The lambda function returns an element of type R, which in this case, turns each receipt item into a product. What you get back is a List of List of Products, which is fine, but clunky to work with.

You can improve this by using the flatMap operator instead:

val receipts = store.receipts // fetch the receipts
val allProducts = receipts.flatMap { it.products } // List<Product>
println(allProducts)

Flat mapping not only means transforming the elements (a la map) but also flattens them into a single list which is easier to use.

But what if you need all the prices of the products or their sum?

val receipts = store.receipts // fetch the receipts
val allProductsEarnings = receipts.flatMap { it.products }
      .map { it.price }
      .sumByDouble { it }
println(allProductsEarnings)

You can chain Kotlin collection operators and create really clean and readable transformation operations.

Here, you first flattened and mapped the receipts to products. Then you mapped it to the price, which you ultimately summed. You can also use sumByDouble { it.price } in place of the map saving you one precious line of code.

Pretty neat, huh? :]

Filtering and Grouping Data

Each store has to keep a record of all the paid and unpaid receipts. You can do this, using filtering and grouping operators, by adding this code to main():

// filtering by condition
val paidReceipts = receipts.filter { it.isPaid }
println(paidReceipts)

// grouping values by condition
val paidUnpaid = receipts.partition { it.isPaid }
val (paid, unpaid) = paidUnpaid
println(paid)
println(unpaid)

val groupedByWorker = receipts.groupBy { it.seller } // Map<Worker, List<Receipt>>
println(groupedByWorker)

You can filter out one partition of the results by using filter() and providing a lambda to filter by. The lambda takes each element T and uses the expression in the curly braces to validate if it should be collected.

partition(), however, takes the same lambda but returns a pair of elements that match the predicate and elements that don’t. Finally, you can use groupBy() and provide a value matcher in the lambda, which it will use to compare to other elements of a list and group the ones which match the value with hashCode().

Here you used a Worker to group the values and, in the end, you receive a map holding the list of all the receipts for each of the workers.

Validating Data

Sometime you don’t need to filter data but instead need to check if the data conforms to a condition. For example, you might want to check if all the receipts are paid. You can do that as well:

  val areThereNoReceipts = receipts.isEmpty() // also isNotEmpty
  val areAllPaid = receipts.all { it.isPaid }
  val nonePaid = receipts.none { it.isPaid }
  val isAtLeastOnePaid = receipts.any { it.isPaid }

The all(), none() and other functions allow you to return a boolean to see if any, all or none of the elements match the given condition.