Basic syntax and idioms of Android development using Kotlin
Since Google I/O in 2019, Kotlin has become the first choice for Android mobile development.
Android development with Kotlin can benefit from:
- Less code and more readable. Spend less time writing code and understanding other people's code.
- Mature language and environment. Since its establishment in 2011, Kotlin has been developing throughout the ecosystem not only through language but also through powerful tools. Now, it has been seamlessly integrated into Android Studio and is actively used by many companies to develop Android applications.
- Android Jetpack and Kotlin support in other libraries. KTX extension adds Kotlin language features to the existing Android library, such as coroutine, extension function, lambdas and named parameters.
- Interoperability with Java. You can use Kotlin with the Java programming language in your application without having to migrate all your code to Kotlin.
- Support multi platform development. You can not only use Kotlin to develop Android, but also iOS, back-end and Web applications. Enjoy the benefits of sharing common code between platforms.
- Code security. Less code and better readability result in fewer errors. The Kotlin compiler detects these remaining errors to make the code safe.
- Easy to learn and use. Kotlin is very easy to learn, especially for Java developers.
- Big community. Kotlin has received strong support and many contributions from the community, which is growing all over the world. According to Google, more than 60% of the top 1000 apps in the Play store use kotlin.
1, Basic grammar
1.1 variables
Define read-only local variables using the keyword val. It can only be assigned once.
fun main() { val a: Int = 1 // Immediate assignment val b = 2 // Automatically infer 'Int' type val c: Int // If there is no initial value type, it cannot be omitted c = 3 // definitely assigned println("a = $a, b = $b, c = $c") } // a = 1, b = 2, c = 3
Variables that can be re assigned use the var keyword:
fun main() { var x = 5 // Automatically infer 'Int' type x += 1 println("x = $x") } // x = 6
Top level variables:
val PI = 3.14 var x = 0 fun incrementX() { x += 1 } fun main() { println("x = $x; PI = $PI") incrementX() println("incrementX()") println("x = $x; PI = $PI") } // x = 0; PI = 3.14 // incrementX() // x = 1; PI = 3.14
1.2 function
Functions in Kotlin are declared with the fun keyword:
The function parameters are defined using Pascal notation, i.e. name: type. Parameters are separated by commas. Each parameter must have an explicit type:
fun double(x: Int, y: Int): Int { return 2 * x * y }
Function parameters can have default values, which are used when corresponding parameters are omitted. This reduces the number of overloads compared to other languages:
fun double(x: Int, y: Int= 2): Int { return 2 * x * y }
If a default parameter precedes a parameter without a default value, the default value can only be used by calling the function with a named parameter:
fun double(x: Int = 3, y: Int): Int { return 2 * x *y } // double(y = 2) uses the default value x = 3
If the last parameter after the default parameter is a lambda expression, it can be passed in or out of parentheses as a named parameter
fun foo( bar: Int = 0, baz: Int = 1, qux: () -> Unit, ) { /*......*/ } foo(1) { println("hello") } // Use the default value baz = 1 foo(qux = { println("hello") }) // Use two default values bar = 0 and baz = 1 foo { println("hello") } // Use two default values bar = 0 and baz = 1
A function that returns Int with two Int parameters:
fun sum(a: Int, b: Int): Int { return a + b }
Functions that automatically infer an expression as a function body and return value type:
fun sum(a: Int, b: Int) = a + b
The calling function uses the traditional method:
val result = double(2)
1.3 string template
String literals can contain template expressions, small pieces of code that evaluate and merge the results into the string. The template expression starts with the dollar sign ($) and consists of a simple name:
val i = 10 println("i = $i") // Output "i = 10"
Or any expression enclosed in curly braces:
val s = "abc" println("$s.length is ${s.length}") // Output "abc.length is 3"
1.4 conditional expression
In Kotlin, if is an expression, that is, it returns a value. Therefore, there is no need for ternary operators (condition? Then: otherwise), because ordinary if can be competent for this role.
fun maxOf(a: Int, b: Int) = if (a > b) a else b
The branch of if can be a code block, and the last expression is the value of the block:
val max = if (a > b) { print("Choose a") a } else { print("Choose b") b }
1.5 null and null detection
In Kotlin, the type system distinguishes whether a reference can accommodate null (nullable reference) or not (non empty reference). For example, a regular variable of type String cannot hold null:
var a: String = "abc" // By default, regular initialization means non empty // a = null / / compilation error
When the value of a variable can be null, it must be added after the type at the declaration? To identify that the reference can be empty.
var b: String? = "abc" // Can be set to null b = null // ok
Now, if you call a's method or access its properties, it will not cause NPE, so you can safely use:
val l = a.length
However, if you want to access the same attribute of b, it is not safe, and the compiler will report an error:
val l = b.length // Error: variable 'b' may be empty
But we still need to access this property, right? There are several ways to do this.
1.5.1 detect null in condition
First, you can explicitly detect whether b is null and handle two possibilities:
val l = if (b != null) b.length else -1
The compiler tracks the information of the detection performed and allows you to call length inside the if. It also supports more complex (smarter) conditions:
val b: String? = "Kotlin" if (b != null && b.length > 0) { print("String of length ${b.length}") } else { print("Empty string") }
Please note that this only applies when b is immutable (that is, a local variable that has not been modified between detection and use, or a val member that is not overwritable and has a field behind the scenes), because otherwise, b may become null after detection.
1.5.2 safe call
Your second choice is to safely call the operator, writing
val a = "Kotlin" val b: String? = null println(b?.length) println(a?.length) // No security call required
If B is not empty, b.length is returned; otherwise, null is returned. The type of this expression is Int?.
Safe calls are useful in chained calls. For example, if an employee Bob may (or may not) be assigned to a department, and another employee may be the head of the Department, get the name of the head of Bob's Department (if any). We write:
bob?.department?.head?.name
If any attribute (link) is empty, the chain call will return null.
If you want to perform an operation only on non null values, the safe call operator can be used with let s:
val listWithNulls: List<String?> = listOf("Kotlin", null) for (item in listWithNulls) { item?.let { println(it) } // Output Kotlin and ignore null }
Security calls can also appear to the left of the assignment. In this way, if any receiver in the call chain is empty, the assignment will be skipped, and the expression on the right will not be evaluated at all:
// If 'person' or 'person Department ` if one of them is empty, this function will not be called: person?.department?.head = managersPool.getManager()
1.5.3 Elvis operator
When we have an nullable reference b, we can say "if b is not empty, I use it; otherwise, I use a non empty value":
val l: Int = if (b != null) b.length else -1
In addition to the complete if expression, this can also be expressed through the Elvis operator. Write?:
val l = b?.length ?: -1
If?: If the left expression is not empty, the elvis operator returns its left expression, otherwise it returns the right expression. Note that the expression on the right side is evaluated if and only if the left side is empty.
Note that because throw and return are both expressions in Kotlin, they can also be used to the right of the elvis operator. This can be very convenient, for example, to detect function parameters:
fun foo(node: Node): String? { val parent = node.getParent() ?: return null val name = node.getName() ?: throw IllegalArgumentException("name expected") // ...... }
1.5.4 !! Operator
The third option is for NPE enthusiasts: non empty assertion operator (!) Converts any value to a non null type, and throws an exception if the value is null. We can write B, This will return a non empty b value (for example, String in our example) or if B is empty, an NPE exception will be thrown:
val l = b!!.length
Therefore, if you want an NPE, you can get it, but you must explicitly require it, otherwise it won't come unexpectedly.
1.5.5 safe type conversion
If the object is not the target type, regular type conversion may result in ClassCastException. Another option is to use safe type conversion. If the conversion attempt is unsuccessful, null will be returned:
val aInt: Int? = a as? Int
1.5.6 collection of nullable types
If you have a collection of nullable elements and want to filter non empty elements, you can use filterNotNull to implement:
val nullableList: List<Int?> = listOf(1, 2, null, 4) val intList: List<Int> = nullableList.filterNotNull()
1.6 type detection and automatic type conversion
1.6.1.1 is and! Is operator
We can use the is operator or its negative form at runtime! Is to detect whether the object conforms to the given type:
if (obj is String) { print(obj.length) } if (obj !is String) { // And! (obj is String) same print("Not a String") } else { print(obj.length) }
1.6.2 intelligent conversion
In many cases, explicit conversion operators do not need to be used in Kotlin, because the compiler tracks is detection of immutable values and explicit conversions, and automatically inserts (SAFE) conversions when needed:
fun demo(x: Any) { if (x is String) { print(x.length) // x is automatically converted to a string } }
The compiler is smart enough to know that if reverse detection results in a return, the conversion is safe:
if (x !is String) return print(x.length) // x is automatically converted to a string
Or to the right of & & and |:
// `||`The x on the right is automatically converted to a string if (x !is String || x.length == 0) return // `&&`The x on the right is automatically converted to a string if (x is String && x.length > 0) { print(x.length) // x is automatically converted to a string }
1.7 For cycle
val items = listOf("apple", "banana", "kiwifruit") for (item in items) { println(item) } val items = listOf("apple", "banana", "kiwifruit") for (index in items.indices) { println("item at $index is ${items[index]}") } for (i in 1..4) print(i) // Output "1234" for (i in 4..1) print(i) // Nothing output if (i in 1..10) { // Equivalent to 1 < = I & & I < = 10 println(i) } // Use step to specify the step size for (i in 1..4 step 2) print(i) // Output "13" for (i in 4 downTo 1 step 2) print(i) // Output "42" // Use the until function to exclude the end element for (i in 1 until 10) { // i in [1, 10) excludes 10 println(i) }
1.8 when expression
The when expression replaces the switch statement of Java like language. Its simplest form is as follows:
when (x) { 1 -> print("x == 1") 2 -> print("x == 2") else -> { // Pay attention to this block print("x is neither 1 nor 2") } }
If when is used as an expression, there must be an else branch unless the compiler can detect that all possible conditions have been overridden
If many branches need to be processed in the same way, you can put multiple branch conditions together and separate them with commas:
when (x) { 0, 1 -> print("x == 0 or x == 1") else -> print("otherwise") }
We can use arbitrary expressions (not just constants) as branching conditions
when (x) { parseInt(s) -> print("s encodes x") else -> print("s does not encode x") }
We can also detect whether a value is in (in) or not in (! In) an interval or set:
when (x) { in 1..10 -> print("x is in the range") in validNumbers -> print("x is valid") !in 10..20 -> print("x is outside the range") else -> print("none of the above") }
Another possibility is to detect whether a value is (is) or not (! Is) a specific type of value. Note: due to intelligent transformation, you can access methods and properties of this type without any additional detection.
fun hasPrefix(x: Any) = when(x) { is String -> x.startsWith("prefix") else -> false }
when can also be used to replace the if else if chain. If no parameters are provided, all branch conditions are simple Boolean expressions. when the condition of a branch is true, the branch is executed:
when { x.isOdd() -> print("x is odd") y.isEven() -> print("y is even") else -> print("x+y is odd.") }
Since Kotlin 1.3, the subject of when can be captured into a variable using the following syntax:
fun Request.getBody() = when (val response = executeRequest()) { is Success -> response.body is HttpError -> throw HttpException(response.status) }
The scope of the variable introduced in the when subject is limited to the when subject.
1.9 collection
1.9.1 set conversion
- mapping
The mapping transformation creates a collection from the result of a function on an element of another collection. The basic mapping function is map(). It applies the given lambda function to each subsequent element and returns a list of lambda results. The order of the results is the same as the original order of the elements. To apply a transformation that also uses an element index as a parameter, use mapIndexed().
val numbers = setOf(1, 2, 3) println(numbers.map { it * 3 }) println(numbers.mapIndexed { idx, value -> value * idx }) //[3, 6, 9] //[0, 2, 6]
If the transformation produces null values on some elements, you can filter out null values from the result set by calling the mapNotNull() function instead of map() or mapIndexedNotNull() instead of mapIndexed().
val numbers = setOf(1, 2, 3) println(numbers.mapNotNull { if ( it == 2) null else it * 3 }) println(numbers.mapIndexedNotNull { idx, value -> if (idx == 0) null else value * idx }) //[3, 9] //[2, 6]
When mapping conversion, there are two options: conversion key, keep the value unchanged, and vice versa. To apply the specified transformation to keys, use mapKeys(); In turn, mapValues() converts values. Both functions use transformations that take mapping entries as arguments, so you can manipulate their keys and values.
fun main() { val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11) println(numbersMap.mapKeys { it.key.toUpperCase() }) println(numbersMap.mapValues { it.value + it.key.length }) } //{KEY1=1, KEY2=2, KEY3=3, KEY11=11} //{key1=5, key2=6, key3=7, key11=16}
- Close
The closure transformation is to build a Pair based on elements with the same position in two sets. In the Kotlin standard library, this is done through the zip() extension function. When called on a collection (or array) with another collection (or array) as a parameter, zip() returns a List of Pair objects. The element of the receiver collection is the first of these pairs. If the size of the collection is different, the result of zip() is the size of the smaller collection; The result does not contain subsequent elements of a larger set. zip() can also call a zip b as an infix.
fun main() { val colors = listOf("red", "brown", "grey") val animals = listOf("fox", "bear", "wolf") println(colors zip animals) val twoAnimals = listOf("fox", "bear") println(colors.zip(twoAnimals)) } // [(red, fox), (brown, bear), (grey, wolf)] // [(red, fox), (brown, bear)]
When you have a Pair's List, you can do the reverse conversion unzipping -- build two lists from these key value pairs:
The first list contains the keys for each Pair in the original list.
The second list contains the values for each Pair in the original list.
To split the list of key value pairs, call unzip().
fun main() { val numberPairs = listOf("one" to 1, "two" to 2, "three" to 3, "four" to 4) println(numberPairs.unzip()) } // ([one, two, three, four], [1, 2, 3, 4])
- relation
Associative transformation allows you to build a Map from a collection element and some of its associated values. In different association types, elements can be keys or values in the association Map.
The basic association function associateWith() creates a Map in which the elements of the original collection are keys and generates values from it through the given transformation function. If the two elements are equal, only the last one remains in the Map.
fun main() { val numbers = listOf("one", "two", "three", "four") println(numbers.associateWith { it.length }) } // {one=3, two=3, three=5, four=4}
To build a Map using collection elements as values, there is a function associateBy(). It requires a function that returns the key based on the value of the element. If the two elements are equal, only the last one remains in the Map. You can also use the value conversion function to call associateBy().
fun main() { val numbers = listOf("one", "two", "three", "four") println(numbers.associateBy { it.first().toUpperCase() }) println(numbers.associateBy(keySelector = { it.first().toUpperCase() }, valueTransform = { it.length })) } // {O=one, T=three, F=four} // {O=3, T=5, F=4}
- Level
If you want to manipulate nested collections, you may find it useful to provide standard library functions that provide flattening access to nested collection elements.
The first function is flatten(). It can be called in a Set of sets (for example, a List composed of sets). This function returns a List of all elements in the nested collection.
fun main() { val numberSets = listOf(setOf(1, 2, 3), setOf(4, 5, 6), setOf(1, 2)) println(numberSets.flatten()) } // [1, 2, 3, 4, 5, 6, 1, 2]
Another function, flatMap(), provides a flexible way to handle nested collections. It requires a function to map one collection element to another. Therefore, flatMap() returns a single list containing the values of all elements. Therefore, flatMap() is represented as a continuous call of map() (taking the set as the mapping result) and flatten().
data class StringContainer(val values: List<String>) fun main() { val containers = listOf( StringContainer(listOf("one", "two", "three")), StringContainer(listOf("four", "five", "six")), StringContainer(listOf("seven", "eight")) ) println(containers.flatMap { it.values }) } // [one, two, three, four, five, six, seven, eight]
- String representation
If you need to retrieve the contents of a collection in a readable format, use the functions that convert the collection to a string: joinToString() and joinTo().
joinToString() builds a single String from a collection element based on the supplied parameters. joinTo() does the same, but attaches the result to the given Appendable object.
When called with default parameters, the result returned by the function is similar to calling toString() on the collection: the String representation of each element is separated by spaces.
fun main() { val numbers = listOf("one", "two", "three", "four") println(numbers) println(numbers.joinToString()) val listString = StringBuffer("The list of numbers: ") numbers.joinTo(listString) println(listString) } // [one, two, three, four] // one, two, three, four // The list of numbers: one, two, three, four
To build a custom string representation, you can specify its parameters in the function parameters separator, prefix, and postfix. The result string will start with prefix and end with postfix. With the exception of the last element, the separator will follow each element.
fun main() { val numbers = listOf("one", "two", "three", "four") println(numbers.joinToString(separator = " | ", prefix = "start: ", postfix = ": end")) } // start: one | two | three | four: end
For larger collections, you might want to specify limit -- the number of elements that will be included in the result. If the collection size exceeds the limit, all other elements are replaced by a single value of the truncated parameter.
fun main() { val numbers = (1..100).toList() println(numbers.joinToString(limit = 10, truncated = "<...>")) } // 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, <...>
Finally, to customize the representation of the element itself, provide the transform function.
fun main() { val numbers = listOf("one", "two", "three", "four") println(numbers.joinToString { "Element: ${it.toUpperCase()}"}) } // Element: ONE, Element: TWO, Element: THREE, Element: FOUR
1.9.2 filtration
- Filter by predicate
The basic filter function is filter(). When called with a predicate, filter() returns the collection elements that match it. For List and Set, the filtering result is a List, and for Map, the result is also a Map.
fun main() { val numbers = listOf("one", "two", "three", "four") val longerThan3 = numbers.filter { it.length > 3 } println(longerThan3) val numbersMap = mapOf("key1" to 1, "key2" to 2, "key3" to 3, "key11" to 11) val filteredMap = numbersMap.filter { (key, value) -> key.endsWith("1") && value > 10} println(filteredMap) } // [three, four] // {key11=11}
The predicate in filter() can only check the value of an element. If you want to use the element's position in the collection in filtering, you should use filterIndexed(). It accepts a predicate with two parameters: the index of the element and the value of the element.
If you want to use negative conditions to filter the collection, use filterNot(). It returns a list of elements that make the predicate false.
fun main() { val numbers = listOf("one", "two", "three", "four") val filteredIdx = numbers.filterIndexed { index, s -> (index != 0) && (s.length < 5) } val filteredNot = numbers.filterNot { it.length <= 3 } println(filteredIdx) println(filteredNot) } // [two, four] // [three, four]
There are also functions that can narrow the type of an element by filtering elements of a given type:
filterIsInstance() returns a collection element of the given type. When called on a List, filterisinstance () returns a List, allowing you to call T-type functions on collection elements.
fun main() { val numbers = listOf(null, 1, "two", 3.0, "four") println("All String elements in upper case:") numbers.filterIsInstance<String>().forEach { println(it.toUpperCase()) } } // All String elements in upper case: // TWO // FOUR
filterNotNull() returns all non empty elements. In a list < T? > When called on, filterNotNull() returns a list < T: any >, allowing you to treat all elements as non empty objects.
fun main() { val numbers = listOf(null, "one", "two", null) numbers.filterNotNull().forEach { println(it.length) // Length is not available for nullable strings } } // 3 3
- divide
Another filter function – partition() – filters the collection through a predicate and stores mismatched elements in a separate List. Therefore, you get the Pair of a List as the return value: the first List contains the elements that match the predicate, and the second List contains all the other elements in the original collection.
fun main() { val numbers = listOf("one", "two", "three", "four") val (match, rest) = numbers.partition { it.length > 3 } println(match) println(rest) } // [three, four] // [one, two]
- Test predicate
Finally, some functions simply detect a predicate for collection elements:
any() returns true if at least one element matches the given predicate.
none() returns true if no element matches the given predicate.
all() returns true if all elements match the given predicate. Note that calling all () with any valid predicate on an empty collection returns true. This behavior is logically called vacuous truth.
fun main() { val numbers = listOf("one", "two", "three", "four") println(numbers.any { it.endsWith("e") }) println(numbers.none { it.endsWith("a") }) println(numbers.all { it.endsWith("e") }) println(emptyList<Int>().all { it > 5 }) // vacuous truth } // true true false true
Any () and none() can also be used without predicates: in this case, they are only used to check whether the collection is empty. If there are elements in the collection, any() returns true, otherwise false; none() is the opposite.
fun main() { val numbers = listOf("one", "two", "three", "four") val empty = emptyList<String>() println(numbers.any()) println(empty.any()) println(numbers.none()) println(empty.none()) } // true false false true
2, Idiomatic usage
2.1 creating DTOs
data class Customer(val name: String, val email: String, var age: Int)
The following functions are provided for the Customer class:
getters of all attributes (setters defined for var)
equals()
hashCode()
toString()
copy()
component1(), component2(), etc. of all attributes
2.2 default parameters of function
fun foo(a: Int = 0, b: String = "") { ...... }
2.3 filter list
val positives = list.filter { x -> x > 0 } // Or shorter: val positives = list.filter { it > 0 }
2.4 detect whether the element exists in the collection
if ("john@example.com" in emailsList) { ...... } if ("jane@example.com" !in emailsList) { ...... }
2.5 string interpolation
println("Name $name")
2.6 type judgment
when (x) { is Foo //-> ...... is Bar //-> ...... else //-> ...... }
2.7 traversing map/pair list
k. v can be changed to any name.
for ((k, v) in map) { println("$k -> $v") }
2.8 service interval
for (i in 1..100) { ...... } // Closed interval: including 100 for (i in 1 until 100) { ...... } // Half open interval: excluding 100 for (x in 2..10 step 2) { ...... } for (x in 10 downTo 1) { ...... } if (x in 1..10) { ...... }
2.9 read only list
val list = listOf("a", "b", "c")
2.10 read only map
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
2.11 access map
println(map["key"]) map["key"] = value
2.12 delay properties
val p: String by lazy { // Evaluate the string }
2.13 extension function
fun String.toMD5(): String { return try { // Get an MD5 converter (if you want to change the SHA1 parameter to "SHA1") val messageDigest = MessageDigest.getInstance("MD5") // Convert the input string to a byte array val inputByteArray: ByteArray = toByteArray() // inputByteArray is a byte array converted from an input string messageDigest.update(inputByteArray) // The conversion and return result is also a byte array, containing 16 elements val resultByteArray = messageDigest.digest() // Convert character array to string and return ByteUtils.bytesToHex(resultByteArray) } catch (e: NoSuchAlgorithmException) { "" } } "How do you do".toMD5()
2.14 creating a single instance
object Resource { val name = "Name" }
2.15 If not null abbreviation
val files = File("Test").listFiles() println(files?.size)
2.16 If not null and else abbreviations
val files = File("Test").listFiles() println(files?.size ?: "empty")
2.17 if not null execution code
val value = ...... value?.let { ...... // The code will execute here if the data is not null }
2.18 mapping nullable value (if not empty)
val value = ...... val mapped = value?.let { transformValue(it) } ?: defaultValue // If the value or its conversion result is empty, defaultValue is returned.
2.19 return when expression
fun transform(color: String): Int { return when (color) { "Red" -> 0 "Green" -> 1 "Blue" -> 2 else -> throw IllegalArgumentException("Invalid color param value") } }
2.20 calling multiple methods (with) on an object instance
class Turtle { fun penDown() fun penUp() fun turn(degrees: Double) fun forward(pixels: Double) } val myTurtle = Turtle() with(myTurtle) { // Draw a 100 pixel square penDown() for (i in 1..4) { forward(100.0) turn(90.0) } penUp() }
2.21 configuration object properties (apply)
This is useful for configuring properties that do not appear in the object constructor.
val myRectangle = Rectangle().apply { length = 4 breadth = 5 color = 0xFAFAFA }
2.22 try with resources
use
val stream = Files.newInputStream(Paths.get("/some/file.txt")) stream.buffered().reader().use { reader -> println(reader.readText()) }
2.23 suitable forms of generic functions requiring generic information
// public final class Gson { // ...... // public <T> T fromJson(JsonElement json, Class<T> classOfT) throws JsonSyntaxException { // ...... inline fun <reified T: Any> Gson.fromJson(json: JsonElement): T = this.fromJson(json, T::class.java)
2.24 exchange two variables
var a = 1 var b = 2 a = b.also { b = a }