Kotlin - rFronteddu/general_wiki GitHub Wiki
In Kotlin:
- fun is used to declare a function
- The main() function is where your program starts from
- The body of a function is written within curly braces {}
- println() and print() functions print their arguments to standard output
fun main() {
println("Hello, world!")
// Hello, world!
}
You can declare:
- Read-only variables with val
- Mutable variables with var
You can use template expressions to access data stored in variables and other objects, and convert them into strings. A string value is a sequence of characters in double quotes ". Template expressions always start with a dollar sign $.
To evaluate a piece of code in a template expression, place the code within curly braces {} after the dollar sign $.
println("There are $customers customers")
// There are 10 customers
println("There are ${customers + 1} customers")
// There are 11 customers
You can use templates both in multiline and escaped strings. However, multiline strings don't support backslash escaping. To insert the dollar sign $ in a multiline string before any symbol allowed at the beginning of an identifier, use the following syntax:
val price = """
${'$'}_9.99
"""
To avoid ${'$'} sequences in strings, you can use the Experimental multi-dollar string interpolation feature:
val KClass<*>.jsonSchema : String
get() = $$"""
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/product.schema.json",
"$dynamicAnchor": "meta",
"title": "$${simpleName ?: qualifiedName ?: "unknown"}",
"type": "object"
}
"""
Use when when you have a conditional expression with multiple branches.
To use when:
-
Place the value you want to evaluate within parentheses ().
-
Place the branches within curly braces {}.
-
Use -> in each branch to separate each check from the action to take if the check is successful.
-
when can be used either as a statement or as an expression. A statement doesn't return anything but performs actions instead.
For example the following print "Greeting"
val obj = "Hello"
when (obj) {
// Checks whether obj equals to "1"
"1" -> println("One")
// Checks whether obj equals to "Hello"
"Hello" -> println("Greeting")
// Default statement
else -> println("Unknown")
}
Note that all branch conditions are checked sequentially until one of them is satisfied. So only the first suitable branch is executed.
An expression returns a value that can be used later in your code.
Here is an example of using when as an expression. The when expression is assigned immediately to a variable which is later used with the println() function:
```
val obj = "Hello"
val result = when (obj) {
// If obj equals "1", sets result to "one"
"1" -> "One"
// If obj equals "Hello", sets result to "Greeting"
"Hello" -> "Greeting"
// Sets result to "Unknown" if no previous condition is satisfied
else -> "Unknown"
}
println(result)
// Greeting
when can also be used without a subject.
This example uses a when expression without a subject to check a chain of Boolean expressions:
fun main() { val trafficLightState = "Red" // This can be "Green", "Yellow", or "Red" val trafficAction = when { trafficLightState == "Green" -> "Go" trafficLightState == "Yellow" -> "Slow down" trafficLightState == "Red" -> "Stop" else -> "Malfunction" } println(trafficAction) // Stop }
However, you can have the same code but with trafficLightState as the subject:
fun main() {
val trafficLightState = "Red" // This can be "Green", "Yellow", or "Red"
val trafficAction = when (trafficLightState) {
"Green" -> "Go"
"Yellow" -> "Slow down"
"Red" -> "Stop"
else -> "Malfunction"
}
println(trafficAction)
// Stop
}
Using when with a subject makes your code easier to read and maintain. When you use a subject with a when expression, it also helps Kotlin check that all possible cases are covered. Otherwise, if you don't use a subject with a when expression, you need to provide an else branch.## Ranges
* To create a range in Kotlin is to use the .. operator. For example, 1..4 is equivalent to 1, 2, 3, 4.
* To declare a range that doesn't include the end value, use the ..< operator. For example, 1..<4 is equivalent to 1, 2, 3.
* To declare a range in reverse order, use downTo. For example, 4 downTo 1 is equivalent to 4, 3, 2, 1.
* To declare a range that increments in a step that isn't 1, use step and your desired increment value. For example, 1..5 step 2 is equivalent to 1, 3, 5.
You can also do the same with Char ranges:
* 'a'..'d' is equivalent to 'a', 'b', 'c', 'd'
* 'z' downTo 's' step 2 is equivalent to 'z', 'x', 'v', 't'
## Single-expression functions
To make your code more concise, you can use single-expression functions. For example, the sum() function can be shortened:
fun sum(x: Int, y: Int): Int { return x + y } fun main() { println(sum(1, 2)) // 3 }
Can become:
fun sum(x: Int, y: Int) = x + y fun main() { println(sum(1, 2)) // 3 }
## Lambda expressions
Kotlin allows you to write even more concise code for functions by using lambda expressions.
For example, the following uppercaseString() function:
fun uppercaseString(text: String): String {
return text.uppercase()
}
fun main() {
println(uppercaseString("hello"))
// HELLO
}
Can also be written as a lambda expression:
fun main() {
val upperCaseString = { text: String -> text.uppercase() }
println(upperCaseString("hello"))
// HELLO
}
Lambda expressions are written within curly braces {}.
Within the lambda expression, you write:
- The parameters followed by an ->.
- The function body after the ->.
If you declare a lambda without parameters, then there is no need to use ->. For example:
{ println("Log message") }
Functions themselves also have a type. Kotlin's type inference can infer a function's type from the parameter type. But there may be times when you need to explicitly specify the function type. The compiler needs the function type so that it knows what is and isn't allowed for that function.
The syntax for a function type has:
- Each parameter's type written within parentheses () and separated by commas ,.
- The return type written after ->.
- For example: (String) -> String or (Int, Int) -> Int.
This is what a lambda expression looks like if a function type for upperCaseString() is defined:
val upperCaseString: (String) -> String = { text -> text.uppercase() }
fun main() {
println(upperCaseString("hello"))
// HELLO
}
If your lambda expression has no parameters then the parentheses () are left empty. For example: () -> Unit
You must declare parameter and return types either in the lambda expression or as a function type. Otherwise, the compiler won't be able to know what type your lambda expression is.
For example, the following won't work:
val upperCaseString = { str -> str.uppercase() }
Invoke separately Lambda expressions can be invoked on their own by adding parenthesis
println({ text: String -> text.uppercase() }("hello"))
// HELLO
As you have already seen, if a lambda expression is the only function parameter, you can drop the function parentheses (). If a lambda expression is passed as the last parameter of a function, then the expression can be written outside the function parentheses (). In both cases, this syntax is called a trailing lambda.
For example, the .fold() function accepts an initial value and an operation:
// The initial value is zero.
// The operation sums the initial value with every item in the list cumulatively.
println(listOf(1, 2, 3).fold(0, { x, item -> x + item })) // 6
// Alternatively, in the form of a trailing lambda
println(listOf(1, 2, 3).fold(0) { x, item -> x + item }) // 6
Another lambda: repeat an action n times
fun repeatN(n: Int, action: () -> Unit) {
for (i in 1..n) {
action()
}
}
fun main() {
repeatN(5) {
println("Hello")
}
}
Characteristics of a class's object can be declared in properties. You can declare properties for a class:
- Within parentheses () after the class name.
class Contact(val id: Int, var email: String)
- Within the class body defined by curly braces {}.
class Contact(val id: Int, var email: String) {
val category: String = ""
}
Prefer read-only (val). You can declare properties without val or var within parentheses but these properties are not accessible after an instance has been created. The content contained within parentheses () is called the class header. You can use a trailing comma when declaring class properties.
Just like with function parameters, class properties can have default values:
class Contact(val id: Int, var email: String = "[email protected]") {
val category: String = "work"
}
To create an object from a class, you declare a class instance using a constructor.
By default, Kotlin automatically creates a constructor with the parameters declared in the class header.
class Contact(val id: Int, var email: String)
fun main() {
val contact = Contact(1, "[email protected]")
}
Kotlin has data classes which are particularly useful for storing data. Data classes have the same functionality as classes, but they come automatically with additional member functions. These member functions allow you to easily print the instance to readable output, compare instances of a class, copy instances, and more. As these functions are automatically available, you don't have to spend time writing the same boilerplate code for each of your classes.
To declare a data class, use the keyword data:
data class User(val name: String, val id: Int)
The most useful predefined member functions of data classes are:
- toString(): Prints a readable string of the class instance and its properties.
- equals() or ==: Compares instances of a class.
- copy(): Creates a class instance by copying another, potentially with some different properties.
To compare data class instances, use the equality operator ==.
To create a copy of a data class instance and change some properties, call the copy() function on the instance and add replacement values for properties as function parameters.
val user = User("Alex", 1)
// Creates an exact copy of user
println(user.copy())
// User(name=Alex, id=1)
// Creates a copy of user with name: "Max"
println(user.copy("Max"))
// User(name=Max, id=1)
// Creates a copy of user with id: 3
println(user.copy(id = 3))
// User(name=Alex, id=3)
To help prevent issues with null values in your programs, Kotlin has null safety in place. Null safety detects potential problems with null values at compile time, rather than at run time.
Null safety is a combination of features that allow you to:
- Explicitly declare when null values are allowed in your program.
- Check for null values.
- Use safe calls to properties or functions that may contain null values.
- Declare actions to take if null values are detected.
Kotlin supports nullable types which allows the possibility for the declared type to have null values. By default, a type is not allowed to accept null values. Nullable types are declared by explicitly adding ? after the type declaration.
var nullable: String? = "You can keep a null here"
// This is OK
nullable = null
// By default, null values aren't accepted
var inferredNonNull = "The compiler assumes non-nullable"
// Throws a compiler error
inferredNonNull = null
To safely access properties of an object that might contain a null value, use the safe call operator ?.. The safe call operator returns null if either the object or one of its accessed properties is null. This is useful if you want to avoid the presence of null values triggering errors in your code.
Safe calls can be chained so that if any property of an object contains a null value, then null is returned without an error being thrown. For example:
person.company?.address?.country
The safe call operator can also be used to safely call an extension or member function. In this case, a null check is performed before the function is called. If the check detects a null value, then the call is skipped and null is returned.
fun main() {
val nullString: String? = null
println(nullString?.uppercase())
// null
}
You can provide a default value to return if a null value is detected by using the Elvis operator ?:.
Write on the left-hand side of the Elvis operator what should be checked for a null value. Write on the right-hand side of the Elvis operator what should be returned if a null value is detected.
fun main() {
val nullString: String? = null
println(nullString?.length ?: 0)
// 0
}
you might want to add extra functionality to a class from a third-party library.
You can do this by adding extension functions to extend a class. You call extension functions the same way you call member functions of a class, using a period ..
The receiver is what the function is called on. In other words, the receiver is where or with whom the information is shared.
To create an extension function, write the name of the class that you want to extend followed by a . and the name of your function. Continue with the rest of the function declaration, including its arguments and return type.
fun String.bold(): String = "<b>$this</b>"
fun main() {
// "hello" is the receiver
println("hello".bold())
// <b>hello</b>
}
| Function | Access to x via | Return value | Use case |
|---|---|---|---|
| let | it | Lambda result | Perform null checks in your code and later perform further actions with the returned object. |
| apply | this | x | Initialize objects at the time of creation. |
| run | this | Lambda result | Initialize objects at the time of creation AND compute a result. |
| also | it | x | Complete additional actions before returning the object. |
| with | this | Lambda result | Call multiple functions on an object. |
The most commonly referred to scopes are the global scope and the local scope:
- Global scope – a variable or object that is accessible from anywhere in the program.
- Local scope – a variable or object that is only accessible within the block or function where it is defined.
In Kotlin, there are also scope functions that allow you to create a temporary scope around an object and execute some code.
Scope functions make your code more concise because you don't have to refer to the name of your object within the temporary scope. Depending on the scope function, you can access the object either by referencing it via the keyword this or using it as an argument via the keyword it.
Kotlin has five scope functions in total: let, apply, run, also, and with. Each scope function takes a lambda expression and returns either the object or the result of the lambda expression.
Use the let scope function when you want to perform null checks in your code and later perform further actions with the returned object.
val address: String? = getNextAddress()
val confirm = address?.let { // confirm is null if address is null
sendNotification(it)
}
Use the apply scope function to initialize objects, like a class instance, at the time of creation rather than later on in your code. This approach makes your code easier to read and manage.
val client = Client().apply {
token = "asdf"
connect()
authenticate()
}
fun main() {
client.getData()
// connected!
// authenticated!
}
Similar to apply, you can use the run scope function to initialize an object, but it's better to use run to initialize an object at a specific moment in your code and immediately compute a result.
val client: Client = Client().apply {
token = "asdf"
}
fun main() {
val result: String = client.run {
connect()
// connected!
authenticate()
// authenticated!
getData()
}
}
Use the also scope function to complete an additional action with an object and then return the object to continue using it in your code, like writing a log.
fun main() {
val medals: List<String> = listOf("Gold", "Silver", "Bronze")
val reversedLongUppercaseMedals: List<String> =
medals
.map { it.uppercase() }
.also { println(it) }
// [GOLD, SILVER, BRONZE]
.filter { it.length > 4 }
.also { println(it) } // Passes a lambdato the .filter() that refers to medals via the it keyword and checks
// [SILVER, BRONZE]
.reversed()
println(reversedLongUppercaseMedals)
// [BRONZE, SILVER]
}
Since the also function returns the object, it is useful for not only logging but debugging, chaining multiple operations, and performing other side-effect operations that don't affect the main flow of your code.
Unlike the other scope functions, with is not an extension function, so the syntax is different. You pass the receiver object to with as an argument.
Use the with scope function when you want to call multiple functions on an object.
val mainMonitorSecondaryBufferBackedCanvas = Canvas()
with(mainMonitorSecondaryBufferBackedCanvas) {
text(10, 10, "Foo")
rect(20, 30, 100, 50)
circ(40, 60, 25)
text(15, 45, "Hello")
rect(70, 80, 150, 100)
circ(90, 110, 40)
text(35, 55, "World")
rect(120, 140, 200, 75)
circ(160, 180, 55)
text(50, 70, "Kotlin")
}