You have certainly come across the term library several times in your coding journey with Kotlin (or even another programming language). But what options are available to you in this realm of libraries? And how should you use them in a proper way without seeing any linting error screaming at your face, or even worse—introducing bugs into your code?
In this topic, you will get answers to all those questions, and you will be able to navigate your way in the uncharted territories of the vast landscape of libraries without feeling lost. Let's get started on your journey to becoming a true librarian.
Understanding libraries
We've discussed what libraries are from a general point of view in a separate topic, but a small reminder never hurts. So, in the case of Kotlin, we can say that libraries are used to extend the language's capabilities and simplify the coding process. They provide pre-built functionalities like functions, classes, and methods, which can be invoked or used in your code, saving you the time and effort of writing these functionalities from scratch. For example, the Kotlin standard library provides functions to handle complex math calculations like the sine and cosine of an angle.
Similar to other languages' libraries, Kotlin libraries also can be categorized into two types: standard libraries and third-party libraries. We will explore each type later and see what features they provide.
Kotlin offers many advantages when you use its libraries. Here are some of the main ones:
- Efficiency. Libraries provide pre-written code that can be used to perform common tasks, saving developers a significant amount of time and effort.
- Quality. Libraries often undergo rigorous testing and quality assurance processes, which means the code they provide is generally reliable and bug-free.
- Simplicity. Libraries abstract away complex tasks, providing simple interfaces that developers can use without needing to understand the underlying complexity.
- Community support. Popular libraries often have a large community of users who can provide support and assistance. This can be particularly helpful when you encounter problems or need help understanding how to use the library.
Let's move now to real examples and explore the amazing world of Kotlin libraries.
Kotlin standard libraries
Some of the recurring things you need to do in your daily life of coding are operations on collections or strings. Imagine every time you need to split a string into multiple sub-strings using a specified delimiter, you have to write that extension function from scratch (maybe you will give it a fancier name, but I think we agree that's not what will make your coding life easier). What if this extension function already exists and is easy to reach (like a snap of fingers) and well-written so it can encompass many possible splitting operations, wouldn't that be great? Well, you are in luck because that's what Kotlin standard libraries exist for.
Kotlin standard libraries are a fundamental part of the Kotlin programming language (all you need is a snap! Well... mostly). They are maintained by JetBrains (the company behind Kotlin). The standard libraries are included with the Kotlin language and provide essential functionality for working with the language. These libraries are designed to work seamlessly with Kotlin's language features, offering developers a robust set of tools to handle various programming tasks efficiently and effectively. They include functions for handling collections, strings, math calculations, and more.
Let's explore some of the main features and functionalities that Kotlin standard libraries offer:
1) Collection functions. The standard libraries provide a comprehensive set of functions for working with collections such as lists, sets, and maps. These include functions like:
map: transforms the collection based on a given function.filter: filters the collection based on a given condition.reduce: combines the elements of the collection in a sequential manner using a specified function.
To use these functions in your code, you just have to call them right away (remember: snap of fingers), and here is an example:
val numbers = listOf(1, 2, 3, 4, 5)
val squaredNumbers = numbers.map { it * it } // [1, 4, 9, 16, 25]
val evenNumbers = numbers.filter { it % 2 == 0 } // [2, 4]
val product = numbers.reduce { acc, i -> acc * i } // 120
2) String functions. The standard libraries offer a variety of functions to manipulate and process strings. These include functions like:
split: splits the string into a list of strings based on a specified delimiter.substring: returns a subsequence of the string based on the specified range.
Here is an example of how to use them:
val str = "Hello, Kotlin!"
val words = str.split(", ") // ["Hello", "Kotlin!"]
val substring = str.substring(0, 5) // "Hello"
3) Math functions. The standard libraries also include various math functions like:
max: returns the maximum of two numbers.min: returns the minimum of two numbers.abs: returns the absolute value of a number.
Because these functions are declared by the same name in other standard libraries, we must import them explicitly on top of our code:
import kotlin.math.max
import kotlin.math.min
import kotlin.math.abs
Alternatively, we can use the * symbol to import all functions in the same package in one line:
import kotlin.math.*
The * symbol imports all the accessible contents of the kotlin.math package into the current file. This means that you can use all the functions and properties from the kotlin.math package without having to qualify them with the package name.
Here is a complete example:
import kotlin.math.*
val max = max(10, 20) // 20
val min = min(10, 20) // 10
val abs = abs(-10) // 10
4) Random methods. The standard libraries provide a suite of methods for generating random numbers through the kotlin.random.Random class.
Let's consider a use case where you want to generate a random number within a range, say, for a dice roll in a game:
import kotlin.random.Random
fun main() {
val diceRoll = Random.nextInt(1, 7)
println("You rolled: $diceRoll")
}
In this example, Random.nextInt(1, 7) generates a random integer between and (inclusive), simulating a dice roll.
Third-party libraries
While the Kotlin standard libraries are core libraries that are included with the language and provide essential functionalities for working with it, third-party libraries are libraries that provide specialized or advanced features that are not covered by the standard libraries. These features can range from those that simplify complex tasks, such as handling network requests (like Retrofit) or database operations (like Exposed), to those that provide completely new functionalities, such as image processing (like COIL) or machine learning capabilities (like Weka or Deeplearning4j). Third-party libraries might be developed and maintained by JetBrains themselves to provide additional functionalities outside the essential ones (like Kotlinx) or by other developers and organizations.
Let's take a look at some popular third-party libraries that can be used in Kotlin and at their key features:
1) Kotlinx libraries. The kotlinx libraries are additional libraries developed by JetBrains, which provide support for various features and functionalities, such as coroutines, serialization, and date/time handling, among others. These libraries are not included with the Kotlin language by default and must be added as dependencies to your project if you want to use them (more on that later).
kotlinx.coroutines: This is a library for managing concurrency. It introduces the concept of coroutines, which are light-weight threads, in Kotlin. This makes it easier to write asynchronous and non-blocking code.
Here is an example of how to use the runBlocking function defined in the kotlinx.coroutines library:
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L)
println("World!")
}
print("Hello, ")
}
In this example, runBlocking is a coroutine builder that blocks the main thread. Inside it, we launch a new coroutine without blocking the current thread. This coroutine waits for 1 second (without blocking, so "Hello, " will be printed first) and then prints "World!"
kotlinx.serialization: This library converts objects to strings or other formats (serialization) and back (deserialization). It supports various serialization formats, including JSON, CBOR, and Protobuf.
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class Person(val name: String, val age: Int)
fun main() {
val person = Person("Omar", 20)
val jsonString = Json.encodeToString(person)
println(jsonString) // Outputs: {"name":"Omar","age":20}
val person2 = Json.decodeFromString<Person>(jsonString)
println(person2) // Outputs: Person(name=Omar, age=20)
}
In this example, we define a Person data class and use kotlinx.serialization to convert an instance of Person to a JSON string and back.
kotlinx.datetime: This library provides a more modern and intuitive API for date and time manipulation than the standard JavaDateandCalendarclasses.
import kotlinx.datetime.*
fun main() {
val currentMoment = Clock.System.now()
println(currentMoment) // Outputs the current moment in ISO-8601 format
val today = LocalDate.todayAt(TimeZone.currentSystemDefault())
println(today) // Outputs today's date
}
2) JUnit. This is a popular testing framework for Java, and it's fully compatible with Kotlin. It provides annotations to identify test methods and contains assertions for testing expected results.
Here's an example of a simple JUnit test:
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class MyTest {
@Test
fun additionTest() {
val sum = 2 + 2
assertEquals(4, sum)
}
}
In this example, we use the @Test annotation to define a test method. Inside the method, we're using the assertEquals function to assert that the sum of and is indeed .
3) Ktor. This is an official framework (built and maintained by JetBrains) for building asynchronous servers and clients in connected systems. It's highly modular, allowing you to include only the features you need.
Here is a simple example of a Ktor server that responds with "Hello, World!" to every request:
import io.ktor.application.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.engine.*
import io.ktor.server.netty.*
fun main() {
embeddedServer(Netty, port = 8080) {
routing {
get("/") {
call.respondText("Hello, World!")
}
}
}.start(wait = true)
}
In this example, the server is started on port 8080 and waits for incoming connections.
4) Retrofit. This is a type-safe HTTP client for Android and Java. It allows you to turn your HTTP API into a Kotlin interface by using annotations to describe the HTTP requests.
To use Retrofit in your code, first, define an interface for your API. Here is an example:
interface MyApi {
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String): List<Repo>
}
Then, create a Retrofit instance and use it to create an implementation of your API interface:
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/")
.build()
val api = retrofit.create(MyApi::class.java)
val repos = api.listRepos("octocat")
In this example, an API interface is defined with Retrofit annotations, and then a Retrofit instance is created to build an implementation of this API interface. This setup allows for easy and safe HTTP communication.
5) Exposed. This is a lightweight SQL library for Kotlin created by the JetBrains team. It provides a typesafe SQL interface allowing you to work with databases in a more Kotlin-idiomatic way.
Here's an example of how you might use Exposed to query a database:
Database.connect("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;", driver = "org.h2.Driver")
transaction {
SchemaUtils.create(Users)
Users.insert {
it[name] = "John"
it[age] = 27
}
Users.select { Users.age greaterEq 27 }.forEach {
println("${it[Users.name]} is ${it[Users.age]} years old")
}
}
In this example, Exposed is used to connect to a database, create a table, insert data, and then query the data.
Third-party libraries won't work in your code just by importing the right packages. Your build tool (like Gradle) needs to know where to find these libraries. This is done by specifying dependencies (and sometimes plugins) in your build configuration. Let's see next how to do that.
Integrating libraries in a project
Once you've chosen the libraries that best suit your project's needs, in order for you to use their functions and classes in your code, you need to integrate them into your project. This process involves specifying dependencies and importing packages. In this section, we will guide you through this process and provide some sample code for demonstration.
The process of integrating a library into your project typically involves the following steps:
1) Add the library as a dependency. Before you can use a library, you need to add it as a dependency to your project. This is usually done in build.gradle (or build.gradle.kts). Here's an example of how you might add a dependency for the kotlinx.coroutines library:
// using Kotlin DSL
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
}
// using Groovy DSL
dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1'
}
2) Specify the plugin (if required). Some libraries require you to specify a plugin in your build.gradle(.kts) file. This is usually mentioned in the library's documentation. Here is an example of how you might specify a plugin for the kotlinx.serialization library:
// using Kotlin DSL
plugins {
id("org.jetbrains.kotlin.plugin.serialization") version "1.7.20"
}
// using Groovy DSL
plugins {
id 'org.jetbrains.kotlin.plugin.serialization' version '1.7.20'
}
3) Sync your project. After adding the library as a dependency and specifying the plugin (if required), you need to sync your project. In IntelliJ IDEA, this can be done by clicking on the "Reload all Gradle projects" button in the Gradle tool window or by using the "Reload All from Disk" option in the File menu.
4) Import the library in your code. Once the library is added to build.gradle(.kts) and your project is synced, you can import the library in your code using the import keyword. For example:
import kotlinx.coroutines.*
5) Use the library's functions and classes. After importing the library, you can use its functions and classes in your code. For example, you can launch a coroutine using the launch function from the kotlinx.coroutines library:
fun main() = runBlocking {
launch {
// some code here
}
}
Remember, the process of integrating a library can vary slightly depending on the build system you're using (like Gradle or Maven), the IDE (like IntelliJ IDEA or Android Studio), and the library itself. Always refer to the official documentation of the library for the most accurate instructions. Here are the links for documentation of some popular libraries:
Managing library dependencies can be challenging, especially in large projects. It will be helpful to follow these tips:
- Use a dependency management tool. Tools like Gradle or Maven can automatically handle downloading and updating libraries, resolving dependencies, and more.
- Specify versions explicitly. When adding a library as a dependency, it's usually a good idea to specify the version explicitly. This can help avoid unexpected changes when the library is updated.
- Understand transitive dependencies. If you include a library that depends on other libraries, those are included in your project as well. These are called transitive dependencies. Be aware of them, as they can impact the size and performance of your application.
Interoperability with Java libraries
One of the key strengths of Kotlin is its seamless interoperability with Java. Java has been around for a long time and has a vast ecosystem of libraries for virtually every programming need. When Kotlin was designed, one of its primary goals was to be fully interoperable with Java. This means that any code you write in Java can be used in Kotlin and vice versa, therefore you can use Java libraries in your Kotlin projects without any issues. This interoperability allows Kotlin developers to leverage the vast ecosystem of existing Java libraries, thus enhancing the capabilities of their Kotlin applications.
Kotlin's Java interoperability is made possible by the fact that both Kotlin and Java compile down to the same bytecode, which runs on the Java Virtual Machine (JVM). This means that at the bytecode level, Kotlin classes and Java classes are indistinguishable from each other.
Much like the Kotlin standard library, Java also has its standard library, which offers tons of utilities and functions. For example, it offers APIs for:
- Data structures like
ArrayListandHashMapclasses in thejava.utilpackage. - Date and time manipulation like the
LocalDate,LocalTime, andLocalDateTimeclasses in thejava.timepackage. - Network programming provided by the
java.netpackage, which includes classes for network programming. - Database connectivity provided by the
java.sqlknown as JDBC–Java Database Connectivity. - ... and more.
Because of Kotlin's interoperability with Java, all these functionalities can be utilized in Kotlin as well. This means that as a Kotlin developer, you have access not only to the Kotlin standard library but also to the comprehensive Java standard library, where you can access all of its functionalities with no need to specify any kind of dependency in the build.gradle(.kts) file.
Here's an example showing the use of the Java BigInteger class in Kotlin:
import java.math.BigInteger
fun factorial(n: Int): BigInteger {
return when (n) {
0 -> BigInteger.ONE
else -> BigInteger.valueOf(n.toLong()) * factorial(n - 1)
}
}
fun main() {
println(factorial(21)) // 51090942171709440000
}
In this example, we're using the BigInteger class from the java.math package to calculate the factorial of a number.
When you're using a Java library in Kotlin, you can call its methods, inherit from its classes, and implement its interfaces just like you would in Java. Kotlin even provides some syntactic sugar that makes using Java libraries more idiomatic and enjoyable in Kotlin.
For example, when using a Java library that makes use of getters and setters, Kotlin allows you to use property access syntax:
import java.util.*
fun main() {
val date = Date()
val time = date.time // Calls the 'getTime' getter method
date.time = time + 3600000 // Calls the 'setTime' setter method
}
While Kotlin's Java interoperability is generally seamless, there can be some minor challenges due to differences in language features and idioms. For example, Java's nullability semantics are different from Kotlin's, which can lead to nullability issues when using Java libraries in Kotlin.
Kotlin provides null-safety by distinguishing nullable and non-nullable types at the language level. However, when calling Java code from Kotlin, the null-safety guarantees can't be ensured because Java doesn't have this distinction.
To overcome this, Kotlin treats types coming from Java as platform types, which leaves the nullability checks to the developer. When using a Java library, you should check whether methods can return null and handle the nullability appropriately in your Kotlin code.
Let's consider an example where we're using the java.util.HashMap class from Java in our Kotlin code:
import java.util.HashMap
fun main() {
val map = HashMap<String, String>()
map["key"] = "value"
val value: String = map["key"]
println(value.length)
}
In the above code, we're trying to access a value from the HashMap using a key. In Java, if the key doesn't exist in the map, the get method returns null. However, in Kotlin, we're trying to assign the result to a non-nullable String variable. This could potentially result in a NullPointerException at runtime if the key doesn't exist in the map.
To avoid this, we should handle the nullability appropriately in our Kotlin code:
import java.util.HashMap
fun main() {
val map = HashMap<String, String>()
map["key"] = "value"
val value: String? = map["key"]
println(value?.length)
}
In this updated code, we're assigning the result of the get method to a nullable String variable and using the safe call operator (?.) to access the length property. This way, we ensure that our code won't throw a NullPointerException even if the key doesn't exist in the map.
Best practices
Using libraries can significantly boost your productivity and enhance the capabilities of your applications. However, it's important to follow some best practices to ensure that your use of libraries doesn't introduce problems down the line. Here are some best practices to keep in mind:
- Understand the library's functionality. Before using a library, make sure you understand what it does and how it works. This can help you use the library more effectively and avoid potential issues.
- Check the library's documentation and support. Good libraries usually have comprehensive documentation and active community support. These resources can be invaluable when you're trying to understand how to use the library or when you run into problems.
- Be aware of the library's size and performance impact. Libraries can add to the size of your application and impact its performance. Always consider these factors when choosing a library (especially if it has transitive dependencies).
- Keep libraries up to date. Libraries are often updated with new features, performance improvements, patches for security vulnerabilities, and bug fixes. Make sure to keep your libraries up to date to benefit from these improvements.
- Handle potential incompatibilities. If you're using multiple libraries, there might be incompatibilities between them. Always test your application thoroughly when adding or updating libraries.
- Check the library's license. Make sure the library's license is compatible with how you plan to use it.
Conclusion
Libraries are an essential part of programming in Kotlin, providing pre-built functionalities that can extend the language's capabilities and simplify the coding process. They can be categorized into standard libraries, which are included with the language and provide essential functionalities, and third-party libraries, which offer specialized or advanced features. Kotlin also offers seamless interoperability with Java, allowing developers to leverage the vast ecosystem of existing Java libraries.
To effectively use libraries, it's important to understand their functionalities and how to integrate them into your projects, which typically involves specifying dependencies and importing packages. Managing library dependencies can be challenging, particularly in large projects, but tools like Gradle can help to automate this process. It's also crucial to keep libraries up to date, be aware of their size and performance impact, handle potential incompatibilities, and ensure their licenses are compatible with your intended use.
Finally, remember that while libraries can significantly boost your productivity and enhance the capabilities of your applications, they should be used judiciously. Always strive to understand the libraries you're using, follow best practices, and use the resources available to you, such as documentation and community support, to get the most out of them.
Now, happy practicing.