Solving real life coding challenges with Kotlin’s popular features

In this blog post, we’ll dive into real life coding problems and show how Kotlin makes our lives easier as developers.

, Yuecel Mert

Overview

I often see the following comment in code reviews;

You can write it much more concisely and readably in Kotlin.

Well, indeed you can! In Kotlin, we can magically transform five lines of code into a single-liner. Besides, we no more need to think about those ugly NullPointerExceptions, because Kotlin lets compiler to flag potential null pointer dereferences systematically. Kotlin has these amazing features that makes coding super concise and easily readable.

In the upcoming sections, we’ll explore many Kotlin features, which I didn’t realize I needed. Once I began using these Kotlin features, I was convinced that they really come in handy in the development world. So buckle up – we dive into real-life coding problems and show how Kotlin makes our lives easier as developers.

Safe calls and the Elvis operator in Kotlin

When we fetch informations from an API, it is possible that some of the fields in the response may be blank or have the value “null”. Safe calls and the Elvis operator play a big role to handle such issues and make sure everything goes smoothly. Let’s say we have an API that provides us with book details. The books have a title, an author’s name, and its publication date, but sometimes the API response might be missing one or more of these fields. In the following example, we’ll see how safe calls and the Elvis operator can be used to ensure that the book information is processed properly, even if the API response is incomplete.data class Student(val name: String, val studentNumber: String) fun parseResponse(apiResponse: Map<String, String>): Student { val name = apiResponse[“name”] ?: “Unknown name” val studentNumber = apiResponse[“studentNumber”] ?: “Unknown student number” return Student(name, studentNumber) }

The function `parseResponse()` takes an API response as an input and returns a `Student` object. We can’t rely on those unreliable APIs, can we? So, we use the Elvis operator ?: for making sure that we use a fallback value like “Unknown name” or “Unknown student number”.fun main() { val response1 = mapOf(name = “Tobias”, studentNumber = “123456”) val response2 = mapOf(name = “Jane”) val student1 = parseResponse(response1) val student2 = parseResponse(response2) println(student1) println(student2) }

When we run the main function with different responses, we see that the `parseResponse()` function always gives a `Student` object, even when some of the informations are missing. This lets us work with data that isn’t full, without making mistakes or exceptions.

Extension functions

