写在前面 Andorid第一行代码Kotlin小课堂
标准函数with、run和apply with函数接收两个参数:第一个参数可以是一个任意类型的对象,第二个参数是一个Lambda表达式。with函数会在Lambda表达式中提供第一个参数对象的上下文,并使用Lambda表达式中的最后一行代码作为返回值返回。
示例代码如下:
1 2 3 4 val result = with(obj) { // 这里是obj的上下文 "value" // with函数的返回值 }
run函数的用法和使用场景其实和with函数是非常类似的,只是稍微做了一些语法改动而已。首先run函数是不能直接调用的,而是一定要调用某个对象的run函数才行;其次run函数只接收一个Lambda参数,并且会在Lambda表达式中提供调用对象的上下文。其他方面和with函数是一样的,包括也会使用Lambda表达式中的最后一行代码作为返回值返回。
示例代码如下:
1 2 3 4 val result = obj.run { // 这里是obj的上下文 "value" // run函数的返回值 }
apply函数和run函数也是极其类似的,都是要在某个对象上调用,并且只接收一个Lambda参数,也会在Lambda表达式中提供调用对象的上下文,但是apply函数无法指定返回值,而是会自动返回调用对象本身。
示例代码如下:
1 2 3 4 val result = obj.apply { // 这里是obj的上下文 } // result == obj
定义静态方法 Kotlin没有直接定义静态方法的关键字,但是提供了一些语法特性来支持类似于静态方法调用的写法,如单例类,companion object等,这些语法特性基本可以满足我们平时的开发需求了。
然而如果你确确实实需要定义真正的静态方法, Kotlin仍然提供了两种实现方式:注解和顶层方法。
1 2 3 4 5 6 7 8 9 10 class Util { companion object { @JvmStatic fun doAction() { println("do action2") } } }
1 2 3 fun doSomething() { println("do something") }
对变量延迟初始化 延迟初始化使用的是lateinit关键字,它可以告诉Kotlin编译器,我会在晚些时候对这个变量进行初始化,这样就不用在一开始的时候将它赋值为null了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class MainActivity : AppCompatActivity(), View.OnClickListener { private lateinit var adapter: MsgAdapter override fun onCreate(savedInstanceState: Bundle?) { … adapter = MsgAdapter(msgList) … } override fun onClick(v: View?) { … adapter.notifyItemInserted(msgList.size - 1) … } }
使用密封类优化代码 当在when语句中传入一个密封类变量作为条件时,Kotlin编译器会自动检查该密封类有哪些子类,并强制要求你将每一个子类所对应的条件全部处理。
这样就可以保证,即使没有编写else条件,也不可能会出现漏写条件分支的情况。
1 2 3 sealed class Result class Success(val msg: String) : Result() class Failure(val error: Exception) : Result()
1 2 3 4 fun getResultMsg(result: Result) = when (result) { is Success -> result.msg is Failure -> "Error is ${result.error.message}" }
扩展函数 扩展函数表示即使在不修改某个类的源码的情况下,仍然可以打开这个类,向该类添加新的函数。
其语法结构非常简单,如下所示:
1 2 3 fun ClassName.methodName(param1: Int, param2: Int): Int { return 0 }
运算符重载 Kotlin的运算符重载允许我们让任意两个对象进行相加,或者是进行更多其他的运算操作。
这里以加号运算符为例,如果想要实现让两个对象相加的功能,那么它的语法结构如下:
1 2 3 4 5 6 7 class Obj { operator fun plus(obj: Obj): Obj { // 处理相加的逻辑 } }
语法糖表达式和实际调用函数对照表如下所示:
定义高阶函数 如果一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,那么该函数就称为高阶函数。
1 2 3 fun example(func: (String, Int) -> Unit) { func("hello", 123) }
可以看到,这里的example()函数接收了一个函数类型的参数,因此example()函数就是一个高阶函数。而调用一个函数类型的参数,它的语法类似于调用一个普通的函数,只需要在参数名的后面加上一对括号,并在括号中传入必要的参数即可。
内联函数的作用 内联函数可以将使用Lambda表达式带来的运行时开销完全消除。它的工作原理并不复杂,就是Kotlin编译器会将内联函数中的代码在编译的时候自动替换到调用它的地方,这样也就不存在运行时的开销了。
定义内联函数很简单,只需要在定义高阶函数时加上inline关键字的声明即可,如下所示:
1 2 3 4 inline fun num1AndNum2(num1: Int, num2: Int, operation: (Int, Int) -> Int): Int { val result = operation(num1, num2) return result }
简化SharedPreferences的用法 我们可以使用高阶函数简化SharedPreferences的用法,如下所示:
1 2 3 4 5 fun SharedPreferences.open(block: SharedPreferences.Editor.() -> Unit) { val editor = edit() editor.block() editor.apply() }
定义好了open函数之后,以后在项目中使用SharedPreferences存储数据就会更加方便了,写法如下:
1 2 3 4 5 getSharedPreferences("data", Context.MODE_PRIVATE).open { putString("name", "Tom") putInt("age", 28) putBoolean("married", false) }
简化ContentValues的用法 我们还可以借助vararg和Pair对象来简化ContentValues的用法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fun cvOf(vararg pairs: Pair<String, Any?>): ContentValues { val cv = ContentValues() for (pair in pairs) { val key = pair.first val value = pair.second when (value) { is Int -> cv.put(key, value) is Long -> cv.put(key, value) is Short -> cv.put(key, value) is Float -> cv.put(key, value) is Double -> cv.put(key, value) is Boolean -> cv.put(key, value) is String -> cv.put(key, value) is Byte -> cv.put(key, value) is ByteArray -> cv.put(key, value) null -> cv.putNull(key) } } return cv }
然后就可以使用如下的写法来简单创建ContentValues对象了:
1 2 val values = cvOf("name" to "Game of Thrones", "author" to "George Martin", "pages" to 720, "price" to 20.85) db.insert("Book", null, values)
泛型的基本用法 泛型主要有两种定义方式,一种是定义泛型类,另一种是定义泛型方法,使用的语法结构都是<T>。当然括号内的T并不是固定要求的,事实上你使用任何英文字母或单词都可以,但是通常情况下,T是一种约定俗成的泛型写法。
如果要定义一个泛型类,就可以这么写:
1 2 3 4 5 6 7 class MyClass<T> { fun method(param: T): T { return param } }
在调用MyClass类和method()方法的时候,可以将泛型指定成具体的类型,如下所示:
1 2 val myClass = MyClass<Int>() val result = myClass.method(123)
而如果不想定义一个泛型类,只是想定义一个泛型方法,只需要将定义泛型的语法结构写在方法上面就可以了,如下所示:
1 2 3 4 5 6 7 class MyClass { fun <T> method(param: T): T { return param } }
此时的调用方式也需要进行相应的调整:
1 2 val myClass = MyClass() val result = myClass.method<Int>(123)
可以看到,现在是在调用method()方法的时候指定泛型类型了。另外,Kotlin还拥有非常出色的类型推导机制,例如传入了一个Int类型的参数,它能够自动推导出泛型的类型就是Int型,因此这里也可以直接省略泛型的指定:
1 2 val myClass = MyClass() val result = myClass.method(123)
类委托 类委托的核心思想在于将一个类的具体实现委托给另一个类去完成。但是委托也有一定的弊端,如果接口中的待实现方法比较少还好,要是有几十甚至上百个方法的话,每个都去这样调用辅助对象中的相应方法实现,写起了就非常复杂了。这个问题在Kotlin中可以通过类委托的功能来解决。
1 2 3 4 5 6 7 class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet { fun helloWorld() = println("Hello World") override fun isEmpty() = false }
现在MySet就成为了一个全新的数据结构类,它不仅永远不会为空,而且还能打印helloWorld(),至于其他Set接口中的功能,则和HashSet保持一致。这就是Kotlin的类委托所能实现的功能。
委托属性 委托属性的核心思想是将一个属性(字段)的具体实现委托给另一个类去完成。
我们看一下委托属性的语法结构,如下所示:
1 2 3 4 5 class MyClass { var p by Delegate() }
这里使用by关键字连接了左边的p属性和右边的Delegate实例,这种写法就代表着将p属性的具体实现委托给了的Delegate类去完成。当调用p属性的时候会自动调用Delegate类的getValue()方法,当给p属性赋值的时候会自动调用Delegate类的setValue()方法。
因此,我们还得对Delegate类进行具体的实现才行,代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 class Delegate { var propValue: Any? = null operator fun getValue(myClass: MyClass, prop: KProperty<*>): Any? { return propValue } operator fun setValue(myClass: MyClass, prop: KProperty<*>, value: Any?) { propValue = value } }
整个委托属性的工作流程就是这样实现的,现在当我们给MyClass的p属性赋值时,就会调用Delegate类的setValue()方法,当获取MyClass中p属性的值时,就会调用Delegate类的getValue()方法。
使用infix函数构建更可读的语法 借助infix函数,我们可以使用一种更具可读性的语法来表达一段代码。
1 infix fun String.beginsWith(prefix: String) = startsWith(prefix)
这里给String类添加了一个beginsWith()函数,它用于判断一个字符串是否是以某个指定参数开头。而加上了infix关键字之后,beginsWith()函数就变成了一个infix函数,这样除了传统的函数调用方式之外,我们还可以用一种特殊的语法糖格式调用beginsWith()函数,如下所示:
1 2 3 if ("Hello Kotlin" beginsWith "Hello") { // 处理具体的逻辑 }
我们还可以给Collection接口添加一个扩展函数:
1 infix fun <T> Collection<T>.has(element: T) = contains(element)
现在我们就可以使用如下的语法来判断集合中是否包括某个指定的元素:
1 2 3 4 val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape") if (list has "Banana") { // 处理具体的逻辑 }
泛型实化 Java中的泛型是通过类型擦除机制来实现的,而Kotlin却允许将内联函数中的泛型进行实化。
要将某个泛型实化需要两个前提条件。首先,该函数必须是内联函数才行,也就是要用inline关键字来修饰该函数。其次,在声明泛型的地方必须加上reified关键字来表示该泛型要进行实化。示例代码如下:
1 inline fun <reified T> getGenericType() = T::class.java
然后我们就可以通过以下代码获得泛型的具体类型了:
1 2 val result1 = getGenericType<String>() val result2 = getGenericType<Int>()
协变和逆变 在开始学习协变和逆变之前,我们还得先了解一个约定。一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为in位置,而它的返回值是输出数据的地方,因此可以称它为out位置,如下图所示。
协变的定义:假如定义了一个MyClass的泛型类,其中A是B的子类型,同时MyClass又是MyClass的子类型,那么我们就可以称MyClass在T这个泛型上是协变的。
逆变的定义:假如定义了一个MyClass<T>的泛型类,其中A是B的子类型,同时MyClass<B>又是MyClass<A>的子类型,那么我们就可以称MyClass在T这个泛型上是逆变的。
观察如下代码,我们在泛型T的声明前面加上了一个out关键字。这就意味着现在T只能出现在out位置上,而不能出现在in位置上,同时也意味着SimpleData在泛型T上是协变的。
1 2 3 4 5 class SimpleData<out T>(val data: T?) { fun get(): T? { return data } }
观察如下代码,我们在泛型T的声明前面加上了一个in关键字。这就意味着现在T只能出现在in位置上,而不能出现在out位置上,同时也意味着Transformer在泛型T上是逆变的。
1 2 3 interface Transformer<in T> { fun transform(t: T): String }
协程的概念 协程和线程是有点类似的,可以简单地将它理解成一种轻量级的线程。
要知道,我们之前所学习的线程是非常重量级的,它需要依靠操作系统的调度才能实现不同线程之间的切换。而使用协程却可以仅在编程语言的层面就能实现不同协程之间的切换,从而大大提升了并发编程的运行效率。
协程的基本用法 Kotlin并没有将协程纳入标准库的API当中,而是以依赖库的形式提供的。所以如果我们想要使用协程功能,需要先在app/build.gradle文件当中添加如下依赖库:
1 2 3 4 5 dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1" }
启动协程最简单的方式就是使用Global.launch函数,如下所示:
1 2 3 4 5 fun main() { GlobalScope.launch { println("codes run in coroutine scope") } }
GlobalScope.launch函数可以创建一个协程的作用域,这样传递给launch函数的代码块(Lambda表达式)就是在协程中运行的了。
runBlocking函数也可以用于启动一个协程,并且会保证在协程作用域内的所有代码和子协程没有全部执行完之前一直阻塞当前线程。
1 2 3 4 5 6 7 fun main() { runBlocking { println("codes run in coroutine scope") delay(1500) println("codes run in coroutine scope finished") } }
使用launch函数可以用于创建多个协程,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 fun main() { runBlocking { launch { println("launch1") delay(1000) println("launch1 finished") } launch { println("launch2") delay(1000) println("launch2 finished") } } }
这里我们调用了两次launch函数,也就是创建了两个子协程。
协程允许我们在单线程模式下模拟多线程编程的效果,代码执行时的挂起与恢复完全是由编程语言来控制的,和操作系统无关。
这种特性使得高并发程序的运行效率得到了极大的提升,试想一下,开启10万个线程完全是不可想象的事吧?而开启10万个协程就是完全可行的。
由于协程相关的内容繁多,更详细的用法,请参考书中内容。
求N个数的最大最小值 我们可以通过自定义一个max函数来获取N个数中的最大值,代码如下所示:
1 2 3 4 5 6 7 fun max(vararg nums: Int): Int { var maxNum = Int.MIN_VALUE for (num in nums) { maxNum = max(maxNum, num) } return maxNum }
仅仅经过这样的一层封装之后,我们在求N个数的最大值就会变得非常简单:
1 2 3 4 val a = 10 val b = 15 val c = 5 val largest = max(a, b, c)
简化Toast的用法 我们可以给String类和Int类各添加一个扩展函数,并在里面封装弹出Toast的具体逻辑。这样以后每次想要弹出Toast提示时,只需要调用它们的扩展函数就可以了。
1 2 3 4 5 6 7 fun String.showToast(context: Context) { Toast.makeText(context, this, Toast.LENGTH_SHORT).show() } fun Int.showToast(context: Context) { Toast.makeText(context, this, Toast.LENGTH_SHORT).show() }
经过这样的扩展之后,我们在使用Toast时就可以变得非常简单了:
1 2 "This is Toast".showToast(context) R.string.app_name.showToast(context)
DSL简介 DSL的全称是领域特定语言(Domain Specific Language),它是编程语言赋予开发者的一种特殊能力,通过它我们可以编写出一些看似脱离其原始语法结构的代码,从而构建出一种专有的语法结构。
构建自己的DSL 借助Kotlin的DSL,我们类似于在Gradle中添加依赖库的语法结构。首先定义一个Dependency类,代码如下所示:
1 2 3 4 5 6 7 8 9 class Dependency { val libraries = ArrayList<String>() fun implementation(lib: String) { libraries.add(lib) } }
接下来再定义一个dependencies高阶函数,代码如下所示:
1 2 3 4 5 fun dependencies(block: Dependency.() -> Unit): List<String> { val dependency = Dependency() dependency.block() return dependency.libraries }
经过这样的DSL设计之后,我们就可以在项目中使用如下的语法结构了:
1 2 3 4 dependencies { implementation("com.squareup.retrofit2:retrofit:2.6.1") implementation("com.squareup.retrofit2:converter-gson:2.6.1") }
另外,我们也可以通过dependencies函数的返回值来获取所有添加的依赖库,代码如下所示:
1 2 3 4 5 6 7 8 9 fun main() { val libraries = dependencies { implementation("com.squareup.retrofit2:retrofit:2.6.1") implementation("com.squareup.retrofit2:converter-gson:2.6.1") } for (lib in libraries) { println(lib) } }