Let’s create a new function together for validating and formatting the given URLs easier. One could make an extra function for the string data type that validates URLs easier and makes sure they are always in the right format.fun String.formatURL(): String { var url = this.trim() if (!url.startsWith(“http://”) && !url.startsWith(“https://”)) { url = “http://$url” } return url }

For the `String` data type, we created an extra function called `formatURL()`. The function checks if the URL starts with `http://` or `https://`. This ensures that the style of the URL is always proper.

Let’s check together how our newly created extension function `formatURL()` from the String class validates and formats a potentially invalid URL.

For the `String` data type, we created an extra function called `formatURL()`. This function checks, if the URL starts with `http://` or `https://`. As a result, it is ensured that the style of the URL is always proper. Let’s check together, how our recently created extension function `formatURL()` from the `String` class validates and formats a potentially invalid URL.fun main() { val formattedUrl = “www.myexample.com”.formatURL() println(“Formatted URL: $formattedUrl”) }

So in this example, our new extension function formatted the given URL as follows:Formatted URL: http://www.myexample.com

The extension function helps us to add useful features to a class without changing the existing class and makes the code easier to manage.

Smart casts

For instance, we want to develop a zoo application which is capable of handling many animal types. We create an abstract class “animal” and several subclasses for animal types like `Mammals`, `Fish`, and `Birds`.abstract class Animal(val name: String) class Mammals(name: String, val fur: Boolean) : Animal(name) class Fish(name: String, val gill: Boolean) : Animal(name) class Birds(name: String, val fly: Boolean) : Animal(name)

Now, let’s create a function to simplify the sorting of the animals.fun animalInfo(animal: Animal) { when (animal) { is Mammals -> { println(“${animal.name} has fur: ${animal.fur}”) } is Fish -> { println(“${animal.name} has gills: ${animal.gill}”) } is Birds -> { println(“${animal.name} can fly: ${animal.fly}”) } } }

In this function, different animals are represented by their subclasses and also to get type-specific data without sorting to explicit casts for different animal types.fun main() { val animals1: Animal = Mammals(“Lion”, true) val animals2: Animal = Fish(“Shark”, true) val animals3: Animal = Birds(“Eagle”, true) animalInfo(animals1) animalInfo(animals2) animalInfo(animals3) }

The output of the main function would provide these results:Lion has fur: true Shark has gills: true Eagle can fly: true

Data classes

Let’s imagine we are developing an application where students sign up and create profiles. In this case, you can easily use Kotlin data classes to store and handle the user data effectively.data class Student( val id: Int, val username: String, val email: String, val birthDate: LocalDate)

By the example below, one can see that this data class is used for generating and managing the student instances.fun main() { val student1 = Student(1, “Nico”, “nico@university.com”, LocalDate.of(2000,11,3)) val student2 = Student(2, “Anna”, “anna@university.com”, LocalDate.of(1998,8,23)) val students = mutableListOf() students.add(student1) students.add(student2) val foundStudents = students.find { it.id == 1 } if (foundStudents != null) { println(“Student: $foundStudents”) } else { println(“Student with the id $studentsToFind could not be found.”) } }

In this example, two more students are added and saved in a list. Then, a student is searched, based on its ID. When the student is found, it will be listed, otherwise a proper notification will be sent.
Kotlin data classes are useful in this scenario, since they generate getter, setter, equals(), hashCode(), and toString() functions for the properties automatically, making the code cleaner and easier to read.

String templates

One can use string templates for the creation of a email template. Let’s say we need to create a university application that automatically sends candidates emails with their names and enrollment dates.fun generateEmail(studentName: String, enrollmentDate: LocalDate): String { return “Dear $studentName, your enrollment is on $enrollmentDate.” } fun main() { val email = generateEmail(“Mert Yücel”, LocalDate.of(1,9,2023)) println(email) }

The `generateEmail()` function helps us for generating an email template that adds the applicant’s name and the enrollment date automatically, by using the string templates.

Coroutines

The coroutines feature helps us for processing multiple data sources in an application asynchronously. Let’s say, we want to create an application that fetches user information from multiple APIs and combines them for the end result.
We have APIs from which we want to get user information and each API has a function for fetching the user information by their IDs.import kotlinx.coroutines.* import kotlin.random.Random data class UserInfo(val userId: Long, val name: String) object UserFirstApi { suspend fun getUserInfo(userId: Long): UserInfo { delay(Random.nextLong(1000)) return UserInfo(userId, “User from first API”) } } object UserSecondApi { suspend fun getUserInfo(userId: Long): UserInfo { delay(Random.nextLong(1000)) return UserInfo(userId, “User from second API”) } }

In this code block we added delay() function for simulating a network slowness to represent our case properly.
Now we can create a function for fetching the user information from many APIs at once.suspend fun getUserInformation(userId: Int): UserInfo = coroutineScope { val deferredUserApi1 = async { UserFirstApi.getUserInfo(userId) } val deferredUserApi2 = async { UserSecondApi.getUserInfo(userId) } val userInfoApi1 = deferredUserApi1.await() val userInfoApi2 = deferredUserApi2.await() userInfoApi1 }

Let’s create now the main function to start coroutine.fun main() { runBlocking { val userIds = listOf(1, 2, 3) val userInfos = userIds.map { userId -> async { getUserInformation(userId) } } val results = userInfos.awaitAll() results.forEach { userInfo -> println( “User ID: ${userInfo.userId}, Name: ${userInfo.name}}”) } }

In the above scenario, coroutines feature helped us to fetch the user informations from three different APIs and every API request is processed at the same time using the async function. One waits for all results with awaitAll() and put them all together in the `fetchUserInfo()` function.

Higher-order functions

Now, we can check together the case of having an employee list with certain properties like job, age, name, etc. We also want to create a filter function that returns us a subset of employees who meet specific employees above a certain age and with a specific profession.
We can now create an employee data class and filter user method.data class Employee(val name: String, val age: Int, val profession: String) fun filterEmployees(employees: List, condition: (Employee) -> Boolean): List { return employees.filter { condition(it) } }

Now we can create a list of employees to filter. The method `filterEmployees()` is a higher-order function, since it takes another function as an argument. The lambda function `(Employee) -> Boolean` tests whether a user meets the conditions. The higher-order method `filterEmployees()` adds all users who fulfill the requirements for the list. Finally, we publish `filteredEmployees`, a list of specific users who are engineers and above 35.fun main() { val employees = listOf( Employee(“Max”, 30, “Engineer”), Employee(“Jenny”, 40, “Engineer”) ) val filteredEmployees = filterEmployees(employees) { user -> user.age > 35 && user.profession == “Engineer” } println(“Filtered employees: $filteredEmployees”) }

By these examples, we saw that the extension functions and function-type parameters play a very important role. It makes our code cleaner and more readable and if you want to learn more about higher-order functions, you can also check my dear colleague Tioman Gally’s blog post.

Companion Objects

A great example for the companion objects is a helper class that has functions to convert temperature values between Dollar and Euro. Let’s create a Currency Converter class which has a companion object.class CurrencyConverter { companion object { fun convertDollarToEuro(dollar: Double, exchangeRate: Double): Double { return dollar * exchangeRate } } }

In this CurrencyConverter we have static functions, which perform the corresponding conversions. You can call these functions in another classes without creating an instance of CurrencyConverter, because they are part of the companion objects.class PrintInformations { fun printDollarToEuro(dollar: Double, exchangeRate: Double) { val euro = CurrencyConverter.convertDollarToEuro(dollar, exchangeRate) println(“$dollar degrees Dollars are equal to $euro Euros.”) } }

To sum up companion objects keeps the code tidy and allows us to group the similar functions together and enable global access to those functions without having to build a class instance.

Conclusion

By using several examples, I tried to show you, how we benefit from Kotlin functions. I can only recommend looking at the source code of the Kotlin standard library, since you can always discover and learn something new there. Extension functions, higher-order functions, null-safety, and other sophisticated features make the code more legible and manageable. It’s commonly known that Kotlin’s expressive language lets developers concentrate on application functionality, rather than boilerplate code. All the features above indicate clearly, how Kotlin helps developers effectively for solving real-life coding challenges and enhance their productivity simultaneously.

General inquiries

We look forward to tackling your challenges together and discussing suitable solutions. Contact us - and get tailored solutions for your business. We look forward to your contact request!

Contact Us